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.hover called
> inside a plotly_hover callback — 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()