# Run as: iex --dot-iex path/to/notebook.exs # Title: Exploring built-in Kinos Mix.install([ {:kino, "~> 0.18.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](/learn/notebooks/deploy-apps) # or for [plotting](/learn/notebooks/intro-to-vega-lite). # 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 ── (⎇ from Introduction) # The [`Kino.Input`](https://hexdocs.pm/kino/Kino.Input.html) # 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 ── (⎇ from Introduction) # The [`Kino.Control`](https://hexdocs.pm/kino/Kino.Control.html) # 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 ── (⎇ from Introduction) # 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`](https://hexdocs.pm/kino/Kino.Markdown.html): # 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 ── (⎇ from Introduction) # 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 ── (⎇ from Introduction) # You can render arbitrary tabular data using [`Kino.DataTable.new/1`](https://hexdocs.pm/kino/Kino.DataTable.html), 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 ── (⎇ from Introduction) # Kino supports multiple other data structures to be rendered # as tables. For example, you can use [`Kino.ETS`](https://hexdocs.pm/kino/Kino.ETS.html) # 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 ── (⎇ from Introduction) # 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 ── (⎇ from Introduction) # 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 ── (⎇ from Introduction) # 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 ── (⎇ from Introduction) # The `Kino` library also provides a [`Kino.Shorts`](https://hexdocs.pm/kino/Kino.Shorts.html) # 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 ── (⎇ from Introduction) # Kino hijacks Elixir's [`dbg/2`](https://hexdocs.pm/elixir/Kernel.html#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](/learn/notebooks/vm-introspection).