Engineering

Let the force be with Livebook

| 3 min read

Do you remember the time when you were watching Star Wars and were breath-taken by starships crossing the lightspeed and instantly travelling to another galaxy? If so, I would like to show you how to play with Star Wars API (SWAPI) using Elixir Livebook. You can follow the link to learn more about SWAPI.

What is the Livebook?

You've probably heard about Jupyter which is a notebook for Python which is willingly used by data scientists and ML engineers. Inspired by its success and based on powerful Phoenix Live View this kind of notebook was created for the Elixir ecosystem

Preparing our environment

Let's clone the official Github repo and then install dependencies:

git clone https://github.com/elixir-nx/livebook.git
cd livebook
mix deps.get --only prod

# Run the Livebook server
MIX_ENV=prod mix phx.server

Create a new notebook and name it whatever you want. Did everything go smoothly? Now, we have to start by installing the required dependencies.

Mix.install([
  {:tesla, "~> 1.4"},
  {:jason, ">= 1.0.0"},
  {:vega_lite, "~> 0.1.0"},
  {:kino, "~> 0.2.0"}
])

mix install dependencies

Tesla and Jason let us play with an external API and Vega Lite together with Kino are required to use charts.

Creating a module with a client function will simplify our requests later on. We can obtain it by using Tesla.client/2 function with the middlewares list inside.

defmodule ForceClient do
  require Tesla

  def client(url) do
    Tesla.client([
      {Tesla.Middleware.BaseUrl, url},
      Tesla.Middleware.JSON
    ])
  end
end

creating tesla client

We can fetch our data now. For this example, I will get https:/swapi.dev/api/vehicles using previously created client and Tesla.get/3 function.

url = "https://swapi.dev/api"

{:ok, %{body: body}} = ForceClient.client(url) |> Tesla.get("/vehicles")

basic swapi request

The fastest one is the coolest? Let's try to get name of the fastest vehicle. We have to get the object with the highest "max_atmosphering_speed" and then get its name.

body["results"]
|> Enum.reduce(fn x, acc ->
  if String.to_integer(x["max_atmosphering_speed"]) <
       String.to_integer(acc["max_atmosphering_speed"]) do
    acc
  else
    x
  end
end)
|> Map.get("name")

getting the fastest vehicle

Now we would like to see our data with diagrams. We will start with adding an alias:

alias VegaLite, as: Vl

elixir alias

After that, we can create our first chart. It's going to be a line chart of max atmospheric speed and length relationship. First, we have to parse our data to provide a suitable format.

data =
  body["results"]
  |> Enum.map(
    &%{
      "length" => elem(Float.parse(&1["length"]), 0),
      "max_atmosphering_speed" => elem(Float.parse(&1["max_atmosphering_speed"]), 0)
    }
  )
[
  %{"length" => 36.8, "max_atmosphering_speed" => 30.0},
  %{"length" => 10.4, "max_atmosphering_speed" => 1.2e3},
  %{"length" => 3.4, "max_atmosphering_speed" => 250.0},
    ...
]

And then we can create a graph using Vl.new/1 and passing our data.

Vl.new(width: 750, height: 400)
|> Vl.data_from_values(data)
|> Vl.mark(:line)
|> Vl.encode_field(:x, "length")
|> Vl.encode_field(:y, "max_atmosphering_speed")

max atmosphering speed to length line chart

The next thing we will prepare is a pie chart of the count of vehicles having a specific class. As previously, we have to prepare data:

data = 
  body["results"]
  |> Enum.frequencies_by(&Map.get(&1, "vehicle_class"))
  |> Enum.map(fn {k, v} -> %{"Class" => k, "value" => v} end)
[
    %{"Class" => "airspeeder", "value" => 1},
  %{"Class" => "assault walker", "value" => 1},
  %{"Class" => "repulsorcraft", "value" => 3},
    ...
]

And then generate pie chart:

Vl.new(width: 600, height: 400)
|> Vl.data_from_values(data)
|> Vl.mark(:arc)
|> Vl.encode_field(:theta, "value", type: :quantitative)
|> Vl.encode_field(:color, "Class", type: :nominal)
|> Vl.config(view: [stroke: nil])

vehicle class pie chart

The last thing we would like to do is a pretty table generated with markdown. Kino library provides an easy way to deal with it, so don't waste time and prepare it using Kino.Markdown.new/1 function.

"""
## Pretty table

| Name | Vehicle class | Manufacturer |
| -----| ------------- | ------------ |
#{Enum.map(body["results"], &("| #{&1["name"]} | #{&1["vehicle_class"]} | #{&1["manufacturer"]} |\n"))}
"""
|> Kino.Markdown.new()

drawing markdown tables

Summary

Did you enjoy it? Livebook provides much more interesting features, which you can find in the official examples. Here you can get the whole notebook file.

We'd love to build something amazing together!

Make the first step for a great partnership! Share your idea with us and check what we can do for you and your company.

Start a project
ClutchFinancial TimesForbes