Embedded interactive visualisations in Anvil using e.g. Vega Lite?

Hello! I’m new to Anvil but love the simplicity, speed and capabilities - plus my brain can only compute Python so that helps too… well done team!

I have a question about embedding interactive visualisations in Anvil - things like Plotly are good for static visualisations, but recently a project called Vega Lite has come to my attention, which looks like an amazing portable way of defining, sharing and publishing interactive visualisations in a clear, (reasonably) concise and portable way. They even have an awesome JSFiddle-like online editor for testing out code (e.g. this example)

Is this something which could be used in an Anvil app, and if so where would the best place to start be? Any pointers greatly appreciated, thanks in advance!

Yes, it is entirely possible to embed Vega Lite – like any Javascript library – into an Anvil app. You have to write a little bit of Javascript to glue it together, but once that’s done you can use it entirely from Python. Here’s an example app, and a step-by-step guide to how I built it.

Here’s the example app, which embeds the example you just linked to:
https://anvil.works/build#clone:EBK7KDYPKS7V4ADB=YQK2LLDCPHBOZAITZIB3BWS6

And here’s how I did it:

1. Import the Vega embedding libraries

I found the documentation on the Vega Lite website about embedding Vega Lite visualisations into a web app, and I found out what HTML snippet it wanted me to add to my app. I copied-and-pasted these into the Native Libraries section of my app:

<script src="https://cdn.jsdelivr.net/npm/vega@4.4.0"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite@3.0.0-rc12"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@3.29.1"></script>

2. Initialise Vega with Javascript

The website also provided sample Javascript code for setting up a visualisation in a particular HTML element. I made a new Custom HTML form, and I put this code into a function in a <script> tag, so I could call it from Python.

Here’s the whole Custom HTML from the VegaLite form:

<div class="vis"></div>
<script>
  function initVegaLite(spec) {
    var visElt = $(this).find(".vis")[0];
    vegaEmbed(visElt, spec);
  }
</script>

3. Make a Python custom component

I set up my new form as a Custom Component, and gave it a property called vl_spec (type “object”, because it’ll want complex nested dictionaries/lists/etc). I then wrote the Python @property getter and setter to store that property, and call the initVegaLite() Javascript function when the form is shown or when the property is updated.

Here’s the full code for that form:

class VegaLite(VegaLiteTemplate):
  def __init__(self, **properties):
    self._vl_spec = {}
    self._shown = False
    
    self.init_components(**properties)

  @property
  def vl_spec(self):
    return self._vl_spec
  
  @vl_spec.setter
  def vl_spec(self, new_val):
    self._vl_spec = new_val
    if self._shown:
      self.call_js('initVegaLite', self._vl_spec)
    
  def form_show(self, **event_args):
    self._shown = True
    # Fire the setter again to call the init function
    self.vl_spec = self._vl_spec

4. Use the component

Now I’ve created a Custom Component, the VegaLite form is in my Toolbox, ready to use from my other forms. I drop one onto Form1, and then set up its vl_spec from Form1’s __init__ function:

self.vega_lite_1.vl_spec = {...}

(The example vl_spec is copied from the example @jim linked to, although I had to change the data URL from "data/movies.json" to "https://vega.github.io/vega-datasets/data/movies.json")

And there we have it – now you have a component you can use to put any Vega Lite visualisation into an Anvil app. And now we’ve built it, you don’t have to touch the Javascript again – you can do it all from Python!

3 Likes

Thanks so much - that is amazing! And because it has the full spec in the Anvil code, that gives way more opportunity for interactively changing the charts based on other events (as opposed to a straight code embed). Legend, this is quite staggeringly brilliant.

1 Like

@jim In addition to Meredydd’s solution, I’ve been using Vega-Lite via its Python API Altair.

With this approach, I can write the code for the chart using Python instead of JSON. Altair produces the corresponding JSON needed for Vega-Lite. The JSON is plugged into some html, and the html is displayed in an iframe.

Client code

    media=anvil.server.call('make_chart')
    self.i_frame_1.url=media.get_url(True)

Server code

make chart with Altair

    @anvil.server.callable
    def make_chart():
      
        source = pd.DataFrame({
            'a': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
            'b': [28, 55, 43, 91, 81, 53, 19, 87, 52]
        })
        
        c=alt.Chart(source).mark_bar().encode(
            x='a',
            y='b'
        )

grab the JSON and stick it into some html

        altair_spec = c.to_json().replace('\n', '')

        html = """
        <!DOCTYPE html>
        <html>
        <head>

        <script src="https://cdn.jsdelivr.net/npm/vega@4"></script>
        <script src="https://cdn.jsdelivr.net/npm/vega-lite@3.0.0-rc8"></script>
        <script src="https://cdn.jsdelivr.net/npm/vega-embed@3"></script>

        </head>
        <body>
          <div id="vis"></div>
          <script type="text/javascript">
            var spec = {json_spec};
            var embed_opt = {{"mode": "vega-lite", "renderer": "svg"}};

            function showError(el, error){{
                el.innerHTML = ('<div class="error">'
                                + '<p>JavaScript Error: ' + error.message + '</p>'
                                + "<p>This usually means there's a typo in your chart specification. "
                                + "See the javascript console for the full traceback.</p>"
                                + '</div>');
                throw error;
            }}
            const el = document.getElementById('vis');
            vegaEmbed("#vis", spec, embed_opt)
              .catch(error => showError(el, error));
          </script>
        </body>
        </html>
        """.format(json_spec=altair_spec)

