bmitc

bmitc

Looking for best way to animate a VegaLite chart in Livebook

I am trying to animate VegaLite charts in Livebook. The idea that I am currently working towards is to animate a 2D histogram over time. I have seen some work (external to Elixir) to specify animations in VegaLite, but what I am wanting is to simply create a VegaLite chart every frame, so I am not looking for super high framerates. Livebook to replicate the issues below. (Note that you need to install Node.js and some packages for exporting VegaLite charts. See here.)

Let’s use this code as example:

alias VegaLite, as: Vl

chart = fn ->
  data =
    for month <- ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] do
      for day <- 1..31 do
        %{month: month, day: day, temperature: Enum.random(32..100)}
      end
    end
    |> List.flatten()

  Vl.new(title: "Daily max temperatures")
  |> Vl.data_from_values(data)
  |> Vl.mark(:rect)
  |> Vl.encode_field(:x, "day", type: :ordinal, title: "Day")
  |> Vl.encode_field(:y, "month", type: :ordinal, title: "Month")
  |> Vl.encode_field(:color, "temperature",
    aggregate: :max,
    type: :quantitative,
    legend: [title: nil]
  )
end

Inside a Livebook cell, I can animate it like this:

Stream.interval(500)
|> Stream.take(10)
|> Kino.animate(nil, fn _, _ ->
  display = chart.()
  {:cont, display, nil}
end)

This works and is fast for animation purposes, but this has highly undesirable blinking and flickering since the chart completely disappears between each frame.

animation1

This code exports the chart to SVG and then creates a Kino image:

Stream.interval(100)
|> Stream.take(100)
|> Kino.animate(nil, fn _, _ ->
  display =
    chart.()
    |> Vl.Export.to_svg()
    |> Kino.Image.new(:svg)

  {:cont, display, nil}
end)

This works as desired to create a smooth animation such that between each frame there is no flickering or resetting of the image, but it is extremely slow. VegaLite.Export.to_svg/1 takes over 1.7 seconds on average. Looking at the implementation, it looks like it has a file write, which may be one reason why it is so slow.

animation2

How can I create the behavior of the second example with the speed of the first?

Marked As Solved

awerment

awerment

Caveat: as I’m definitely not an expert, there might be a better way to do it. Just got curious reading your question and decided to look into it.

Solution I got to work (You’ll need to add the kino_vega_lite dependency for this):

Chart & Data functions

alias VegaLite, as: Vl

chart = fn ->
  Vl.new(title: "Daily max temperatures")
  |> Vl.mark(:rect)
  |> Vl.encode_field(:x, "day", type: :ordinal, title: "Day")
  |> Vl.encode_field(:y, "month", type: :ordinal, title: "Month")
  |> Vl.encode_field(:color, "temperature",
    aggregate: :max,
    type: :quantitative,
    legend: [title: nil]
  )
end

data = fn ->
  for month <- ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] do
    for day <- 1..31 do
      %{month: month, day: day, temperature: Enum.random(32..100)}
    end
  end
  |> List.flatten()
end

Render & update

update = fn chart ->
  data = data.()
  window = Enum.count(data)
  Kino.VegaLite.push_many(chart, data, window: window)
end

chart = chart.() |> Kino.VegaLite.new() |> Kino.render()

# you could use your approach with `Stream.interval/1` here as well
Kino.VegaLite.periodically(chart, 100, nil, fn _ ->
  update.(chart)
  {:cont, nil}
end)

Basically, chart definition and data generation are split and the data points are pushed to the chart.

As push_many only adds new data points to the chart, we have to make sure that old ones are removed from the dataset - that’s what calculating and setting the :window is supposed to achieve.

Also Liked

bmitc

bmitc

Thanks for taking another look! For now, I am using Kino.animate/3 and returning the iterations for it to render, while the loop calls Kino.VegaLite.push_many/3. Something like this:

    Stream.interval(interval_ms)
    |> Stream.take(iterations)
    |> Kino.animate(environment, fn i, env ->
      update.(chart, env)

      <some stuff>

      if stop do
        :halt
      else
        {:cont, "Iteration: #{i + 1}", tick(env)}
      end
    end)

