Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us
Notesclub

Exploring built-in Kinos

intro_to_kino.livemd

Exploring built-in Kinos

Mix.install([
  {:kino, "~> 0.12.0"}
])

Introduction

Throughout the Learning section, we have used Kino several times. Sometimes we use built-in Kinos, such as using Kino.Control and Kino.Frame to deploy applications, other times we used custom Kinos tailored for data exploration or plotting.

In this notebook, we will explore several of the built-in Kinos. kino is already listed as a dependency, so let’s 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.

One important feature of inputs is that they are shared. Once someone changes an input, it will reflect on all users currently seeing the notebook.

Kino.Control

The Kino.Control module represents forms, buttons, and other interactive controls. Opposite to Kino.Input, each user has their own control and the main way to interact with controls is by listening to their events.

button = Kino.Control.button("Click me!")
Kino.listen(button, fn event -> IO.inspect(event) end)

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 and Kino.Frame

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 🚀"

The Kino.Frame construct we used when deploying our chat app is a generalization of Kino.render, which gives us more control over when to update, append, or clear the output:

frame = Kino.Frame.new()

By default, a frame will update in place. Try running the cell below several times:

Kino.Frame.render(frame, "Got: #{Enum.random(1..100)}")

but you can use append and clear to get different results.

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("![](#{url})")
    label = Kino.Markdown.new("**Image #{i}**")
    Kino.Layout.grid([image, label], boxed: true)
  end

Kino.Layout.grid(images, columns: 3)

Kino.Shorts

The Kino library also provides a Kino.Shorts module, which wraps and unifies most of the functionality we’ve seen so far. Let’s import it and give it a try:

import Kino.Shorts

For example, we use Kino.Input to render and then read an input. With Kino.Shorts, this can be achieved with a single step:

name = read_text("Your name: ")

Once you execute the cell, the input will appear and, the first time around, the value with be an empty string. You can pair this with Kino.interrupt!/2 to read and validate inputs:

name = read_text("Your name: ")

if name == "" do
  Kino.interrupt!(:error, "You must fill in your name")
end

There are several other helpers in Kino.Shorts. For example, in the previous section we used grids with markdown to display and link several images. Using Kino.Shorts, we can simplify it as:

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
    image = markdown("![](#{url})")
    label = markdown("**Image #{i}**")
    grid([image, label], boxed: true)
  end

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

We have learned many new Kinos in this section. In the next guide, we will put some of our new found knowledge into practice by rendering inputs, plotting graphs, and drawing diagrams with information retrieved from the notebook runtime.