Mark Kino guides as deep dive

This commit is contained in:
José Valim 2022-07-25 21:41:27 +02:00
parent 8e0bd198de
commit db6e459f64
5 changed files with 40 additions and 193 deletions

View file

@ -64,6 +64,13 @@ defmodule Livebook.Notebook.Explore do
cover_url: "/images/elixir.png"
}
},
%{
path: Path.join(__DIR__, "explore/intro_to_kino.livemd"),
details: %{
description: "Make your notebooks interactive with inputs, controls, and more with the Kino package.",
cover_url: "/images/kino.png"
}
},
%{
path: Path.join(__DIR__, "explore/intro_to_vega_lite.livemd"),
details: %{
@ -74,14 +81,10 @@ defmodule Livebook.Notebook.Explore do
%{
path: Path.join(__DIR__, "explore/intro_to_maplibre.livemd"),
details: %{
description: "Learn how to seamlessly plot maps using geospatial and tabular data.",
description: "Seamlessly plot maps using geospatial and tabular data.",
cover_url: "/images/maplibre.png"
}
},
%{
ref: :kino_intro,
path: Path.join(__DIR__, "explore/kino/intro_to_kino.livemd")
},
%{
ref: :kino_vm_introspection,
path: Path.join(__DIR__, "explore/kino/vm_introspection.livemd")
@ -197,12 +200,11 @@ defmodule Livebook.Notebook.Explore do
@group_configs [
%{
title: "Interactions with Kino",
title: "Deep dive into Kino",
description:
"Kino is an Elixir package for displaying and controlling rich, interactive widgets in Livebook. Learn how to make your notebooks more engaging with inputs, plots, tables, and much more!",
"Advanced guides for learning more about the Kino package, including the creation of custom UI components.",
cover_url: "/images/kino.png",
notebook_refs: [
:kino_intro,
:kino_vm_introspection,
:kino_chat_app,
:kino_pong,

View file

@ -41,56 +41,6 @@ return the Elixir version.
Note you can also press <kbd>tab</kbd> to cycle across the completion
alternatives.
## Using packages
Sometimes you need a dependency or two and notebooks are no exception to this.
In Livebook, you can use [`Mix.install/2`](https://hexdocs.pm/mix/Mix.html#install/2)
to bring dependencies into your notebook! This approach is especially useful when
sharing notebooks because everyone will be able to get the same dependencies.
Installing dependencies is a one-off setup operation and for those we use a special
setup cell. Copy the code below, then scroll to the very top of the notebook,
double-click on "Notebook dependencies and setup", add and run the code.
<!-- livebook:{"force_markdown":true} -->
```elixir
Mix.install([
{:kino, "~> 0.6.1"}
])
```
**Note:** compiling dependencies may use a reasonable amount of memory. If you are
hosting Livebook, make sure you have enough memory allocated to the Livebook
instance, otherwise the installation will fail.
<!-- livebook:{"break_markdown":true} -->
### Kino
The package we installed is [Kino](https://github.com/elixir-nx/kino) - a library
that allows you to control parts of Livebook directly from the Elixir code. Let's
use it to print a data table:
```elixir
data = [
%{id: 1, name: "Elixir", website: "https://elixir-lang.org"},
%{id: 2, name: "Erlang", website: "https://www.erlang.org"}
]
Kino.DataTable.new(data)
```
There is much more to `Kino` and we have [a series of Kino guides
in the Explore section to teach you more](/explore).
Note that it is a good idea to specify versions of the installed packages,
so that the notebook is easily reproducible later on. The install
command goes beyond simply installing dependencies, it also caches
them, consolidates protocols, and more. Check
[its documentation](https://hexdocs.pm/mix/Mix.html#install/2)
to learn more.
## Runtimes
Livebook has a concept of **runtime**, which in practice is an Elixir node responsible
@ -126,7 +76,7 @@ parent = self()
Process.put(:info, "deal carefully with process dictionaries")
```
<!-- livebook:{"branch_parent_index":4} -->
<!-- livebook:{"branch_parent_index":3} -->
## More on branches #2

View file

@ -207,7 +207,12 @@ The above example renders new Markdown output every 100ms.
You can use the same approach to render regular output
or images too!
With this, we finished our introduction to Kino. Now we are
ready to bring two concepts we have already learned together:
`Kino` and `VegaLite`. [Let's use them to introspect the Elixir
runtime your livebooks run on](/explore/notebooks/vm-introspection).
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](/explore/notebooks/intro-to-vega-lite)
for plotting charts or [the MapLibre guide](/explore/notebooks/intro-to-vega-lite)
for rendering maps.
We also have a collection of deep dive guides into Kino in the
[Explore](/explore) page if you want to learn more, including how
to create your custom widgets.

View file

@ -8,8 +8,8 @@ Mix.install([
## Introduction
Starting from version v0.5, Livebook allows developers to implement
their own kinos. This allows developers to bring their own ideas to
Livebook allows developers to implement their own kinos.
This allows developers to bring their own ideas to
life and extend Livebook in unexpected ways.
There are two types of custom kinos: static, via `Kino.JS`, and dynamic,
@ -81,16 +81,14 @@ To learn more about other features provided by `Kino.JS`,
including persisting the output of your custom kinos to `.livemd` files,
[check out the documentation](https://hexdocs.pm/kino/Kino.JS.html).
## Dynamic maps with Leaflet
## Bidirectional live counter
Kinos with static data are useful, but they offer just a small peek
into what can be achieved with custom kinos. This time we will try out
something more exciting. We will set up a simple map and then push points
directly from the Elixir code with [`Kino.JS.Live`](https://hexdocs.pm/kino/Kino.JS.Live.html).
There is a number of different JavaScript packages to pick from
when dealing with maps, for our purpose we will use [Leaflet](https://leafletjs.com),
which is an established solution in this area.
something more exciting. Let's use [`Kino.JS.Live`](https://hexdocs.pm/kino/Kino.JS.Live.html)
to build a counter that can be incremented both through Elixir calls
and client interactions. Not only that, our counter will automatically
synchronize across pages as multiple users access our notebook.
Our custom kino must use both `Kino.JS` (for the assets)
and `Kino.JS.Live`. `Kino.JS.Live` works under the client-server
@ -106,120 +104,13 @@ and other Elixir behaviours). In particular, we need to define:
client connects. In here you must return the initial state of
the new client
You can also optionally define a `handle_cast/2` callback, responsible
for handling any messages sent via `Kino.JS.Live.cast/2`. Here is how
the code will look like:
* A `handle_event/3` callback, responsible for handling any messages
sent by the client
```elixir
defmodule KinoGuide.Leaflet do
use Kino.JS
use Kino.JS.Live
* A `handle_cast/2` callback, responsible for handling any Elixir
messages sent via `Kino.JS.Live.cast/2`
def new(center, zoom) do
Kino.JS.Live.new(__MODULE__, {normalize_location(center), zoom})
end
def add_marker(kino, location) do
Kino.JS.Live.cast(kino, {:add_marker, normalize_location(location)})
end
@impl true
def init({center, zoom}, ctx) do
{:ok, assign(ctx, center: center, zoom: zoom, locations: [])}
end
@impl true
def handle_connect(ctx) do
data = %{
center: ctx.assigns.center,
zoom: ctx.assigns.zoom,
locations: ctx.assigns.locations
}
{:ok, data, ctx}
end
@impl true
def handle_cast({:add_marker, location}, ctx) do
broadcast_event(ctx, "add_marker", location)
ctx = update(ctx, :locations, &[location | &1])
{:noreply, ctx}
end
defp normalize_location({lag, lng}), do: [lag, lng]
asset "main.js" do
"""
import * as L from "https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet-src.esm.js";
export async function init(ctx, data) {
ctx.root.style.height = "400px";
// Leaflet requires styles to be present before creating the map,
// so we await for the import to finish
await ctx.importCSS("https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet.css");
const { center, zoom, locations } = data;
const map = L.map(ctx.root, { center, zoom });
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
locations.forEach((location) => {
L.marker(location).addTo(map);
});
ctx.handleEvent("add_marker", (location) => {
L.marker(location).addTo(map);
});
}
"""
end
end
```
This is a bit more code, however the flow is very straightforward.
The map is initialized with the central location and zoom, we store
those in the server state and pass to each client when they connect.
Additionally we keep a list of locations that we want to mark on the
map. The public `add_marker` function allows for pushing new locations
to the server, in which case we send the it to the client. On the
client we render all initial markers we get and subscribe to any new
that appear later on.
Note that we keep track of all locations on the server, this way
whenever a new user joins the page, we can send them all of the
locations we already have. To verify this behaviour you can refresh
the page and you should see all of the markers still in place. Feel
free to try this out in separte browser tabs too!
```elixir
map = KinoGuide.Leaflet.new({51.505, -0.09}, 13)
```
The below cell marks a random location, so you can evaluate it
multiple times for better results.
```elixir
delta = fn -> (:rand.uniform() - 0.5) * 0.05 end
KinoGuide.Leaflet.add_marker(map, {51.505 + delta.(), -0.09 + delta.()})
```
We barely scratched the surface of maps, the Leaflet API alone is extremely
extensive and there are other packages worth exploring. However, even with
this simple kino we could already visualize some geographic data in real-time!
## Bidirectional live counter
The map example reiterated how we can send events from the server
to the clients, however communication in the other direction is
possible as well!
Let's build a counter that can be incremented both through Elixir
calls and client interactions.
Here is how the code will look like:
```elixir
defmodule KinoGuide.Counter do
@ -286,11 +177,8 @@ defmodule KinoGuide.Counter do
end
```
The server mechanics are quite similar to the Leaflet example.
The only difference is that we have a new callback, `handle_event/3`,
for handling events sent from the client. On the client side,
this is done by listening to button clicks and dispatching them
to the server via `pushEvent`.
On the client side, we wrote some JavaScript that listens to
button clicks and dispatches them to the server via `pushEvent`.
Let's render our counter!

View file

@ -12,13 +12,15 @@ Mix.install([
So far we discussed how Livebook supports interactive outputs and
covered creating custom outputs with Kino. Livebook v0.6 opens up
the door to a whole new category of extensions that we call Smart
cells
cells.
Just like the Code and Markdown cells, Smart cells are building
blocks for our notebooks. A Smart cell provides a user interface
specifically designed for a particular task. Under the hood, each
Smart cell is just a regular piece of code, however the code is
generated automatically based on the UI interactions.
specifically designed for a particular task. While `Kino.JS` and
`Kino.JS.Live` are about customizing the output, without changing
the runtime, Smart cells run just like a regular piece of code,
however the code is generated automatically based on the UI
interactions.
Smart cells allow for accomplishing high-level tasks faster, without
writing any code, yet, without sacrificing code! This characteristic