livebook/lib/livebook/notebook/explore/intro_to_kino.livemd

4 KiB

Interactions with Kino

Setup

In this notebook we will explore the possibilities that kino brings into your notebooks. Kino can be thought of as Livebook's friend that instructs it how to render certain output and interact with it.

Mix.install([
  {:kino, "~> 0.1.2"},
  {:vega_lite, "~> 0.1.0"}
])
alias VegaLite, as: Vl

VegaLite

In the Plotting with VegaLite notebook we show numerous ways in which you can visualize your data. However, all of the plots there are static.

Using Kino, we can dynamically stream data to the plot, so that it keeps updating! To do that, all you need is a regular VegaLite specification that you then pass to Kino.VegaLite.start/1. You don't have to specify any data up-front.

widget =
  Vl.new(width: 400, height: 400)
  |> Vl.mark(:line)
  |> Vl.encode_field(:x, "x", type: :quantitative)
  |> Vl.encode_field(:y, "y", type: :quantitative)
  |> Kino.VegaLite.start()

Then you can push data to the plot widget at any point and see it update dynamically:

for i <- 1..300 do
  point = %{x: i / 10, y: :math.sin(i / 10)}

  # The :window option ensures we only show the latest
  # 100 data points on the plot
  Kino.VegaLite.push(widget, point, window: 100)

  Process.sleep(25)
end

You can also explicitly clear the data:

Kino.VegaLite.clear(widget)

Periodical updates

You may want to have a plot running forever and updating in the background. There is a dedicated Kino.VegaLite.periodically/4 function that allows you do do just that! You just need to specify the interval and the reducer callback like this, then you interact with the widget as usually.

widget =
  Vl.new(width: 400, height: 400)
  |> Vl.mark(:line)
  |> Vl.encode_field(:x, "x", type: :quantitative)
  |> Vl.encode_field(:y, "y", type: :quantitative)
  |> Kino.VegaLite.start()
  |> Kino.render()

# Add a callback to run every 25ms
Kino.VegaLite.periodically(widget, 25, 0, fn i ->
  point = %{x: i / 10, y: :math.sin(i / 10)}
  # Interacting with the widget is as usual
  Kino.VegaLite.push(widget, point, window: 100)
  # Continue with the new accumulator value
  {:cont, i + 1}
end)

Kino.ETS

You can use Kino.ETS.start/1 to render ETS tables and easily browse their contents. Let's first create our own table:

tid = :ets.new(:users, [:set, :public])
Kino.ETS.start(tid)

In fact, Livebook automatically recognises an ETS table and renders it as such:

tid

Currently the table is empty, so it's time to insert some rows.

for id <- 1..24 do
  :ets.insert(tid, {id, "User #{id}", :rand.uniform(100), "Description #{id}"})
end

Having the rows inserted, click on the "Refetch" icon in the table output above to see them.

Kino.render/1

As we saw, Livebook automatically recognises widgets returned from each cell and renders them accordingly. However, sometimes it's useful to explicitly render a widget in the middle of the cell, similarly to IO.puts/1 and that's exactly what Kino.render/1 does! It works with any type and tells Livebook to render the value in its special manner.

# Arbitrary data structures
Kino.render([%{name: "Ada Lovelace"}, %{name: "Alan Turing"}])

# Static plots
vl =
  Vl.new(width: 400, height: 400)
  |> Vl.data_from_series(x: 1..100, y: 1..100)
  |> Vl.mark(:line)
  |> Vl.encode_field(:x, "x", type: :quantitative)
  |> Vl.encode_field(:y, "y", type: :quantitative)

Kino.render(vl)
Kino.render(vl)

Kino.render("Plain text")

"Cell result 🚀"

Before we saw how you can render and stream data to the plot from a separate cell, the same could be rewritten in one go like this:

widget =
  Vl.new(width: 400, height: 400)
  |> Vl.mark(:line)
  |> Vl.encode_field(:x, "x", type: :quantitative)
  |> Vl.encode_field(:y, "y", type: :quantitative)
  |> Kino.VegaLite.start()
  |> Kino.render()

for i <- 1..300 do
  point = %{x: i / 10, y: :math.sin(i / 10)}
  Kino.VegaLite.push(widget, point, window: 100)
  Process.sleep(25)
end