mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-10 06:01:44 +08:00
Mark Kino guides as deep dive
This commit is contained in:
parent
8e0bd198de
commit
db6e459f64
5 changed files with 40 additions and 193 deletions
|
|
@ -64,6 +64,13 @@ defmodule Livebook.Notebook.Explore do
|
||||||
cover_url: "/images/elixir.png"
|
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"),
|
path: Path.join(__DIR__, "explore/intro_to_vega_lite.livemd"),
|
||||||
details: %{
|
details: %{
|
||||||
|
|
@ -74,14 +81,10 @@ defmodule Livebook.Notebook.Explore do
|
||||||
%{
|
%{
|
||||||
path: Path.join(__DIR__, "explore/intro_to_maplibre.livemd"),
|
path: Path.join(__DIR__, "explore/intro_to_maplibre.livemd"),
|
||||||
details: %{
|
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"
|
cover_url: "/images/maplibre.png"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
%{
|
|
||||||
ref: :kino_intro,
|
|
||||||
path: Path.join(__DIR__, "explore/kino/intro_to_kino.livemd")
|
|
||||||
},
|
|
||||||
%{
|
%{
|
||||||
ref: :kino_vm_introspection,
|
ref: :kino_vm_introspection,
|
||||||
path: Path.join(__DIR__, "explore/kino/vm_introspection.livemd")
|
path: Path.join(__DIR__, "explore/kino/vm_introspection.livemd")
|
||||||
|
|
@ -197,12 +200,11 @@ defmodule Livebook.Notebook.Explore do
|
||||||
|
|
||||||
@group_configs [
|
@group_configs [
|
||||||
%{
|
%{
|
||||||
title: "Interactions with Kino",
|
title: "Deep dive into Kino",
|
||||||
description:
|
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",
|
cover_url: "/images/kino.png",
|
||||||
notebook_refs: [
|
notebook_refs: [
|
||||||
:kino_intro,
|
|
||||||
:kino_vm_introspection,
|
:kino_vm_introspection,
|
||||||
:kino_chat_app,
|
:kino_chat_app,
|
||||||
:kino_pong,
|
:kino_pong,
|
||||||
|
|
|
||||||
|
|
@ -41,56 +41,6 @@ return the Elixir version.
|
||||||
Note you can also press <kbd>tab</kbd> to cycle across the completion
|
Note you can also press <kbd>tab</kbd> to cycle across the completion
|
||||||
alternatives.
|
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
|
## Runtimes
|
||||||
|
|
||||||
Livebook has a concept of **runtime**, which in practice is an Elixir node responsible
|
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")
|
Process.put(:info, "deal carefully with process dictionaries")
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- livebook:{"branch_parent_index":4} -->
|
<!-- livebook:{"branch_parent_index":3} -->
|
||||||
|
|
||||||
## More on branches #2
|
## More on branches #2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,12 @@ The above example renders new Markdown output every 100ms.
|
||||||
You can use the same approach to render regular output
|
You can use the same approach to render regular output
|
||||||
or images too!
|
or images too!
|
||||||
|
|
||||||
With this, we finished our introduction to Kino. Now we are
|
With this, we finished our introduction to Kino. Most the guides
|
||||||
ready to bring two concepts we have already learned together:
|
ahead of us will use Kino in one way or the other. You can jump
|
||||||
`Kino` and `VegaLite`. [Let's use them to introspect the Elixir
|
into [the VegaLite guide](/explore/notebooks/intro-to-vega-lite)
|
||||||
runtime your livebooks run on](/explore/notebooks/vm-introspection).
|
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.
|
||||||
|
|
@ -8,8 +8,8 @@ Mix.install([
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Starting from version v0.5, Livebook allows developers to implement
|
Livebook allows developers to implement their own kinos.
|
||||||
their own kinos. This allows developers to bring their own ideas to
|
This allows developers to bring their own ideas to
|
||||||
life and extend Livebook in unexpected ways.
|
life and extend Livebook in unexpected ways.
|
||||||
|
|
||||||
There are two types of custom kinos: static, via `Kino.JS`, and dynamic,
|
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,
|
including persisting the output of your custom kinos to `.livemd` files,
|
||||||
[check out the documentation](https://hexdocs.pm/kino/Kino.JS.html).
|
[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
|
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
|
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
|
something more exciting. Let's use [`Kino.JS.Live`](https://hexdocs.pm/kino/Kino.JS.Live.html)
|
||||||
directly from the Elixir code with [`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
|
||||||
There is a number of different JavaScript packages to pick from
|
synchronize across pages as multiple users access our notebook.
|
||||||
when dealing with maps, for our purpose we will use [Leaflet](https://leafletjs.com),
|
|
||||||
which is an established solution in this area.
|
|
||||||
|
|
||||||
Our custom kino must use both `Kino.JS` (for the assets)
|
Our custom kino must use both `Kino.JS` (for the assets)
|
||||||
and `Kino.JS.Live`. `Kino.JS.Live` works under the client-server
|
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
|
client connects. In here you must return the initial state of
|
||||||
the new client
|
the new client
|
||||||
|
|
||||||
You can also optionally define a `handle_cast/2` callback, responsible
|
* A `handle_event/3` callback, responsible for handling any messages
|
||||||
for handling any messages sent via `Kino.JS.Live.cast/2`. Here is how
|
sent by the client
|
||||||
the code will look like:
|
|
||||||
|
|
||||||
```elixir
|
* A `handle_cast/2` callback, responsible for handling any Elixir
|
||||||
defmodule KinoGuide.Leaflet do
|
messages sent via `Kino.JS.Live.cast/2`
|
||||||
use Kino.JS
|
|
||||||
use Kino.JS.Live
|
|
||||||
|
|
||||||
def new(center, zoom) do
|
Here is how the code will look like:
|
||||||
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: '© <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.
|
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
defmodule KinoGuide.Counter do
|
defmodule KinoGuide.Counter do
|
||||||
|
|
@ -286,11 +177,8 @@ defmodule KinoGuide.Counter do
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
The server mechanics are quite similar to the Leaflet example.
|
On the client side, we wrote some JavaScript that listens to
|
||||||
The only difference is that we have a new callback, `handle_event/3`,
|
button clicks and dispatches them to the server via `pushEvent`.
|
||||||
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`.
|
|
||||||
|
|
||||||
Let's render our counter!
|
Let's render our counter!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,15 @@ Mix.install([
|
||||||
So far we discussed how Livebook supports interactive outputs and
|
So far we discussed how Livebook supports interactive outputs and
|
||||||
covered creating custom outputs with Kino. Livebook v0.6 opens up
|
covered creating custom outputs with Kino. Livebook v0.6 opens up
|
||||||
the door to a whole new category of extensions that we call Smart
|
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
|
Just like the Code and Markdown cells, Smart cells are building
|
||||||
blocks for our notebooks. A Smart cell provides a user interface
|
blocks for our notebooks. A Smart cell provides a user interface
|
||||||
specifically designed for a particular task. Under the hood, each
|
specifically designed for a particular task. While `Kino.JS` and
|
||||||
Smart cell is just a regular piece of code, however the code is
|
`Kino.JS.Live` are about customizing the output, without changing
|
||||||
generated automatically based on the UI interactions.
|
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
|
Smart cells allow for accomplishing high-level tasks faster, without
|
||||||
writing any code, yet, without sacrificing code! This characteristic
|
writing any code, yet, without sacrificing code! This characteristic
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue