Update references to kino (#1148)

* Update references to kino

* Update changelog

* Make the Chart cell section branching

* Apply suggestions from code review

Co-authored-by: José Valim <jose.valim@dashbit.co>

* Capitalization

* Add link to the Iris dataset

* Pass keys to Process.info/2

Co-authored-by: José Valim <jose.valim@dashbit.co>
This commit is contained in:
Jonatan Kłosko 2022-04-29 13:45:33 +02:00 committed by GitHub
parent 6884d3cec3
commit 8f72d0175e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 141 additions and 738 deletions

View file

@ -30,6 +30,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Warning when running in the cloud and system memory is low ([#1122](https://github.com/livebook-dev/livebook/pull/1122))
- Notification when a new Livebook version is available ([#1121](https://github.com/livebook-dev/livebook/pull/1121))
- Insert button for Markdown cell with a Mermaid diagram ([#1134](https://github.com/livebook-dev/livebook/pull/1134))
- Taskbar icon for the desktop app ([#1119](https://github.com/livebook-dev/livebook/pull/1119))
- Notebook discussing Smart cells ([#1141](https://github.com/livebook-dev/livebook/pull/1141))
### Changed

View file

@ -30,11 +30,11 @@ defmodule Livebook do
[
%{
dependency: {:kino, "~> 0.5.2"},
dependency: {:kino, "~> 0.6.0"},
description: "Interactive widgets for Livebook",
name: "kino",
url: "https://hex.pm/packages/kino",
version: "0.5.2"
version: "0.6.0"
}
]

View file

@ -71,21 +71,6 @@ defmodule Livebook.Notebook.Explore do
cover_url: "/images/vega_lite.png"
}
},
%{
path: Path.join(__DIR__, "explore/intro_to_nx.livemd"),
details: %{
description:
"Enter Numerical Elixir, experience the power of multi-dimensional arrays of numbers.",
cover_url: "/images/nx.png"
}
},
# %{
# path: Path.join(__DIR__, "explore/intro_to_axon.livemd"),
# details: %{
# description: "Build Neural Networks in Elixir using a high-level, composable API.",
# cover_url: "/images/axon.png"
# }
# },
%{
ref: :kino_intro,
path: Path.join(__DIR__, "explore/kino/intro_to_kino.livemd")

View file

@ -56,7 +56,7 @@ double-click on "Notebook dependencies and setup", add and run the code.
```elixir
Mix.install([
{:kino, "~> 0.5.0"}
{:kino, "~> 0.6.0"}
])
```

View file

@ -1,3 +0,0 @@
# Neural Networks with Axon
TODO: content 🐈

View file

@ -1,641 +0,0 @@
# Introduction to Nx
```elixir
Mix.install([
{:nx, "~> 0.1.0-dev", github: "elixir-nx/nx", sparse: "nx", override: true}
])
```
## Numerical Elixir
Elixir's primary numerical datatypes and structures are not optimized
for numerical programming. Nx is a library built to bridge that gap.
[Elixir Nx](https://github.com/elixir-nx/nx) is a numerical computing library
to smoothly integrate to typed, multidimensional data implemented on other
platforms (called tensors). This support extends to the compilers and
libraries that support those tensors. Nx has three primary capabilities:
* In Nx, tensors hold typed data in multiple, named dimensions.
* Numerical definitions, known as `defn`, support custom code with
tensor-aware operators and functions.
* [Automatic differentiation](https://arxiv.org/abs/1502.05767), also known as
autograd or autodiff, supports common computational scenarios
such as machine learning, simulations, curve fitting, and probabilistic models.
Here's more about each of those capabilities. Nx [tensors]() can hold
unsigned integers (u8, u16, u32, u64),
signed integers (s8, s16, s32, s64),
floats (f32, f64), and brain floats (bf16).
Tensors support backends implemented outside of Elixir, including Google's
Accelerated Linear Algebra (XLA) and LibTorch.
Numerical definitions have compiler support to allow just-in-time compilation
that support specialized processors to speed up numeric computation including
TPUs and GPUs.
To know Nx, we'll get to know tensors first. This rapid overview will touch
on the major libraries. Then, future notebooks will take a deep dive into working
with tensors in detail, autograd, and backends. Then, we'll dive into specific
problem spaces like Axon, the machine learning library.
## Nx and tensors
Systems of equations are a central theme in numerical computing.
These equations are often expressed and solved with multidimensional
arrays. For example, this is a two dimensional array:
$$
\begin{bmatrix}
1 & 2 \\
3 & 4
\end{bmatrix}
$$
Elixir programmers typically express a similar data structure using
a list of lists, like this:
```elixir
[
[1, 2],
[3, 4]
]
```
This data structure works fine within many functional programming
algorithms, but breaks down with deep nesting and random access.
On top of that, Elixir numeric types lack optimization for many numerical
applications. They work fine when programs
need hundreds or even thousands of calculations. They tend to break
down with traditional STEM applications when a typical problem
needs millions of calculations.
In Nx, we express multi-dimensional data using typed tensors. Simply put,
a tensor is a multi-dimensional array with a predetermined shape and
type. To interact with them, Nx relies on tensor-aware operators rather
than `Enum.map/2` and `Enum.reduce/3`.
In this section, we'll look at some of the various tools for
creating and interacting with tensors. The IEx helpers will assist our
exploration of the core tensor concepts.
```elixir
import IEx.Helpers
```
Now, everything is set up, so we're ready to create some tensors.
<!-- livebook:{"break_markdown":true} -->
### Creating tensors
Start out by getting a feel for Nx through its documentation.
Do so through the IEx helpers, like this:
<!-- livebook:{"disable_formatting":true} -->
```elixir
h Nx
```
Immediately, you can see that tensors are at the center of the
API. The main API for creating tensors is `Nx.tensor/2`:
<!-- livebook:{"disable_formatting":true} -->
```elixir
h Nx.tensor
```
We use it to create tensors from raw Elixir lists of numbers, like this:
```elixir
tensor =
1..4
|> Enum.chunk_every(2)
|> Nx.tensor(names: [:y, :x])
```
The result shows all of the major fields that make up a tensor:
* The data, presented as the list of lists `[[1, 2], [3, 4]]`.
* The type of the tensor, a signed integer 64 bits long, with the type `s64`.
* The shape of the tensor, going left to right, with the outside dimensions listed first.
* The names of each dimension.
We can easily convert it to a binary:
```elixir
binary = Nx.to_binary(tensor)
```
A tensor of type s64 uses four bytes for each integer. The binary
shows the individual bytes that make up the tensor, so you can see
the integers `1..4` interspersed among the zeros that make
up the tensor. If all of our data only uses positive numbers from
`0..255`, we could save space with a different type:
```elixir
Nx.tensor([[1, 2], [3, 4]], type: {:u, 8}) |> Nx.to_binary()
```
If you have already have binary, you can directly convert it to a tensor
by passing the binary and the type:
```elixir
Nx.from_binary(<<0, 1, 2>>, {:u, 8})
```
This function comes in handy when working with published datasets
because they must often be processed. Elixir binaries make quick work
of dealing with numerical data structured for platforms other than
Elixir.
We can get any cell of the tensor:
```elixir
tensor[0][1]
```
Now, try getting the first row of the tensor:
```elixir
# ...your code here...
```
We can also get a whole dimension:
```elixir
tensor[x: 1]
```
or a range:
```elixir
tensor[y: 0..1]
```
Now,
* create your own `{3, 3}` tensor with named dimensions
* return a `{2, 2}` tensor containing the first two columns
of the first two rows
We can get information about this most recent term with
the IEx helper `i`, like this:
<!-- livebook:{"disable_formatting":true} -->
```elixir
i tensor
```
The tensor is a struct that supports the usual `Inspect` protocol.
The struct has keys, but we typically treat the `Nx.Tensor`
as an _opaque data type_ (meaning we typically access the contents and
shape of a tensor using the tensor's API instead of the struct).
Primarily, a tensor is a struct, and the
functions to access it go through a specific backend. We'll get to
the backend details in a moment. For now, use the IEx `h` helper
to get more documentation about tensors. We could also open a Code
cell, type Nx.tensor, and hover the cursor over the word `tensor`
to see the help about that function.
We can get the shape of the tensor with `Nx.shape/1`:
```elixir
Nx.shape(tensor)
```
We can also create a new tensor with a new shape using `Nx.reshape/2`:
```elixir
Nx.reshape(tensor, {1, 4}, names: [:batches, :values])
```
This operation reuses all of the tensor data and simply
changes the metadata, so it has no notable cost.
The new tensor has the same type, but a new shape.
Now, reshape the tensor to contain three dimensions with
one batch, one row, and four columns.
```elixir
# ...your code here...
```
We can create a tensor with named dimensions, a type, a shape,
and our target data. A dimension is called an _axis_, and axes
can have names. We can specify the tensor type and dimension names
with options, like this:
```elixir
Nx.tensor([[1, 2, 3]], names: [:rows, :cols], type: {:u, 8})
```
We created a tensor of the shape `{1, 3}`, with the type `u8`,
the values `[1, 2, 3]`, and two axes named `rows` and `cols`.
Now we know how to create tensors, so it's time to do something with them.
## Tensor aware functions
In the last section, we created a `s64[2][2]` tensor. In this section,
we'll use Nx functions to work with it. Here's the value of `tensor`:
```elixir
tensor
```
We can use `IEx.exports/1` or code completion to find
some functions in the `Nx` module that operate on tensors:
<!-- livebook:{"disable_formatting":true} -->
```elixir
exports Nx
```
You might recognize that many of those functions have names that
suggest that they would work on primitive values, called scalars.
Indeed, a tensor can be a scalar:
```elixir
pi = Nx.tensor(3.1415, type: {:f, 32})
```
Take the cosine:
```elixir
Nx.cos(pi)
```
That function took the cosine of `pi`. We can also call them
on a whole tensor, like this:
```elixir
Nx.cos(tensor)
```
We can also call a function that aggregates the contents
of a tensor. For example, to get a sum of the numbers
in `tensor`, we can do this:
```elixir
Nx.sum(tensor)
```
That's `1 + 2 + 3 + 4`, and Nx went to multiple dimensions to get that sum.
To get the sum of values along the `x` axis instead, we'd do this:
```elixir
Nx.sum(tensor, axes: [:x])
```
Nx sums the values across the `x` dimension: `1 + 2` in the first row
and `3 + 4` in the second row.
Now,
* create a `{2, 2, 2}` tensor
* with the values `1..8`
* with dimension names `[:z, :y, :x]`
* calculate the sums along the `y` axis
```elixir
# ...your code here...
```
Sometimes, we need to combine two tensors together with an
operator. Let's say we wanted to subtract one tensor from
another. Mathematically, the expression looks like this:
$$
\begin{bmatrix}
5 & 6 \\
7 & 8
\end{bmatrix} -
\begin{bmatrix}
1 & 2 \\
3 & 4
\end{bmatrix} =
\begin{bmatrix}
4 & 4 \\
4 & 4
\end{bmatrix}
$$
To solve this problem, add each integer on the left with the
corresponding integer on the right. Unfortunately, we cannot
use Elixir's built-in subtraction operator as it is not tensor-aware.
Luckily, we can use the `Nx.subtract/2` function to solve the
problem:
```elixir
tensor2 = Nx.tensor([[5, 6], [7, 8]])
Nx.subtract(tensor2, tensor)
```
We get a `{2, 2}` shaped tensor full of fours, exactly as we expected.
When calling `Nx.subtract/2`, both operands had the same shape.
Sometimes, you might want to process functions where the dimensions
don't match. To solve this problem, Nx takes advantage of
a concept called _broadcasting_.
## Broadcasts
Often, the dimensions of tensors in an operator don't match.
For example, you might want to subtract a `1` from every
element of a `{2, 2}` tensor, like this:
$$
\begin{bmatrix}
1 & 2 \\
3 & 4
\end{bmatrix} - 1 =
\begin{bmatrix}
0 & 1 \\
2 & 3
\end{bmatrix}
$$
Mathematically, it's the same as this:
$$
\begin{bmatrix}
1 & 2 \\
3 & 4
\end{bmatrix} -
\begin{bmatrix}
1 & 1 \\
1 & 1
\end{bmatrix} =
\begin{bmatrix}
0 & 1 \\
2 & 3
\end{bmatrix}
$$
That means we need a way to convert `1` to a `{2, 2}` tensor.
`Nx.broadcast/2` solves that problem. This function takes
a tensor or a scalar and a shape.
```elixir
Nx.broadcast(1, {2, 2})
```
This broadcast takes the scalar `1` and translates it
to a compatible shape by copying it. Sometimes, it's easier
to provide a tensor as the second argument, and let `broadcast/2`
extract its shape:
```elixir
Nx.broadcast(1, tensor)
```
The code broadcasts `1` to the shape of `tensor`. In many operators
and functions, the broadcast happens automatically:
```elixir
Nx.subtract(tensor, 1)
```
This result is possible because Nx broadcasts _both tensors_
in `subtract/2` to compatible shapes. That means you can provide
scalar values as either argument:
```elixir
Nx.subtract(10, tensor)
```
Or subtract a row or column. Mathematically, it would look like this:
$$
\begin{bmatrix}
1 & 2 \\
3 & 4
\end{bmatrix} -
\begin{bmatrix}
1 & 2
\end{bmatrix} =
\begin{bmatrix}
0 & 0 \\
2 & 2
\end{bmatrix}
$$
which is the same as this:
$$
\begin{bmatrix}
1 & 2 \\
3 & 4
\end{bmatrix} -
\begin{bmatrix}
1 & 2 \\
1 & 2
\end{bmatrix} =
\begin{bmatrix}
0 & 0 \\
2 & 2
\end{bmatrix}
$$
This rewrite happens in Nx too, also through a broadcast. We want to
broadcast the tensor `[1, 2]` to match the `{2, 2}` shape, like this:
```elixir
Nx.broadcast(Nx.tensor([1, 2]), {2, 2})
```
The `subtract` function in `Nx` takes care of that broadcast
implicitly, as before:
```elixir
Nx.subtract(tensor, Nx.tensor([1, 2]))
```
The broadcast worked as advertised, copying the `[1, 2]` row
enough times to fill a `{2, 2}` tensor. A tensor with a
dimension of `1` will broadcast to fill the tensor:
```elixir
[[1], [2]] |> Nx.tensor() |> Nx.broadcast({1, 2, 2})
```
```elixir
[[[1, 2, 3]]]
|> Nx.tensor()
|> Nx.broadcast({4, 2, 3})
```
Both of these examples copy parts of the tensor enough
times to fill out the broadcast shape. You can check out the
Nx broadcasting documentation for more details:
<!-- livebook:{"disable_formatting":true} -->
```elixir
h Nx.broadcast
```
Much of the time, you won't have to broadcast yourself. Many of
the functions and operators Nx supports will do so automatically.
We can use tensor-aware operators via various `Nx` functions and
many of them implicitly broadcast tensors.
Throughout this section, we have been invoking `Nx.subtract/2` and
our code would be more expressive if we could use its equivalent
mathematical operator. Fortunately, Nx provides a way. Next, we'll
dive into numerical definitions using `defn`.
## Numerical definitions (defn)
The `defn` macro simplifies the expression of mathematical formulas
containing tensors. Numerical definitions have two primary benefits
over classic Elixir functions.
* They are _tensor-aware_. Nx replaces operators like `Kernel.-/2`
with the `Defn` counterparts &mdash; which in turn use `Nx` functions
optimized for tensors &mdash; so the formulas we express can use
tensors out of the box.
* `defn` definitions allow for building computation graph of all the
individual operations and using a just-in-time (JIT) compiler to emit
highly specialized native code for the desired computation unit.
We don't have to do anything special to get access to
get tensor awareness beyond importing `Nx.Defn` and writing
our code within a `defn` block.
To use Nx in a Mix project or a notebook, we need to include
the `:nx` dependency and import the `Nx.Defn` module. The
dependency is already included, so import it in a Code cell,
like this:
```elixir
import Nx.Defn
```
Just as the Elixir language supports `def`, `defmacro`, and `defp`,
Nx supports `defn`. There are a few restrictions. It allows only
numerical arguments in the form of primitives or tensors as arguments
or return values, and has a subset of the language within
the `defn` block.
The subset of Elixir allowed within `defn` is quite broad, though. We can
use macros, pipes, and even conditionals, so we're not giving up
much when you're declaring mathematical functions.
Additionally, despite these small concessions, `defn` provides huge benefits.
Code in a `defn` block uses tensor aware operators and types, so the math
beneath your functions has a better chance to shine through. Numerical
definitions can also run on accelerated numerical processors like GPUs and
TPUs. Here's an example numerical definition:
```elixir
defmodule TensorMath do
import Nx.Defn
defn subtract(a, b) do
a - b
end
end
```
This module has a numerical definition that will be compiled.
If we wanted to specify a compiler for this module, we could add
a module attribute before the `defn` clause. One of such compilers
is [the EXLA compiler](https://github.com/elixir-nx/nx/tree/main/exla).
You'd add the `mix` dependency for EXLA and do this:
<!-- livebook:{"force_markdown":true} -->
```elixir
@defn_compiler EXLA
defn subtract(a, b) do
a - b
end
```
Now, it's your turn. Add a `defn` to `TensorMath`
that accepts two tensors representing the lengths of sides of a
right triangle and uses the pythagorean theorem to return the
[length of the hypotenuse](https://www.mathsisfun.com/pythagoras.html).
Add your function directly to the previous Code cell.
The last major feature we'll cover is called auto-differentiation, or autograd.
## Automatic differentiation (autograd)
An important mathematical property for a function is the
rate of change, or the gradient. These gradients are critical
for solving systems of equations and building probabilistic
models. In advanced math, derivatives, or differential equations,
are used to take gradients. Nx can compute these derivatives
automatically through a feature called automatic differentiation,
or autograd.
Here's how it works.
<!-- livebook:{"disable_formatting":true} -->
```elixir
h Nx.Defn.grad
```
We'll build a module with a few functions,
and then create another function to create the gradients of those
functions. The function `grad/1` takes a function, and returns
a function returning the gradient. We have two functions: `poly/1`
is a simple numerical definitin, and `poly_slope_at/1` returns
its gradient:
$$
poly: f(x) = 3x^2 + 2x + 1 \\
$$
$$
polySlopeAt: g(x) = 6x + 2
$$
Here's the Elixir equivalent of those functions:
```elixir
defmodule Funs do
defn poly(x) do
3 * Nx.power(x, 2) + 2 * x + 1
end
def poly_slope_at(x) do
grad(&poly/1).(x)
end
end
```
Notice the second `defn`. It uses `grad/1` to take its
derivative using autograd. It uses the intermediate `defn` AST
and mathematical composition to compute the derivative. You can
see it at work here:
```elixir
Funs.poly_slope_at(2)
```
Nice. If you plug the number 2 into the function $$ 6x + 2 $$
you get 14! Said another way, if you look at the graph at
exactly 2, the rate of increase is 14 units of `poly(x)`
for every unit of `x`, precisely at `x`.
Nx also has helpers to get gradients corresponding to a number of inputs.
These come into play when solving systems of equations.
Now, you try. Find a function computing the gradient of a `sin` wave.
```elixir
# your code here
```

View file

@ -2,8 +2,8 @@
```elixir
Mix.install([
{:vega_lite, "~> 0.1.2"},
{:kino, "~> 0.5.0"}
{:vega_lite, "~> 0.1.4"},
{:kino_vega_lite, "~> 0.1.0"}
])
```
@ -14,8 +14,8 @@ We need two libraries for plotting in Livebook:
* The [`vega_lite`](https://github.com/elixir-nx/vega_lite)
package allows us to define our graph specifications
* The [`kino`](https://github.com/elixir-nx/kino) package
renders our specifications
* The [`kino_vega_lite`](https://github.com/elixir-nx/kino) package
instructs Livebook how to render our specifications
Let's install them by running the setup cell above.
@ -28,6 +28,69 @@ alias VegaLite, as: Vl
<!-- livebook:{"branch_parent_index":0} -->
## The Chart smart cell
Before we get into exploring all the various chart types, let's have
a look at an awesome feature that comes with `kino_vega_lite` - the
**Chart** smart cell!
Coding up a chart usually involves a couple steps. If you don't know
the API of a particular library, it may be a bit challenging. On the
other hand, if you know the API in-and-out, it's a rather repetitive
task. That's where the **Chart** smart cell comes in, it is a
high-level UI that helps us write our chart code. It is great
a tool for learning and for automating our workflows. Let's
give it a try!
First, we need some data to work with, here's a small excerpt from
the popular [Iris dataset](https://www.kaggle.com/datasets/uciml/iris):
```elixir
iris = [
%{"petal_length" => 5.1, "petal_width" => 1.9, "species" => "Iris-virginica"},
%{"petal_length" => 4.0, "petal_width" => 1.3, "species" => "Iris-versicolor"},
%{"petal_length" => 1.6, "petal_width" => 0.2, "species" => "Iris-setosa"},
%{"petal_length" => 1.6, "petal_width" => 0.2, "species" => "Iris-setosa"},
%{"petal_length" => 4.6, "petal_width" => 1.4, "species" => "Iris-versicolor"},
%{"petal_length" => 4.8, "petal_width" => 1.8, "species" => "Iris-virginica"},
%{"petal_length" => 5.6, "petal_width" => 2.2, "species" => "Iris-virginica"},
%{"petal_length" => 5.1, "petal_width" => 1.6, "species" => "Iris-versicolor"},
%{"petal_length" => 1.5, "petal_width" => 0.3, "species" => "Iris-setosa"},
%{"petal_length" => 4.5, "petal_width" => 1.6, "species" => "Iris-versicolor"}
]
:ok
```
Now, to insert a new Chart cell, place your cursor between cells,
click on the <kbd>+ Smart</kbd> button and select Chart.
You can see an example of that below. Click on the "Evaluate"
button to see the chart, then see how it changes as you customize
the parameters.
<!-- livebook:{"attrs":{"chart_title":"Iris","height":200,"layers":[{"chart_type":"point","color_field":"species","color_field_aggregate":null,"color_field_type":null,"data_variable":"iris","x_field":"petal_length","x_field_aggregate":null,"x_field_type":"quantitative","y_field":"petal_width","y_field_aggregate":null,"y_field_type":"quantitative"}],"vl_alias":"Elixir.Vl","width":400},"kind":"Elixir.KinoVegaLite.ChartCell","livebook_object":"smart_cell"} -->
```elixir
Vl.new(width: 400, height: 200, title: "Iris")
|> Vl.data_from_values(iris, only: ["petal_length", "petal_width", "species"])
|> Vl.mark(:point)
|> Vl.encode_field(:x, "petal_length", type: :quantitative)
|> Vl.encode_field(:y, "petal_width", type: :quantitative)
|> Vl.encode_field(:color, "species")
```
Under cell actions there is a "Source" button, click on it to
see the source code of your chart. You can even convert it to
a regular Code cell for further adjustments!
The Chart smart cell is one of many Smart cells available
in Livebook ⚡ Not only that, you can create your own Smart
cells too, which we discuss in the
[Exploring Smart cells](/explore/notebooks/smart-cells) notebook!
<!-- livebook:{"branch_parent_index":0} -->
## Basic concepts
Composing a basic Vega-Lite graphic usually consists of the following steps:
@ -36,7 +99,7 @@ Composing a basic Vega-Lite graphic usually consists of the following steps:
# Initialize the specification, optionally with some top-level properties
Vl.new(width: 400, height: 400)
# Specify data source for the graphic using one of the data_from_* functions
|> Vl.data_from_series(iteration: 1..100, score: 1..100)
|> Vl.data_from_values(iteration: 1..100, score: 1..100)
# Pick a visual mark
|> Vl.mark(:line)
# Map data fields to visual properties of the mark, in this case point positions
@ -437,7 +500,7 @@ the shape, conflating the two dimensions.
# Source: https://vega.github.io/vega-lite/examples/arc_radial.html
Vl.new()
|> Vl.data_from_series(data: [12, 23, 47, 6, 52, 19])
|> Vl.data_from_values(data: [12, 23, 47, 6, 52, 19])
|> Vl.encode_field(:theta, "data", type: :quantitative, stack: true)
|> Vl.encode_field(:radius, "data", scale: [type: :sqrt, zero: true, range_min: 20])
|> Vl.encode_field(:color, "data", type: :nominal, legend: nil)

View file

@ -2,7 +2,7 @@
```elixir
Mix.install([
{:kino, "~> 0.5.0"}
{:kino, "~> 0.6.0"}
])
```

View file

@ -2,7 +2,7 @@
```elixir
Mix.install([
{:kino, "~> 0.5.0"}
{:kino, "~> 0.6.0"}
])
```
@ -43,10 +43,8 @@ end
Let's break it down.
To define a custom kino we need to create a new module,
conventionally under the `Kino.` prefix, so that the end
user can easily autocomplete all available kinos. In
this case we went with `KinoGuide.HTML`.
To define a custom kino we need to create a new module. In
this case we go with `KinoGuide.HTML`.
We start by adding `use Kino.JS`, which makes our module
asset-aware. In particular, it allows us to use the `asset/2`

View file

@ -2,7 +2,7 @@
```elixir
Mix.install([
{:kino, "~> 0.5.0"}
{:kino, "~> 0.6.0"}
])
```
@ -96,16 +96,14 @@ side-by-side. We can actually gather information about these
processes and render it as a table:
```elixir
processes = Process.list() |> Enum.map(&Process.info/1)
```
keys = [:registered_name, :initial_call, :reductions, :stack_size]
We can pick the data keys that are relevant for us:
processes =
for pid <- Process.list(),
info = Process.info(pid, keys),
do: info
```elixir
Kino.DataTable.new(
processes,
keys: [:registered_name, :initial_call, :reductions, :stack_size]
)
Kino.DataTable.new(processes)
```
Now you can use the table above to sort by the number of

View file

@ -2,7 +2,7 @@
```elixir
Mix.install([
{:kino, "~> 0.5.0"}
{:kino, "~> 0.6.0"}
])
```

View file

@ -2,7 +2,7 @@
```elixir
Mix.install([
{:kino, github: "livebook-dev/kino"},
{:kino, "~> 0.6.0"},
{:jason, "~> 1.3"}
])
```

View file

@ -2,8 +2,8 @@
```elixir
Mix.install([
{:vega_lite, "~> 0.1.2"},
{:kino, "~> 0.5.0"}
{:kino, "~> 0.6.0"},
{:kino_vega_lite, "~> 0.1.0"}
])
alias VegaLite, as: Vl

View file

@ -102,11 +102,11 @@ defmodule Livebook.Runtime.Dependencies do
## Examples
iex> Livebook.Runtime.Dependencies.parse_term(~s|{:kino, "~> 0.5.0"}|)
{:ok, {:kino, "~> 0.5.0"}}
iex> Livebook.Runtime.Dependencies.parse_term(~s|{:jason, "~> 1.3.0"}|)
{:ok, {:jason, "~> 1.3.0"}}
iex> Livebook.Runtime.Dependencies.parse_term(~s|{:kino, "~> 0.5.0", runtime: false, meta: 'data'}|)
{:ok, {:kino, "~> 0.5.0", runtime: false, meta: 'data'}}
iex> Livebook.Runtime.Dependencies.parse_term(~s|{:jason, "~> 1.3.0", runtime: false, meta: 'data'}|)
{:ok, {:jason, "~> 1.3.0", runtime: false, meta: 'data'}}
iex> Livebook.Runtime.Dependencies.parse_term(~s|%{name: "Jake", numbers: [1, 2, 3.4]}|)
{:ok, %{name: "Jake", numbers: [1, 2, 3.4]}}

View file

@ -17,37 +17,38 @@ defmodule Livebook.Runtime.ElixirStandalone do
server_pid: pid() | nil
}
kino_dep = {:kino, github: "livebook-dev/kino"}
kino_vega_lite_dep = {:kino_vega_lite, "~> 0.1.0"}
kino_db_dep = {:kino_db, "~> 0.1.0"}
@extra_smart_cell_definitions [
%{
kind: "Elixir.Kino.SmartCell.DBConnection",
kind: "Elixir.KinoDB.ConnectionCell",
name: "Database connection",
requirement: %{
name: "Kino",
name: "KinoDB",
variants: [
%{name: "PostgreSQL", dependencies: [kino_dep, {:postgrex, "~> 0.16.1"}]},
%{name: "MySQL", dependencies: [kino_dep, {:myxql, "~> 0.6.1"}]}
%{name: "PostgreSQL", dependencies: [kino_db_dep, {:postgrex, "~> 0.16.3"}]},
%{name: "MySQL", dependencies: [kino_db_dep, {:myxql, "~> 0.6.2"}]}
]
}
},
%{
kind: "Elixir.Kino.SmartCell.SQL",
kind: "Elixir.KinoDB.SQLCell",
name: "SQL query",
requirement: %{
name: "Kino",
name: "KinoDB",
variants: [
%{name: "Default", dependencies: [kino_dep]}
%{name: "Default", dependencies: [kino_db_dep]}
]
}
},
%{
kind: "Elixir.Kino.SmartCell.ChartBuilder",
name: "Chart builder",
kind: "Elixir.KinoVegaLite.ChartCell",
name: "Chart",
requirement: %{
name: "Kino",
name: "KinoVegaLite",
variants: [
%{name: "Default", dependencies: [kino_dep, {:vega_lite, "~> 0.1.3"}]}
%{name: "Default", dependencies: [kino_vega_lite_dep]}
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View file

@ -5,23 +5,23 @@ defmodule Livebook.Runtime.DependenciesTest do
doctest Dependencies
@kino {:kino, "~> 0.5.0"}
@jason {:jason, "~> 1.3.0"}
describe "add_mix_deps/2" do
test "prepends Mix.install/2 call if there is none" do
assert Dependencies.add_mix_deps("", [@kino]) ==
assert Dependencies.add_mix_deps("", [@jason]) ==
{:ok,
"""
Mix.install([
{:kino, "~> 0.5.0"}
{:jason, "~> 1.3.0"}
])\
"""}
assert Dependencies.add_mix_deps("# Comment", [@kino]) ==
assert Dependencies.add_mix_deps("# Comment", [@jason]) ==
{:ok,
"""
Mix.install([
{:kino, "~> 0.5.0"}
{:jason, "~> 1.3.0"}
])
# Comment\
@ -37,12 +37,12 @@ defmodule Livebook.Runtime.DependenciesTest do
# Final comment\
""",
[@kino]
[@jason]
) ==
{:ok,
"""
Mix.install([
{:kino, "~> 0.5.0"}
{:jason, "~> 1.3.0"}
])
# Outer comment
@ -62,13 +62,13 @@ defmodule Livebook.Runtime.DependenciesTest do
{:req, "~> 0.2.0"}
])\
""",
[@kino]
[@jason]
) ==
{:ok,
"""
Mix.install([
{:req, "~> 0.2.0"},
{:kino, "~> 0.5.0"}
{:jason, "~> 1.3.0"}
])\
"""}
@ -84,7 +84,7 @@ defmodule Livebook.Runtime.DependenciesTest do
# Result
:ok\
""",
[@kino]
[@jason]
) ==
{:ok,
"""
@ -92,7 +92,7 @@ defmodule Livebook.Runtime.DependenciesTest do
Mix.install([
# Inner comment leading
{:req, "~> 0.2.0"},
{:kino, "~> 0.5.0"}
{:jason, "~> 1.3.0"}
# Inner comment trailing
])
@ -111,14 +111,14 @@ defmodule Livebook.Runtime.DependenciesTest do
]
)\
""",
[@kino]
[@jason]
) ==
{:ok,
"""
Mix.install(
[
{:req, "~> 0.2.0"},
{:kino, "~> 0.5.0"}
{:jason, "~> 1.3.0"}
],
system_env: [
# {"XLA_TARGET", "cuda111"}
@ -130,34 +130,34 @@ defmodule Livebook.Runtime.DependenciesTest do
test "does not add the dependency if it already exists" do
code = """
Mix.install([
{:kino, "~> 0.5.2"}
{:jason, "~> 1.3.0"}
])\
"""
assert Dependencies.add_mix_deps(code, [@kino]) == {:ok, code}
assert Dependencies.add_mix_deps(code, [@jason]) == {:ok, code}
code = """
Mix.install([
{:kino, "~> 0.5.2", runtime: false}
{:jason, "~> 1.3.0", runtime: false}
])\
"""
assert Dependencies.add_mix_deps(code, [@kino]) == {:ok, code}
assert Dependencies.add_mix_deps(code, [@jason]) == {:ok, code}
end
test "given multiple dependencies adds the missing ones" do
assert Dependencies.add_mix_deps(
"""
Mix.install([
{:kino, "~> 0.5.2"}
{:jason, "~> 1.3.0"}
])\
""",
[{:vega_lite, "~> 0.1.3"}, {:kino, "~> 0.5.0"}, {:req, "~> 0.2.0"}]
[{:vega_lite, "~> 0.1.3"}, {:jason, "~> 1.3.0"}, {:req, "~> 0.2.0"}]
) ==
{:ok,
"""
Mix.install([
{:kino, "~> 0.5.2"},
{:jason, "~> 1.3.0"},
{:vega_lite, "~> 0.1.3"},
{:req, "~> 0.2.0"}
])\
@ -165,11 +165,11 @@ defmodule Livebook.Runtime.DependenciesTest do
code = """
Mix.install([
{:kino, "~> 0.5.2", runtime: false}
{:jason, "~> 1.3.0", runtime: false}
])\
"""
assert Dependencies.add_mix_deps(code, [@kino]) == {:ok, code}
assert Dependencies.add_mix_deps(code, [@jason]) == {:ok, code}
end
test "returns an error if the code has a syntax error" do
@ -178,7 +178,7 @@ defmodule Livebook.Runtime.DependenciesTest do
# Comment
[,1]
""",
[@kino]
[@jason]
) ==
{:error,
"""

View file

@ -142,7 +142,7 @@ defmodule Livebook.SessionTest do
Session.subscribe(session.id)
Session.add_dependencies(session.pid, [{:kino, "~> 0.5.0"}])
Session.add_dependencies(session.pid, [{:jason, "~> 1.3.0"}])
session_pid = session.pid
assert_receive {:operation, {:apply_cell_delta, ^session_pid, "setup", :primary, _delta, 1}}
@ -154,7 +154,7 @@ defmodule Livebook.SessionTest do
%{
source: """
Mix.install([
{:kino, "~> 0.5.0"}
{:jason, "~> 1.3.0"}
])\
"""
}
@ -173,7 +173,7 @@ defmodule Livebook.SessionTest do
Session.subscribe(session.id)
Session.add_dependencies(session.pid, [{:kino, "~> 0.5.0"}])
Session.add_dependencies(session.pid, [{:json, "~> 1.3.0"}])
assert_receive {:error, "failed to add dependencies to the setup cell, reason:" <> _}
end

View file

@ -905,12 +905,12 @@ defmodule LivebookWeb.SessionLiveTest do
# Search the predefined dependencies in the embedded runtime
search_view
|> element(~s{form[phx-change="search"]})
|> render_change(%{"search" => "ki"})
|> render_change(%{"search" => "ja"})
page = render(view)
assert page =~ "kino"
assert page =~ "Interactive widgets for Livebook"
assert page =~ "0.5.2"
assert page =~ "jason"
assert page =~ "A blazing fast JSON parser and generator in pure Elixir"
assert page =~ "1.3.0"
end
end

View file

@ -22,11 +22,11 @@ defmodule Livebook.Runtime.Embedded.Dependencies do
def entries() do
[
%{
dependency: {:kino, "~> 0.5.2"},
description: "Interactive widgets for Livebook",
name: "kino",
url: "https://hex.pm/packages/kino",
version: "0.5.2"
dependency: {:jason, "~> 1.3.0"},
description: "A blazing fast JSON parser and generator in pure Elixir",
name: "jason",
url: "https://hex.pm/packages/jason",
version: "1.3.0"
}
]
end