Introduction to Kino
Mix.install([
{:kino, "~> 0.8.0"}
])
Introduction
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 widgets
and interact with them. You can see kino
listed as a
dependency above, let’s run the setup cell and get started!
Kino.Input
The Kino.Input
module contains the most common kinos you will use. They are
used to define inputs in one cell, which you can read in a
future cell:
name = Kino.Input.text("Your name")
and now we can greet the user back:
IO.puts("Hello, #{Kino.Input.read(name)}!")
There are multiple types of inputs, such as text areas, color dialogs, selects, and more. Feel free to explore them.
Kino.Markdown
Given our notebooks already know how to render Markdown,
you won’t be surprised to find we can also render Markdown
directly from our Code cells. This is done by wrapping
the Markdown contents in Kino.Markdown.new/1
:
Kino.Markdown.new("""
# Example
A regular Markdown file.
## Code
```elixir
"Elixir" |> String.graphemes() |> Enum.frequencies()
```
## Table
| ID | Name | Website |
| -- | ------ | ----------------------- |
| 1 | Elixir | https://elixir-lang.org |
| 2 | Erlang | https://www.erlang.org |
""")
The way it works is that Livebook automatically detects the output is a kino and renders it in Markdown. That’s the first of many kinos we will explore today. Let’s move forward.
Kino.Mermaid
You can include Mermaid diagrams in Markdown, however when generating diagrams dynamically, use Kino.Mermaid.new/1
. This way the graphs will appear in the notebook source, if the user chooses to persist outputs.
Kino.Mermaid.new("""
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
""")
Kino.DataTable
You can render arbitrary tabular data using Kino.DataTable.new/1
, let’s have a look:
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 or keyword lists.
Now, let’s get some more realistic data. Whenever you run Elixir code, you have several lightweight processes running side-by-side. We can actually gather information about these processes and render it as a table:
keys = [:registered_name, :initial_call, :reductions, :stack_size]
processes =
for pid <- Process.list(),
info = Process.info(pid, keys),
do: info
Kino.DataTable.new(processes)
Now you can use the table above to sort by the number of reductions and identify the most busy processes!
Kino.ETS
Kino supports multiple other data structures to be rendered
as tables. For example, you can use Kino.ETS
to render ETS tables and easily browse their contents.
Let’s first create our own table:
tid = :ets.new(:users, [:set, :public])
Kino.ETS.new(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.Tree
By default cell results are inspected with a limit on the text size. Inspecting large data structures with no limit makes the representation impractical to read, that’s where Kino.Tree
comes in!
data = Process.info(self())
Kino.Tree.new(data)
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"}])
Kino.render("Plain text")
# Some kinos
Kino.render(Kino.Markdown.new("**Hello world**"))
"Cell result 🚀"
Kino.Frame and animations
Kino.Frame
allows us to render an empty frame and update it
as we progress. Let’s render an empty frame:
frame = Kino.Frame.new()
Now, let’s render a random number between 1 and 100 directly in the frame:
Kino.Frame.render(frame, "Got: #{Enum.random(1..100)}")
Notice how every time you reevaluate the cell above it updates
the frame. You can also use Kino.Frame.append/2
to append to
the frame:
Kino.Frame.append(frame, "Got: #{Enum.random(1..100)}")
Appending multiple times will always add new contents. The content
can be reset by calling Kino.Frame.render/2
or Kino.Frame.clear/1
.
By using loops, you can use Kino.Frame
to dynamically add contents
or animate your livebooks. In fact, there is a convenience function
called Kino.animate/2
to be used exactly for this purpose:
Kino.animate(100, fn i ->
Kino.Markdown.new("**Iteration: `#{i}`**")
end)
The above example renders new Markdown output every 100ms. You can use the same approach to render regular output or images too!
There’s also Kino.animate/3
, in case you need to accumulate state or halt the animation at certain point!
button = Kino.Control.button("Click") |> Kino.render()
button
|> Kino.Control.stream()
|> Kino.animate(0, fn _event, counter ->
new_counter = counter + 1
md = Kino.Markdown.new("**Clicks: `#{new_counter}`**")
{:cont, md, new_counter}
end)
Note that this time, instead of refreshing the animation every 100ms, we use an event stream. This way we refresh the animation whenever the button is clicked.
Finally, there’s Kino.listen/{2,3}
, that allows you to consume a stream the same way, but doesn’t render anything on its own.
Kino.Layout
In case you need to arrange multiple kinos, Kino.Layout
gives you some options!
For one, you can create tabs to show just one thing at a time:
data = [
%{id: 1, name: "Elixir", website: "https://elixir-lang.org"},
%{id: 2, name: "Erlang", website: "https://www.erlang.org"}
]
Kino.Layout.tabs(
Table: Kino.DataTable.new(data),
Raw: data
)
Then, there is a simple grid that you can use for laying out multiple elements:
Kino.Layout.grid(["1", "2", "3", "4"], columns: 2)
And you can nest grid any way you like:
urls = [
"https://images.unsplash.com/photo-1603203040743-24aced6793b4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=580&h=580&q=80",
"https://images.unsplash.com/photo-1578339850459-76b0ac239aa2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=580&h=580&q=80",
"https://images.unsplash.com/photo-1633479397973-4e69efa75df2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=580&h=580&q=80",
"https://images.unsplash.com/photo-1597838816882-4435b1977fbe?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=580&h=580&q=80",
"https://images.unsplash.com/photo-1629778712393-4f316eee143e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=580&h=580&q=80",
"https://images.unsplash.com/photo-1638667168629-58c2516fbd22?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=580&h=580&q=80"
]
images =
for {url, i} <- Enum.with_index(urls, 1) do
# For in-memory photo we would use Kino.Image
image = Kino.Markdown.new("")
label = Kino.Markdown.new("**Image #{i}**")
Kino.Layout.grid([image, label], boxed: true)
end
Kino.Layout.grid(images, columns: 3)
dbg
Kino hijacks Elixir’s dbg/2
to provide Kino-based debugging:
dbg(Atom.to_string(:hello))
When debugging a pipeline, Kino will render each step of the pipeline, allowing to inspect, toggle, and swap each operation along the way:
"Elixir is cool!"
|> String.trim_trailing("!")
|> String.split()
|> List.first()
|> dbg()
Next steps with custom Kinos
With this, we finished our introduction to Kino. Most the guides ahead of us will use Kino in one way or the other. You can jump into the VegaLite guide for plotting charts or the MapLibre guide for rendering maps to learn how other packages extend Livebook through Kino.
We also have a collection of deep dive guides into Kino in the Learn page if you want to learn more, including how to create your custom widgets.