Server-Side SVG Charts in Elixir: Contex vs Client-Side JavaScript Charting
by Ridhwaan Mayet, Data Engineering Consultant
The default assumption
Ask most web developers how to put a chart on a page and the answer is automatic: pick a JavaScript charting library. Chart.js, ECharts, Highcharts, D3 if you’re feeling brave. The chart is a client-side problem. Ship the data down, render in the browser.
That default is so ingrained that people rarely notice what it costs. You pay a bundle tax: charting libraries are among the heaviest dependencies a dashboard carries. You pay a hydration tax: the page arrives, then the chart pops in after the JS parses and runs. And on any app with live data, you pay a state-sync tax: the server knows the truth, the browser holds a copy, and you now maintain the plumbing that keeps the two aligned. Serialize the data, ship it, deserialize it, feed it to the chart, handle the update path, handle the reconnect path.
None of this is wrong. It’s just not free, and for a large class of applications (operational dashboards, monitoring views, reports) it’s paying for interactivity you never use.
LiveView changes the frame
Phoenix LiveView inverts the usual model. State lives on the server. The browser holds a thin runtime that patches the DOM from diffs pushed over a websocket. When an assign changes, LiveView re-renders the affected template on the server and sends only what changed.
Once you accept that model, a chart stops being special. A chart is markup. SVG is markup. If the server can render your table rows and your status badges, it can render your bar chart, and when the underlying data changes, the chart updates over the same websocket diff as everything else on the page.
That’s the real argument for server-side charts in Elixir, and it’s an architectural one, not a performance one: one source of truth. There is no client-side copy of the chart data to keep in sync, no update API to call, no lifecycle to manage. New reading arrives, assign changes, chart re-renders. Done.
I’ve shipped this model in production. At Decarb.Earth I delivered NeoEMS, an Elixir / Phoenix LiveView energy-management platform with real-time monitoring and optimisation views, the same company whose broader solar-data platform I cover in the Decarb.Earth case study. Energy dashboards are close to the ideal case for this pattern: readings stream in continuously, every connected operator should see the same numbers, and the interactivity users actually need is modest.
What Contex is
Contex is an Elixir library for composable, server-side charting. You hand it a dataset, describe the plot, and it emits SVG. As of writing it covers the workhorse chart types: bar charts (including grouped and stacked), line plots, point/scatter plots, sparklines and Gantt charts, though you should check the current docs for exact coverage, since the library evolves.
The shape of the API is simple: build a Dataset, wrap it in a Plot with a chart module and options, render to SVG.
defp power_chart(readings) do
readings
|> Contex.Dataset.new(["hour", "kw"])
|> Contex.Plot.new(Contex.BarChart, 600, 300,
mapping: %{category_col: "hour", value_cols: ["kw"]}
)
|> Contex.Plot.to_svg()
end
to_svg/1 returns safe iodata, so it drops straight into a HEEx template:
def render(assigns) do
~H"""
<div class="card">
<h2>Site output: last 24h</h2>
{power_chart(@readings)}
</div>
"""
end
And the live-update path is just LiveView doing what LiveView does:
def handle_info({:reading, reading}, socket) do
{:noreply, update(socket, :readings, &append_window(&1, reading))}
end
No JavaScript written, no chart library shipped, no update handler on the client. The diff engine sends the changed SVG nodes down the wire and the chart moves.
Top tip
Treat chart rendering as a pure function of assigns and keep it in a private component function. When charts are just functions returning markup, they get the same testing, refactoring and composition story as the rest of your Elixir code.
The honest limitations
I’ll state these plainly, because the pattern only earns trust if you know where it stops.
The ecosystem is small. Contex is a focused library maintained by a small community. The JavaScript charting world has had fifteen-plus years and thousands of contributors. If you need a violin plot, a sankey diagram or a candlestick chart, Contex doesn’t have it and you’ll be writing SVG yourself or reaching for JS.
Fewer chart types, fewer knobs. Styling and axis options exist but are nowhere near as deep as ECharts or Highcharts. Expect to accept the defaults or drop to custom SVG.
Interactivity needs JS anyway. Tooltips that follow the cursor, zoom, brushing, pan: these are inherently client-side behaviours. Server round-trips are fine for a click-to-drill-down, but not for 60fps hover feedback. The moment you need rich interaction, you’re writing JS hooks regardless, which weakens the “no JavaScript” pitch.
A websocket per viewer. Server-rendered charts assume the LiveView model, with its stateful connections. That’s a Phoenix strength, but it’s a different operational profile from a static page hitting a JSON API.
The hybrid pattern
In practice, real dashboards land on a hybrid. Most charts are server-rendered SVG. The one or two views that genuinely need heavy interactivity get a LiveView JS hook that hands off to a client-side library, with the server still owning the data and pushing updates as events.
<div id="load-profile" phx-hook="EChart" phx-update="ignore"></div>
Hooks.EChart = {
mounted() {
this.chart = echarts.init(this.el)
this.handleEvent("chart:data", ({ option }) =>
this.chart.setOption(option)
)
},
}
The important part is that the server remains the source of truth: the LiveView pushes chart:data events when assigns change, and the hook is a dumb renderer. You’ve paid the bundle cost for one library on one view, not made client-side state management the architecture of the whole dashboard.
When each approach wins
| Situation | Better fit |
|---|---|
| Operational dashboards, modest interactivity, real-time updates | Server-side SVG (Contex) |
| Reports, emails, print, anything crawlable | Server-side SVG |
| Exploratory analytics with zoom, brush, cross-filtering | Client-side JS |
| Very large point counts (tens of thousands and up) | Client-side canvas / WebGL |
| Exotic chart types beyond bar/line/scatter | Client-side JS |
| Team has no appetite for a JS build pipeline | Server-side SVG |
The dividing line is roughly this: if the user’s job is to watch the data, render on the server. If the user’s job is to interrogate the data (zooming, brushing, slicing at interactive speed) render on the client. And SVG itself has a ceiling: it’s a DOM node per mark, so once you’re plotting tens of thousands of points, canvas or WebGL is the right tool no matter which side renders it.
Top tip
Audit an existing dashboard before assuming you need a JS charting library. Count how many charts users actually hover, zoom or filter. In my experience most operational views are watch-only, and the interactive minority can be handled by one hook.
Practical notes
A few properties of server-rendered SVG that don’t show up in framework debates but matter in production:
- It’s real markup. The chart exists in the HTML response. It’s crawlable, it prints correctly, and the same rendering code can generate charts for emails and PDF reports: something a canvas-based client library can’t give you without a headless browser.
- No canvas accessibility hole. A canvas chart is a bitmap to assistive technology unless you do extra work. SVG elements can carry titles, descriptions and ARIA attributes, and text in the chart is actual text.
- Server CPU cost is trivial at dashboard scale. Rendering a few hundred SVG elements is string-building, and the BEAM does it in microseconds-to-milliseconds territory for typical dashboard sizes. I won’t quote benchmarks (measure your own) but in practice the query producing the data dominates the render every time.
- Diffs stay small. LiveView sends changed parts of the template, so a moving chart doesn’t mean re-sending the page. If you’re pushing high-frequency updates, batch them server-side; nobody needs a chart repainting more than about once a second.
Where this leaves you
If you’re building on Phoenix LiveView, server-side SVG charting should be your default, with client-side JS as the deliberate exception for views that earn it. You keep one source of truth, one language, one rendering model, and the pragmatic escape hatch of JS hooks when the interactivity budget genuinely demands it.
If you’re weighing this kind of decision for a real-time dashboard or data product, I’ve made these trade-offs in production and can help you make them for your stack. Start with the scoping tool, or get in touch directly.