Here’s a video of the animation I got going:

I feel like the first technique should really be working, since it is supposed to be updating a Kino.Frame (my understanding), but I’m not sure why the frame seems to go away between updates.

However, the technique you provided is at least working for my needs currently. Thank you for your help on this! I’m super happy about this.

awerment

awerment

I should also mention that you could use VegaLite.param/3 to bind to input elements; that functionality is built in if you just need controls to modify the chart based on user input.

The proposed Kino.VegaLite.signal/3 function allows updating the values from Elixir code, though. The binding of signal<->control seems to be two-way, so it works as you’d expect.

alias VegaLite, as: Vl

default_stroke_width = 5

chart_spec = fn ->
  Vl.new(width: 400, height: 400)
  |> Vl.param("strokeWidth",
    value: default_stroke_width,
    # bind the signal to a range input, rendered by vega
    bind: [input: "range", min: 1, max: 10, step: 1, name: "Stroke Width"]
  )
  |> Vl.mark(:line, stroke_width: [signal: "strokeWidth"])
  |> Vl.encode_field(:x, "x", type: :quantitative)
  |> Vl.encode_field(:y, "y", type: :quantitative)
end

data = fn ->
  for i <- 1..150, do: %{x: i / 10, y: :math.sin(i / 10)}
end

update = fn chart ->
  data = data.()
  window = Enum.count(data)
  Kino.VegaLite.push_many(chart, data, window: window)
  chart
end

chart = chart_spec.() |> Kino.VegaLite.new() |> update.()
Kino.render(chart)

Kino.Control.button("Reset Stroke Width")
|> Kino.render()
|> Kino.listen(fn _event ->
  Kino.VegaLite.signal(chart, "strokeWidth", default_stroke_width)
end)

awerment

awerment

Sorry for spamming this thread so much, but wanted to leave some additional info for anyone stumbling into [expr: "param_name"] not working for certain chart properties.

You’ll need to consult the vega-lite docs, which properties accept an ExprRef, see for example the title spec. So, a correction for the above: you can dynamically update the chart title by setting it to a param like so:

VegaLite.new(title: [text: [expr: "title_param"]])
|> VegaLite.param("title_param", value: "Default Title")

and updating it later via Kino.VegaLite with

Kino.VegaLite.set_param(chart, "title_param", "Updated Title")

On the other hand, looking at the encoding field definition spec, we see that its title property only accepts Text | Null, which means this will not work:

VegaLite.new(title: "Static Chart Title")
|> VegaLite.param("x_title", value: "Default X-Axis Label")
|> VegaLite.mark(:line)
|> VegaLite.encode_field(:x, "x", type: :quantitative, title: [expr: "x_title"]) # does not accept an 'ExprRef'
|> VegaLite.encode_field(:y, "y", type: :quantitative, title: "Static Y-Axis Label")

Now, as mentioned above, signals are not documented or officially supported by vega-lite (see here and here). That being said… if [expr: "param_name"] does not work, you could try [signal: "param_name"] instead. It does work for the field title, for example:

VegaLite.new(title: "Static Chart Title")
|> VegaLite.param("x_title", value: "Default X-Axis Label")
|> VegaLite.mark(:line)
|> VegaLite.encode_field(:x, "x", type: :quantitative, title: [signal: "x_title"])
|> VegaLite.encode_field(:y, "y", type: :quantitative, title: "Static Y-Axis Label")

Updating it via Kino.VegaLite.set_param/3 works the same.

Just be aware that we’re using a non-public API, strictly speaking.

Where Next?

Popular in Questions Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
nobody
How to bind a phoenix app to a specific ip address? could not find anything about that, nowhere, unfortunately, but for me this is qui...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 53578 245
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
nobody
Hi! In PHP: $SERVER['SERVERADDR'] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New

We're in Beta

About us Mission Statement