return the html as media

        media = anvil.BlobMedia(content_type='text/html', content=html)
        
        return media

An example app (note that you will need the iframe component)

https://anvil.works/build#clone:EKTF2GOLLOTSWKOS=ZIHI6PZRUKESB6GVB4GYV6OJ

1 Like

Even better, you can combine these two approaches! The JSON format returned by Altair is the same JSON format consumed by the Javascript libraries I imported via the Native Libraries.

So, if you wanted to, @alcampopiano, you could replace your code with the following:

Server:

@anvil.server.callable
def make_chart():
  
    source = pd.DataFrame({
        'a': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
        'b': [28, 55, 43, 91, 81, 53, 19, 87, 52]
    })
    
    return alt.Chart(source).mark_bar().encode(
        x='a',
        y='b'
    )

Client:

self.vega_lite_1.vl_spec = anvil.server.call('make_chart')

Simple :slight_smile:

1 Like

That is so awesome. Thanks @meredydd!!

Hi team,

I’m (finally!) trying to build some charts with Altair and I can’t quite get this to work… @meredydd what does vega_lite_1.vl_spec refer to? Do I need an iFrame component?

Either way, when I call the make_chart server function from client side code, I get the following error:

anvil.server.SerializationError: Cannot serialize return value from function. Cannot serialize <class 'altair.vegalite.v3.api.Chart'> object at msg['response']

I’m assuming that there are reasons it’s not simple to install altair on the client like plotly in the dashboard tutorial, however this pattern seems more appropriate to me for front-end (i.e. data visualisation) libraries.

Hi Jim,

The spec refers to the json that vega lite consumes. The error you are getting refers to the fact that you cannot pass an Altair object from the server to the client, but since you can convert that to a dictionary, you are in the clear. In other words, you can simply use Altair’s to_dict() method on the chart object to return a dictionary.

For example:

#On the server
@anvil.server.callable
def make_chart()
    .
    .
    .
  return my_chart.to_dict()
# On the client
# now returning a dictionary
self.vega_lite_1.vl_spec=anvil.server.call("make_chart")

clone:
https://anvil.works/build#clone:J634OC5LZNUY7VGP=JM27IS6673ITRE3OX54ALJ2B

2 Likes

@meredydd thank you for this post, this is amazing! I know I’m coming to it quite late but it addresses exactly what I’ve wanted to do recently. I am using Vega rather than Vega-Lite but this approach works perfectly. I have a couple of follow-up questions:

  1. I’d like to make my plot reactive, i.e. resize it when the parent component is resized. I think I could do this through pure Javascript event capture, without “Anvil” knowing anything about it at all, but I’m wondering if it makes sense to do it through some kind of Anvil functionality - for instance does Anvil already capture resize events?

  2. Would it make any sense to attempt to capture and respond to Vega event streams, like hover or click, within Anvil? For instance, modifying your Step 3 slightly to run straight Vega code rather than vega-embed:

function initVega(spec){
  view = new vega.View(vega.parse(spec), {
    renderer:  'canvas',  // renderer (canvas or svg)
    container: '#vis',   // parent DOM container
    hover:     true       // enable hover processing
  });
  view.runAsync();
}

If this were a pure JS app, I would call return view.runAsync() at the end of that function and be able to use the Vega view API through the returned object. I’m not sure if there is any sensible way to handle the returned object within Anvil.

A use-case for this, for instance, would be to allow Anvil’s client-side-Python code to know which data points on the plot the user has selected, as (I think) it can do for Plotly plots.

Hi @claresloggett,

Glad you found @meredydd’s example useful!

  1. The best way to do this is with Javascript, since you’re driving an external JS library.
  2. Yes, absolutely - I’d start by implementing any functionality you’d like to expose as Javascript functions. You can then use self.call_js() to call these functions from the Python code on your Form, or use anvil.call() to call into your Python from your JS functions.

Thanks @bridget !

For the event streams, the bit I was confused about is how to keep track of the view object in Javascript, if that makes sense. I at first unthinkingly had a return view.runAsync() statement in my initVega() function, but this resulted in an error as self.call_js('initVega') doesn’t know how to handle the returned Javascript View object.

What I’m trying now is creating a global variable in the custom JS snippet and using it to store a persistent object, like

<div id="vis"></div>
<script>
  let vegaView;
  function initVega(spec){
    view = new vega.View(vega.parse(spec), {
      renderer:  'canvas',  // renderer (canvas or svg)
      container: '#vis',   // parent DOM container
      hover:     true       // enable hover processing
    });
    vegaView = view.runAsync();
  }
</script>

I was about to say that this at least doesn’t throw any errors, but actually it throws the error Identifier 'vegaView' has already been declared. Probably this snippet is being loaded more than once? Is there a right way to do it?

Hi @claresloggett,

If you move your let vegaView; statement inside your initVega() function, this should work just fine :slight_smile:

Hi @bridget,

Really? The whole point is that I want the vegaView object to persist and be usable outside the initVega() function - won’t that make it local? And then how do I access it in other function calls? I am not much of a Javascript programmer so I might well be missing the point; sorry if these are basic questions!