Powered by AppSignal & Oban Pro

Plotly for Elixir: Fluent Builder

notebooks/02_builder.livemd

Plotly for Elixir: Fluent Builder

Mix.install([
  {:plotly_ex, "~> 0.1"},
  {:kino, "~> 0.18"}
])

Section

The fluent builder gives you precise control over every plotly.js property. Instead of the Express shorthand, you construct trace structs directly and chain Figure operations with |>.

# Aliases used throughout this notebook
alias Plotly.{Figure, Scatter, Bar}

1. Minimal figure

The three-layer model: build a Figure, add a trace struct, render with Plotly.show/1.

Figure.new()
|> Figure.add_trace(Scatter.new(x: [1, 2, 3, 4, 5], y: [2, 1, 4, 3, 5]))
|> Plotly.show()

2. Layout customisation

Figure.update_layout/2 shallow-merges a keyword list (or map) into the figure’s layout. Shallow means if you supply a nested map key like xaxis:, it replaces the entire previous xaxis map rather than deep-merging into it. This is intentional — it matches how plotly.js consumes the layout object.

Figure.new()
|> Figure.add_trace(Scatter.new(
  x: [1, 2, 3, 4, 5],
  y: [2, 1, 4, 3, 5],
  name: "Series A",
  mode: "markers"
))
|> Figure.update_layout(
  title: "Customised Layout",
  xaxis: %{title: "X axis label"},
  yaxis: %{title: "Y axis label"},
  showlegend: true
)
|> Plotly.show()

3. Multi-trace

Call add_trace/2 multiple times. Each call appends to fig.data. The name: field on each trace becomes its legend entry.

Figure.new()
|> Figure.add_trace(Scatter.new(
  x: [1, 2, 3, 4, 5], y: [2, 1, 4, 3, 5], name: "Series A"
))
|> Figure.add_trace(Scatter.new(
  x: [1, 2, 3, 4, 5], y: [1, 3, 2, 5, 4], name: "Series B"
))
|> Figure.update_layout(title: "Two Traces")
|> Plotly.show()

4. Mixed trace types

add_trace/2 accepts any trace struct — mix types freely. Here a Bar trace (monthly revenue) is combined with a Scatter trend line on the same axes. mode: "lines" on the Scatter hides the point markers.

months  = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
revenue = [42, 55, 61, 48, 73, 80]
trend   = [44, 49, 55, 59, 64, 70]

Figure.new()
|> Figure.add_trace(Bar.new(x: months, y: revenue, name: "Revenue"))
|> Figure.add_trace(Scatter.new(x: months, y: trend, name: "Trend", mode: "lines"))
|> Figure.update_layout(title: "Revenue with Trend Line")
|> Plotly.show()

5. Live update

Plotly.show/1 returns a PlotlyLive widget (a Kino.JS.Live process). PlotlyLive.push/2 sends a new figure to that widget, which the browser applies via Plotly.react — a smooth in-place update with no flicker or re-initialisation.

> Pattern — two cells: evaluate Cell A first to create and display the > widget. Then evaluate Cell B to run the animation loop. The widget in > Cell A updates live as Cell B runs.

> Why not Kino.animate? Kino.animate creates a fresh widget for every > frame, causing Plotly.newPlot to fire each time — visible flickering. > PlotlyLive.push/2 uses Plotly.react (diff-based update) instead.

Cell A — create the widget

initial_fig =
  Figure.new()
  |> Figure.add_trace(Scatter.new(x: [0], y: [0.0], mode: "lines", name: "sin(x/3)"))
  |> Figure.update_layout(
    title: "Live Update — Step 0",
    xaxis: %{range: [0, 30]},
    yaxis: %{range: [-1.1, 1.1]}
  )

kino = Plotly.show(initial_fig)

Cell B — run the animation (evaluate after Cell A)

for i <- 1..30 do
  xs = Enum.to_list(0..i)
  ys = Enum.map(xs, fn x -> :math.sin(x / 3.0) end)

  fig =
    Figure.new()
    |> Figure.add_trace(Scatter.new(x: xs, y: ys, mode: "lines", name: "sin(x/3)"))
    |> Figure.update_layout(
      title: "Live Update — Step #{i}",
      xaxis: %{range: [0, 30]},
      yaxis: %{range: [-1.1, 1.1]}
    )

  Plotly.Kino.PlotlyLive.push(kino, fig)
  Process.sleep(100)
end

:ok