Current section

Files

Jump to
livebook lib livebook notebook explore intro_to_kino.livemd
Raw

lib/livebook/notebook/explore/intro_to_kino.livemd

# Interactions with Kino
## Setup
In this notebook we will explore the possibilities that
[`kino`](https://github.com/elixir-nx/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.
```elixir
Mix.install([
{:kino, "~> 0.2.0"},
{:vega_lite, "~> 0.1.0"}
])
```
```elixir
alias VegaLite, as: Vl
```
## Kino.VegaLite
In the [Plotting with VegaLite](/explore/notebooks/intro-to-vega-lite) 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.new/1`. You don't have to specify any data up-front.
```elixir
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.new()
```
Then you can push data to the plot widget at any point and see it update dynamically:
```elixir
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:
```elixir
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.
```elixir
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.new()
|> 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.new/1` to render ETS tables and easily
browse their contents. Let's first create our own table:
```elixir
tid = :ets.new(:users, [:set, :public])
Kino.ETS.new(tid)
```
In fact, Livebook automatically recognises an ETS table and
renders it as such:
```elixir
tid
```
Currently the table is empty, so it's time to insert some rows.
```elixir
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.DataTable
When it comes to tables, we are not limited to ETS! You can render
arbitrary tabular data using `Kino.DataTable.new/1`, let's have
a look:
```elixir
data = [
%{id: 1, name: "Elixir", website: "https://elixir-lang.org"},
%{id: 2, name: "Erlang", website: "https://www.erlang.org"}
]
Kino.DataTable.new(data)
```
The data must be an enumerable, with records being maps,
keyword lists or tuples.
Now, let's get some more realistic data:
```elixir
processes = Process.list() |> Enum.map(&Process.info/1)
```
We can easily pick only the data keys that are relevant
for us:
```elixir
Kino.DataTable.new(
processes,
keys: [:registered_name, :initial_call, :reductions, :stack_size]
)
```
We can sort by the number of reductions to identify the
most busy processes!
## 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.
```elixir
# 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:
```elixir
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.new()
|> 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
```