Phoenix LiveView Integration
GnuplotEx provides Phoenix LiveView support for real-time, interactive data visualization in web applications.
Installation
Add both dependencies to your mix.exs:
def deps do
[
{:gnuplot_ex, "~> 0.5"},
{:phoenix_live_view, "~> 1.0"}
]
endThen run:
mix deps.get
Quick Start
1. Basic Usage
The simplest way to render a plot in LiveView is using the live_gnuplot/1 component:
defmodule MyAppWeb.ChartLive do
use Phoenix.LiveView
import GnuplotEx.LiveView.Component
def render(assigns) do
~H"""
<div class="container">
<h1>My Chart</h1>
<.live_gnuplot plot={@plot} />
</div>
"""
end
def mount(_params, _session, socket) do
data = [[1, 2], [2, 4], [3, 6], [4, 8], [5, 10]]
plot = GnuplotEx.new()
|> GnuplotEx.scatter(data, label: "Points")
|> GnuplotEx.title("My Plot")
|> GnuplotEx.x_label("X Axis")
|> GnuplotEx.y_label("Y Axis")
{:ok, assign(socket, plot: plot)}
end
end2. Real-time Updates
Update the plot when data changes:
defmodule MyAppWeb.RealtimeChartLive do
use Phoenix.LiveView
import GnuplotEx.LiveView.Component
def render(assigns) do
~H"""
<.live_gnuplot plot={@plot} width={1200} height={400} />
"""
end
def mount(_params, _session, socket) do
# Subscribe to data updates
if connected?(socket) do
:timer.send_interval(1000, self(), :tick)
end
socket = assign(socket,
data: [],
max_points: 50
)
{:ok, socket}
end
def handle_info(:tick, socket) do
# Add new data point
new_point = [
System.system_time(:second),
:rand.uniform() * 100
]
# Keep only last N points (rolling window)
data = ([new_point] ++ socket.assigns.data)
|> Enum.take(socket.assigns.max_points)
# Create updated plot
plot = GnuplotEx.line(data,
label: "Sensor Data",
color: "#E95420"
)
|> GnuplotEx.x_label("Time")
|> GnuplotEx.y_label("Value")
|> GnuplotEx.theme(:dark)
{:noreply, assign(socket, data: data, plot: plot)}
end
end3. Interactive 3D Plots
For 3D plots with mouse/touch controls, use the JavaScript hook:
Step 1: Add the hook to your app.js:
// assets/js/app.js
import { GnuplotInteractive } from "../../deps/gnuplot_ex/priv/static/gnuplot_ex_hooks"
let liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
hooks: { GnuplotInteractive }
})Step 2: Use the hook in your LiveView:
def render(assigns) do
~H"""
<.live_gnuplot
plot={@plot}
id="interactive-3d"
phx-hook="GnuplotInteractive"
width={800}
height={600}
/>
"""
end
def mount(_params, _session, socket) do
plot = GnuplotEx.surface(
fn x, y -> :math.sin(x) * :math.cos(y) end,
x_range: -5..5,
y_range: -5..5,
title: "Interactive Surface"
)
|> GnuplotEx.palette(:viridis)
{:ok, assign(socket, plot: plot)}
endControls:
- Left-click drag: Rotate
- Scroll wheel: Zoom
- Right-click drag: Pan
- Touch: Drag to rotate, pinch to zoom
Component Options
The live_gnuplot/1 component accepts the following attributes:
| Attribute | Type | Default | Description |
|---|---|---|---|
plot | GnuplotEx.Plot | required | The plot to render |
format | :svg | :png | :svg | Output format |
width | integer | 800 | Plot width in pixels |
height | integer | 600 | Plot height in pixels |
cache | boolean | true | Enable plot caching |
cache_ttl | integer | 60_000 | Cache TTL in milliseconds |
class | string | "" | CSS class for container |
on_error | function | nil | Custom error handler |
rest | global | - | Additional HTML attributes |
Examples
# Custom size
<.live_gnuplot plot={@plot} width={1200} height={600} />
# PNG format
<.live_gnuplot plot={@plot} format={:png} />
# Disable caching
<.live_gnuplot plot={@plot} cache={false} />
# Custom cache TTL (5 minutes)
<.live_gnuplot plot={@plot} cache_ttl={300_000} />
# Custom CSS class
<.live_gnuplot plot={@plot} class="my-chart shadow-lg" />
# With fallback content
<.live_gnuplot plot={@plot}>
<:fallback>
<div class="loading">Rendering plot...</div>
</:fallback>
</.live_gnuplot>
# Custom error handler
<.live_gnuplot
plot={@plot}
on_error={fn reason ->
Phoenix.HTML.Tag.content_tag(:div, "Error: #{reason}", class: "error")
end}
/>Advanced Patterns
Multiple Plots in a Dashboard
defmodule MyAppWeb.DashboardLive do
use Phoenix.LiveView
import GnuplotEx.LiveView.Component
def render(assigns) do
~H"""
<div class="grid grid-cols-2 gap-4">
<.live_gnuplot plot={@plot1} />
<.live_gnuplot plot={@plot2} />
<.live_gnuplot plot={@plot3} />
<.live_gnuplot plot={@plot4} />
</div>
"""
end
def mount(_params, _session, socket) do
socket = assign(socket,
plot1: create_plot1(),
plot2: create_plot2(),
plot3: create_plot3(),
plot4: create_plot4()
)
{:ok, socket}
end
endDebouncing Rapid Updates
For high-frequency data, debounce updates to avoid overwhelming the renderer:
def handle_info({:data_point, point}, socket) do
# Cancel previous debounce timer if exists
if socket.assigns[:debounce_timer] do
Process.cancel_timer(socket.assigns.debounce_timer)
end
# Add point to buffer
data = [point | socket.assigns.data] |> Enum.take(100)
# Schedule update in 100ms
timer = Process.send_after(self(), :update_plot, 100)
{:noreply, assign(socket, data: data, debounce_timer: timer)}
end
def handle_info(:update_plot, socket) do
plot = GnuplotEx.line(socket.assigns.data)
{:noreply, assign(socket, plot: plot, debounce_timer: nil)}
endUser-Controlled Plot Options
Let users customize the plot:
def render(assigns) do
~H"""
<div>
<form phx-change="update_options">
<select name="theme">
<option value="default">Default</option>
<option value="dark">Dark</option>
<option value="publication">Publication</option>
</select>
<select name="palette">
<option value="viridis">Viridis</option>
<option value="plasma">Plasma</option>
<option value="magma">Magma</option>
</select>
</form>
<.live_gnuplot plot={@plot} />
</div>
"""
end
def handle_event("update_options", %{"theme" => theme, "palette" => palette}, socket) do
plot = socket.assigns.base_plot
|> GnuplotEx.theme(String.to_atom(theme))
|> GnuplotEx.palette(String.to_atom(palette))
{:noreply, assign(socket, plot: plot)}
endStreaming Data with Channels
Receive real-time data via Phoenix Channels:
def mount(_params, _session, socket) do
if connected?(socket) do
MyAppWeb.Endpoint.subscribe("sensor:#{socket.assigns.sensor_id}")
end
{:ok, assign(socket, data: [], plot: initial_plot())}
end
def handle_info(%{event: "measurement", payload: measurement}, socket) do
data = [measurement | socket.assigns.data] |> Enum.take(100)
plot = GnuplotEx.line(data, label: "Sensor #{socket.assigns.sensor_id}")
{:noreply, assign(socket, data: data, plot: plot)}
endPerformance Tips
1. Enable Caching
The component caches rendered plots by default to avoid redundant gnuplot executions:
# Cache enabled (default) - renders once, reuses SVG
<.live_gnuplot plot={@plot} cache={true} cache_ttl={60_000} />2. Limit Data Points
For real-time plots, use rolling windows to limit data size:
# Keep only last 100 points
data = ([new_point] ++ data) |> Enum.take(100)3. Debounce High-Frequency Updates
Batch rapid updates to reduce render frequency:
# Update plot every 100ms instead of every data point
Process.send_after(self(), :update_plot, 100)4. Use Appropriate Formats
- SVG: Best for web display, scales well, larger file size
- PNG: Smaller file size, fixed resolution, faster transfer
# Use PNG for large complex plots
<.live_gnuplot plot={@plot} format={:png} />5. Monitor Cache Statistics
# In IEx
GnuplotEx.LiveView.Cache.stats()
# => %{entries: 42, memory_bytes: 1_048_576}
# Clear cache if needed
GnuplotEx.LiveView.Cache.clear()Testing
Component Testing
defmodule MyAppWeb.ChartLiveTest do
use MyAppWeb.ConnCase, async: true
import Phoenix.LiveViewTest
test "renders plot", %{conn: conn} do
{:ok, view, html} = live(conn, "/charts")
assert html =~ "gnuplot-container"
assert html =~ "<svg"
end
test "updates plot on data change", %{conn: conn} do
{:ok, view, _html} = live(conn, "/charts")
# Send update event
send(view.pid, {:new_data, [[1, 2], [3, 4]]})
html = render(view)
assert html =~ "<svg"
end
endUnit Testing Plots
test "creates valid plot struct" do
plot = GnuplotEx.new()
|> GnuplotEx.scatter([[1, 2], [3, 4]])
|> GnuplotEx.title("Test")
assert %GnuplotEx.Plot{} = plot
assert plot.title == "Test"
assert length(plot.series) == 1
endTroubleshooting
Plot Not Rendering
Check gnuplot installation:
gnuplot --version # Should be 6.0+Check logs for errors:
# In LiveView require Logger Logger.debug("Rendering plot: #{inspect(@plot)}")Disable cache to debug:
<.live_gnuplot plot={@plot} cache={false} />
Interactive Controls Not Working
Verify hook is loaded:
// app.js should have: import { GnuplotInteractive } from "..." hooks: { GnuplotInteractive }Check element has
phx-hookattribute:<.live_gnuplot plot={@plot} id="my-plot" phx-hook="GnuplotInteractive" />Ensure plot has an
id: Theidattribute is required for hooks to attach properly.
High Memory Usage
Clear cache periodically:
# Schedule cache cleanup Process.send_after(self(), :clear_cache, 300_000) def handle_info(:clear_cache, socket) do GnuplotEx.LiveView.Cache.clear() {:noreply, socket} endReduce cache TTL:
<.live_gnuplot plot={@plot} cache_ttl={10_000} /> # 10 secondsUse PNG for large plots:
<.live_gnuplot plot={@plot} format={:png} />
Examples
Real-time Sensor Dashboard
Complete example of a real-time monitoring dashboard:
defmodule MyAppWeb.SensorDashboardLive do
use Phoenix.LiveView
import GnuplotEx.LiveView.Component
def mount(_params, _session, socket) do
if connected?(socket) do
:timer.send_interval(1000, self(), :tick)
end
socket = assign(socket,
temperature: [],
humidity: [],
pressure: [],
max_points: 100
)
{:ok, socket}
end
def render(assigns) do
~H"""
<div class="sensor-dashboard">
<h1>Sensor Dashboard</h1>
<div class="grid grid-cols-3 gap-4">
<div>
<h2>Temperature</h2>
<.live_gnuplot plot={temperature_plot(@temperature)} />
</div>
<div>
<h2>Humidity</h2>
<.live_gnuplot plot={humidity_plot(@humidity)} />
</div>
<div>
<h2>Pressure</h2>
<.live_gnuplot plot={pressure_plot(@pressure)} />
</div>
</div>
</div>
"""
end
def handle_info(:tick, socket) do
# Simulate sensor readings
temp = [System.system_time(:second), 20 + :rand.uniform() * 10]
humid = [System.system_time(:second), 40 + :rand.uniform() * 20]
press = [System.system_time(:second), 1000 + :rand.uniform() * 50]
socket = socket
|> update(:temperature, &append_point(&1, temp, socket.assigns.max_points))
|> update(:humidity, &append_point(&1, humid, socket.assigns.max_points))
|> update(:pressure, &append_point(&1, press, socket.assigns.max_points))
{:noreply, socket}
end
defp append_point(data, point, max) do
([point] ++ data) |> Enum.take(max)
end
defp temperature_plot(data) do
GnuplotEx.line(data,
label: "°C",
color: "#E95420"
)
|> GnuplotEx.y_label("Temperature (°C)")
|> GnuplotEx.y_range(0..40)
end
defp humidity_plot(data) do
GnuplotEx.line(data,
label: "%",
color: "#0066CC"
)
|> GnuplotEx.y_label("Humidity (%)")
|> GnuplotEx.y_range(0..100)
end
defp pressure_plot(data) do
GnuplotEx.line(data,
label: "hPa",
color: "#00AA00"
)
|> GnuplotEx.y_label("Pressure (hPa)")
|> GnuplotEx.y_range(950..1050)
end
end