2024-10-22
Code notebooks, which can provide an interactive combination of code, data, AI, text and visualizations, have seen increasing use in recent years. Livebook is such a notebook environment that has some interesting features to explore. In this article I will write a basic Livebook notebook to show off some these features. This notebook will describe information from the Green Lantern series of comic books. In the Green Lantern series there exists a book called the Book of Oa
that contains knowledge about Green Lanterns and it can convey this in a variety of forms. Inspired by this, the notebook in this article will show information about Green Lanterns in a variety of ways: text, code, visualizations and even with some AI applications.
If one wants to follow along with the full source of the notebook presented here it can be found in this repo on GitHub.
In order to run Livebook one needs to install the Livebook app, either locally or in the cloud. The application is itself a web application that the user, or even multiple users, can connect to. Within the app one can create a new notebook, or open an existing one.
In a new notebook the first step is usually installing some dependencies for the specific visualizations and AI tasks that we aim to do. Many of these can be added 'on demand' when we want to create them during the development, but here we just load in all the dependencies we need for the example notebook in one go:
Mix.install([
{:kino_maplibre, "~> 0.1.12"},
{:kino_vega_lite, "~> 0.1.11"},
{:kino_bumblebee, "~> 0.5.0"},
{:exla, "~> 0.7.1"}
],
config: [
nx: [
default_backend: EXLA.Backend,
default_defn_options: [compiler: EXLA]
]
],
system_env: [
XLA_TARGET: "cpu",
]
)
In Livebook the main language for code blocks is Elixir, a functional programming language in which Livebook itself was written. These code blocks can be evaluated, which in this case results in the atom:ok
being returned, denoting that this setup went well. Atoms in Elixir are constants whose value is their name. They are often used to note the state of an operation, in this case :ok
showing success, or distinct values, an example for which we will see later.
Aside from code blocks, another type of element we can add to these notebooks is text. For example a brief part of the introduction in the notebook contains the following text using Markdown:
"Green Lanterns are superheroes that are part of an intergalactic law enforcement agency called the Green Lantern Corps. They derive their powers through their Power Rings, which they aim to control through their willpower. The Power Rings give the Green Lanters various powers such as flight, creating forcefields and contructs made for energy. The headquarters of the Green Lanterns are on the planet Oa, which is the home planet of the Guardians of the Universe. On the planet there is the Main Power Battery that powers all rings, as well as the Book of Oa (from which the name of this article is derived) which contains the laws and history of the Green Lantern Corps. Aside from Markdown, Livebooks can have many other types of content. For example we can create a diagram for the First Appearance of a number of Green Lanterns using the built in support for Mermaid.js."
As mentioned previously, there are many other options for different types of content. For example creating diagrams using Mermaid.js can be a breeze. The following block creates a diagram of the timelines of the first appearances of some of the Green Lanterns:
timeline
title First Appearance
1959: Hal Jordan
1968: Guy Gardner
1972: John Stewart
1994: Kyle Rayner
2012: Simon Baz
2014: Jessica Cruz
2020: Sojurner Mullein
Code blocks can do more than just set up our notebook. For example we can define a struct to describe data relating to a Lantern.
defmodule Lantern do
defstruct name: "", color: :green, sector: "", home_town: "", appearances: 0
end
Without diving deep into Elixir semantics, the Lantern struct holds data about the name, color, sector, home town and appearances of a Lantern, with some basic default values. Notably the default value for the color of the Lanterns is green, denoted by the atom :green
.
With these structs we can specify the data of six Green Lanterns from earth, and put them in a list named green_lanterns_from_earth
.
hal_jordan = %Lantern{name: "Hal Jordan", sector: 2814, home_town: "Coast City, California", appearances: 5396 }
guy_gardner = %Lantern{name: "Guy Gardner", sector: 2814, home_town: "Baltimore", appearances: 1631 }
john_stewart= %Lantern{name: "John Stewart", sector: 2814, home_town: "Detroit, Michigan", appearances: 1866}
kyle_rayner = %Lantern{name: "Kyle Rayner", sector: 2814, home_town: "Los Angeles, California", appearances: 1696}
simon_baz = %Lantern{name: "Simon Baz", sector: 2814, home_town: "Dearborn, Michigan", appearances: 443}
jessica_cruz= %Lantern{name: "Jessica Cruz", sector: 2814, home_town: "Portland, Oregon", appearances: 431}
sojourner_mullein = %Lantern{name: "Sojourner Mullein", sector: 2814, home_town: "New York City, New York", appearances: 72}
green_lanterns_from_earth = [hal_jordan, guy_gardner, john_stewart, kyle_rayner, simon_baz, jessica_cruz, sojourner_mullein]
As these Green Lanterns all have a hometown on Earth we can also specify the coordinates of these locations.
hometown_coordinates = %{
"coordinates" => ["37.865894, -122.498055", "39.299236, -76.609383", "42.331429, -83.045753", "34.052235, -118.243683", "42.322262, -83.176315", "45.523064, -122.676483", "40.730610, -73.935242"],
"name" => ["Coast City", "Baltimore", "Detroit", "Los Angeles", "Dearborn", "Portland", "New York City"]
}
The above coordinates can be used to create a visualization of a map marked with the hometowns of the Lanterns. Although this map could be created programmatically, Livebook has the notion of smart cells
with which UI components can be rapidly created, without reaching for code. The following map was created using this feature.
In addition to Green Lanterns there are also Lanterns of other colors in the comics as well. Thankfully our Lantern
struct also allows us to specify these:
atrocitus = %Lantern{name: "Atrocitus", color: :red, sector: 666, appearances: 321}
bleez = %Lantern{name: "Bleez", color: :red, sector: 33, appearances: 198}
red_lanterns = [atrocitus, bleez]
We can combine the two lists of Lanterns into one with some Elixir code.
lanterns = green_lanterns_from_earth ++ red_lanterns
Now that we have all these lanterns defined, we can create a visualization that lists all lanterns with their respective appearances. The first step towards this to create a data structure combining together the lanterns names, number of appearances and color. The follow code creates this:
lantern_appearances = Enum.reduce(lanterns, %{lanterns: [], appearances: [], color: []}, fn lantern, acc ->
%{
lanterns: acc.lanterns ++ [lantern.name],
appearances: acc.appearances ++ [lantern.appearances],
color: acc.color ++ [lantern.color]
}
end)
Which results in the following data when evaluated:
%{
color: [:green, :green, :green, :green, :green, :green, :green, :red, :red],
appearances: [5396, 1631, 1866, 1696, 443, 431, 72, 321, 198],
lanterns: ["Hal Jordan", "Guy Gardner", "John Stewart", "Kyle Rayner", "Simon Baz",
"Jessica Cruz", "Sojourner Mullein", "Atrocitus", "Bleez"]
}
This data can be used to create our visualization. Instead of using a smart cell, here we programmatically create the chart with the following code:
VegaLite.new(title: "Lantern Appearances")
|> VegaLite.data_from_values(lantern_appearances, only: ["lanterns", "appearances", "color"])
|> VegaLite.mark(:bar)
|> VegaLite.encode_field(:x, "lanterns", type: :nominal)
|> VegaLite.encode_field(:y, "appearances", type: :quantitative)
|> VegaLite.encode_field(:color, "color",
type: :nominal,
title: "Lantern Color",
scale: [
domain: ["green", "red"],
range: ["green", "red"]
])
Evaluation of the above the code results in the following bar chart:
Finally, we can also add some AI functionality to round out our notebook. For example using a smart cell we can add a question answering system with only a few clicks:
Even AI based image generation can be embedded with smart cells. We can use this to generate an image of a Green Lantern power ring.
Of course this is just a very short glimpse of the things that a Livebook based notebook can do. One feature that is very useful in practice is that Livebook notebooks are saved as livemd
files which are a subset of markdown files. This makes them very suitable choice for writing documentation (such as for Github which renders livemd
files are markdown) and are easily used with version control. Another feature we did not touch on here is using real-time collaboration on the notebooks when using Livebook.
Hopefully with this article I helped to shine some light on the nice features of the Livebook environment. Feel free to take a look on Livebook's website for more details on using it.