Chart Events
Mix.install([
{:plotly_ex, "~> 0.1"},
{:kino, "~> 0.18"}
])
Chart Events > Click Event Data
PlotlyLive.subscribe/2 registers the current process (or a given pid) to receive
{tag, event_map} messages whenever a Plotly event fires in the browser.
Click a data point in the chart below, then evaluate the receive cell to see the event payload.
alias Plotly.{Figure, Scatter}
alias Plotly.Kino.PlotlyLive
chart =
Figure.new()
|> Figure.add_trace(Scatter.new(
x: [1, 2, 3, 4, 5],
y: [1, 4, 9, 16, 25],
mode: "markers",
marker: %{size: 15, color: "steelblue"},
text: ["A", "B", "C", "D", "E"]
))
|> Figure.update_layout(title: "Click any point — then run the next cell")
|> PlotlyLive.new()
PlotlyLive.subscribe(chart, :click_data, :click)
chart
# Click a point above, then evaluate this cell
receive do
{:click_data, event} ->
IO.inspect(event, label: "click event", pretty: true)
after
10_000 -> "No click within 10 s — click a point first, then re-run this cell"
end
Chart Events > Binding to Click Events
Spawn a dedicated listener process and subscribe it so click events stream
into a Kino.Frame in real time — no need to re-evaluate cells.
alias Plotly.{Figure, Scatter}
alias Plotly.Kino.PlotlyLive
output = Kino.Frame.new()
chart =
Figure.new()
|> Figure.add_trace(Scatter.new(
x: [1, 2, 3, 4, 5],
y: [2, 4, 1, 5, 3],
mode: "markers+text",
text: ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"],
textposition: "top center",
marker: %{size: 12}
))
|> Figure.update_layout(title: "Click points — output appears below")
|> PlotlyLive.new()
# Spawn a persistent listener process and subscribe IT to click events
listener =
spawn(fn ->
for _ <- 1..1000 do
receive do
{:click, event} ->
[point | _] = event["data"]["points"] || []
text = "**Clicked:** x=#{point["x"]}, y=#{point["y"]}, label=#{point["text"]}"
Kino.Frame.render(output, Kino.Markdown.new(text))
after
60_000 -> :timeout
end
end
end)
PlotlyLive.subscribe(chart, listener, :click, :click)
Kino.Layout.grid([chart, output])
Chart Events > Create Annotation on Click Event
On each click, add a text annotation at the clicked point by updating the figure
with a new layout.annotations list via PlotlyLive.push/2.
alias Plotly.{Figure, Scatter}
alias Plotly.Kino.PlotlyLive
# Initial scatter trace
base_fig =
Figure.new()
|> Figure.add_trace(Scatter.new(
x: Enum.to_list(1..10),
y: [5, 2, 8, 1, 6, 3, 9, 4, 7, 2],
mode: "markers",
marker: %{size: 12, color: "coral"}
))
|> Figure.update_layout(title: "Click to annotate points")
chart = PlotlyLive.new(base_fig)
# Listener: accumulates annotations and pushes updated figure
defmodule AnnotationLoop do
def run(chart, base_fig, annotations) do
receive do
{:annotate, event} ->
[point | _] = event["data"]["points"] || []
new_ann = %{
x: point["x"],
y: point["y"],
text: "#{point["x"]}, #{point["y"]}",
showarrow: true,
arrowhead: 4,
ax: 30,
ay: -30
}
updated_annotations = [new_ann | annotations]
updated_fig = Figure.update_layout(base_fig, annotations: updated_annotations)
PlotlyLive.push(chart, updated_fig)
run(chart, base_fig, updated_annotations)
after
60_000 -> :ok
end
end
end
listener = spawn(fn -> AnnotationLoop.run(chart, base_fig, []) end)
PlotlyLive.subscribe(chart, listener, :annotate, :click)
chart
Chart Events > Hover Event Data
plotly_hover fires when the cursor enters a data point’s hover region.
The event payload contains a "points" list — same structure as click events.
alias Plotly.{Figure, Scatter}
alias Plotly.Kino.PlotlyLive
chart =
Figure.new()
|> Figure.add_trace(Scatter.new(
x: [1, 2, 3, 4, 5],
y: [10, 15, 13, 17, 12],
mode: "lines+markers",
marker: %{size: 10}
))
|> Figure.update_layout(title: "Hover over points")
|> PlotlyLive.new()
PlotlyLive.subscribe(chart, :hover_data, :hover)
chart
# Hover over the chart above, then run this cell
receive do
{:hover_data, event} ->
IO.inspect(event, label: "hover event", pretty: true)
after
10_000 -> "No hover within 10 s"
end
Chart Events > Capturing Hover Events: Data
Display x/y values, trace name, and point index from hover events in a live table.
alias Plotly.{Figure, Scatter}
alias Plotly.Kino.PlotlyLive
output = Kino.Frame.new()
chart =
Figure.new()
|> Figure.add_trace(Scatter.new(
x: Enum.to_list(1..10),
y: [5, 2, 8, 1, 6, 3, 9, 4, 7, 2],
name: "Series A",
mode: "lines+markers"
))
|> Figure.add_trace(Scatter.new(
x: Enum.to_list(1..10),
y: [3, 6, 2, 7, 4, 8, 1, 5, 3, 9],
name: "Series B",
mode: "lines+markers"
))
|> Figure.update_layout(title: "Hover to capture data", hovermode: "closest")
|> PlotlyLive.new()
listener =
spawn(fn ->
for _ <- 1..1000 do
receive do
{:hover, event} ->
[pt | _] = event["data"]["points"] || []
md = """
| Field | Value |
|-------|-------|
| x | #{pt["x"]} |
| y | #{pt["y"]} |
| trace | #{pt["name"]} |
| curveNumber | #{pt["curveNumber"]} |
| pointIndex | #{pt["pointIndex"]} |
"""
Kino.Frame.render(output, Kino.Markdown.new(md))
after 60_000 -> :timeout
end
end
end)
PlotlyLive.subscribe(chart, listener, :hover, :hover)
Kino.Layout.grid([chart, output])
Chart Events > Capturing Hover Events: Pixels
The plotly_hover event data includes bbox with pixel bounding box of the
hovered point — useful for positioning custom tooltips or UI elements.
Note: the bbox field ({x0, x1, y0, y1}) gives the bounding box of the
hovered element in pixel coordinates relative to the plot div.
alias Plotly.{Figure, Scatter}
alias Plotly.Kino.PlotlyLive
output = Kino.Frame.new()
chart =
Figure.new()
|> Figure.add_trace(Scatter.new(
x: [1, 2, 3, 4, 5],
y: [4, 2, 6, 1, 5],
mode: "markers",
marker: %{size: 20, color: "steelblue"}
))
|> Figure.update_layout(title: "Hover — pixel coordinates shown below")
|> PlotlyLive.new()
listener =
spawn(fn ->
for _ <- 1..1000 do
receive do
{:hover_px, event} ->
[pt | _] = event["data"]["points"] || []
bbox = pt["bbox"] || %{}
md = "**Hover bbox (pixels):** x0=#{bbox["x0"]}, x1=#{bbox["x1"]}, y0=#{bbox["y0"]}, y1=#{bbox["y1"]}"
Kino.Frame.render(output, Kino.Markdown.new(md))
after 60_000 -> :timeout
end
end
end)
PlotlyLive.subscribe(chart, listener, :hover_px, :hover)
Kino.Layout.grid([chart, output])
Chart Events > Triggering Hover Events
N/A — JavaScript-only API
The JS example calls
Plotly.Fx.hover(graphDiv, [{curveNumber:0, pointNumber:0}])to programmatically trigger a hover — a browser DOM API with no Elixir equivalent.For Livebook, hover effects are handled automatically by the browser when the user moves the mouse over data points. No Elixir code required.
Chart Events > Coupled Hover Events
N/A — JavaScript-only linked subplot hover
The JS example synchronises hover across subplots using
Plotly.Fx.hovercalled inside aplotly_hovercallback — requires direct access to multiple graph divs.In Livebook,
layout.hovermode: "x unified"achieves a similar visual effect natively (all traces at the same x show in one tooltip) without custom JS.
Chart Events > Combined Click and Hover Events
A single subscriber can differentiate events by checking event["type"].
This example listens for both plotly_click and plotly_hover on the same chart.
alias Plotly.{Figure, Scatter}
alias Plotly.Kino.PlotlyLive
output = Kino.Frame.new()
chart =
Figure.new()
|> Figure.add_trace(Scatter.new(
x: [1, 2, 3, 4, 5],
y: [2, 4, 1, 5, 3],
mode: "markers",
marker: %{size: 14, color: "purple"}
))
|> Figure.update_layout(title: "Hover or click the points")
|> PlotlyLive.new()
listener =
spawn(fn ->
for _ <- 1..1000 do
receive do
{:chart_event, %{"type" => type} = event} ->
[pt | _] = event["data"]["points"] || []
label = if type == "plotly_click", do: "click", else: "hover"
Kino.Frame.render(
output,
Kino.Markdown.new("**#{label}** — x: #{pt["x"]}, y: #{pt["y"]}")
)
after 60_000 -> :timeout
end
end
end)
PlotlyLive.subscribe(chart, listener, :chart_event)
Kino.Layout.grid([chart, output])
Chart Events > Binding to Zoom Events
plotly_relayout fires on zoom, pan, and axis range changes.
The event data is a flat map of the changed layout keys,
e.g. %{"xaxis.range[0]" => 1.5, "xaxis.range[1]" => 3.2}.
Use layout.yaxis.fixedrange: true to constrain zooming to one axis.
alias Plotly.{Figure, Scatter}
alias Plotly.Kino.PlotlyLive
output = Kino.Frame.new()
chart =
Figure.new()
|> Figure.add_trace(Scatter.new(
x: Enum.to_list(1..20),
y: Enum.map(1..20, fn i -> :math.sin(i / 3.0) end),
mode: "lines+markers"
))
|> Figure.update_layout(
title: "Zoom or pan — relayout events appear below",
xaxis: %{title: "x"},
yaxis: %{title: "y", fixedrange: true}
)
|> PlotlyLive.new()
listener =
spawn(fn ->
for _ <- 1..1000 do
receive do
{:relayout, %{"data" => data}} ->
changes =
data
|> Map.drop(["_index"])
|> Enum.map(fn {k, v} -> "#{k} = #{inspect(v)}" end)
|> Enum.join(", ")
Kino.Frame.render(output, Kino.Markdown.new("**relayout:** #{changes}"))
after 60_000 -> :timeout
end
end
end)
PlotlyLive.subscribe(chart, listener, :relayout, :relayout)
Kino.Layout.grid([chart, output])
Chart Events > Disabling Zoom Events for X Axis
xaxis.fixedrange: true prevents zooming/panning on the X axis.
The Y axis remains zoomable. No event subscription needed — this is a layout config.
alias Plotly.{Figure, Scatter}
Figure.new()
|> Figure.add_trace(Scatter.new(
x: Enum.to_list(1..20),
y: Enum.map(1..20, fn i -> :math.sin(i / 3.0) end),
mode: "lines+markers"
))
|> Figure.update_layout(
title: "X zoom disabled — try to zoom on each axis",
xaxis: %{fixedrange: true, title: "X — fixed range"},
yaxis: %{title: "Y — zoomable"}
)
|> Plotly.show()
Chart Events > Disabling Zoom Events for X and Y Axis
Set fixedrange: true on both axes to disable all zooming and panning.
The chart becomes a static display — useful for dashboards where chart
proportions must stay fixed.
alias Plotly.{Figure, Scatter}
Figure.new()
|> Figure.add_trace(Scatter.new(
x: Enum.to_list(1..20),
y: Enum.map(1..20, fn i -> :math.cos(i / 3.0) end),
mode: "lines+markers"
))
|> Figure.update_layout(
title: "All zoom disabled — drag or scroll has no effect",
xaxis: %{fixedrange: true},
yaxis: %{fixedrange: true}
)
|> Plotly.show()