ChartsEx
SVG chart rendering for Elixir powered by charts-rs -- 10 chart types, 9 built-in themes, an idiomatic builder API, and a Phoenix component for LiveView.
Installation
Add charts_ex to your dependencies in mix.exs:
def deps do
[
{:charts_ex, "~> 0.1.0"}
]
endQuick Start
alias ChartsEx.BarChart
{:ok, svg} =
BarChart.new()
|> BarChart.title("Monthly Revenue")
|> BarChart.theme(:grafana)
|> BarChart.width(600)
|> BarChart.height(400)
|> BarChart.x_axis(["Jan", "Feb", "Mar", "Apr", "May", "Jun"])
|> BarChart.add_series("Revenue ($k)", [48.2, 53.1, 61.7, 59.3, 72.0, 84.5])
|> ChartsEx.render()
# svg is a self-contained SVG string -- write it to a file, embed it in HTML, etc.
File.write!("revenue.svg", svg)Examples
Bar Chart
Basic vertical bar chart with two series and a theme:
alias ChartsEx.BarChart
{:ok, svg} =
BarChart.new()
|> BarChart.title("Quarterly Sales by Region")
|> BarChart.sub_title("FY 2025 — All figures in $k")
|> BarChart.theme(:ant)
|> BarChart.width(700)
|> BarChart.height(420)
|> BarChart.x_axis(["Q1", "Q2", "Q3", "Q4"])
|> BarChart.add_series("North America", [312.0, 348.0, 295.0, 410.0])
|> BarChart.add_series("Europe", [228.0, 265.0, 241.0, 302.0])
|> BarChart.series_colors(["#5470C6", "#91CC75"])
|> BarChart.legend_align(:right)
|> BarChart.radius(4.0)
|> ChartsEx.render()Bar chart with a line overlay (dual series types) and mark lines:
alias ChartsEx.BarChart
{:ok, svg} =
BarChart.new()
|> BarChart.title("Website Traffic vs. Conversion Rate")
|> BarChart.theme(:walden)
|> BarChart.width(720)
|> BarChart.height(400)
|> BarChart.x_axis(["Jan", "Feb", "Mar", "Apr", "May", "Jun"])
|> BarChart.y_axis_configs([
%{axis_font_size: 12},
%{axis_font_size: 12}
])
|> BarChart.add_series("Visitors", [14200.0, 16800.0, 15400.0, 18900.0, 21300.0, 24100.0],
mark_lines: [%{category: "average"}]
)
|> BarChart.add_series("Conversion %", [2.1, 2.4, 1.9, 3.0, 3.5, 3.8],
category: "line",
y_axis_index: 1,
stroke_dash_array: "4,2"
)
|> ChartsEx.render()Line Chart
Smooth curves with area fill:
alias ChartsEx.LineChart
{:ok, svg} =
LineChart.new()
|> LineChart.title("Server Response Times")
|> LineChart.sub_title("p50 and p99 latency in ms")
|> LineChart.theme(:grafana)
|> LineChart.width(700)
|> LineChart.height(400)
|> LineChart.smooth(true)
|> LineChart.fill(true)
|> LineChart.x_axis(["00:00", "04:00", "08:00", "12:00", "16:00", "20:00", "23:59"])
|> LineChart.add_series("p50", [12.0, 8.0, 35.0, 62.0, 48.0, 24.0, 14.0])
|> LineChart.add_series("p99", [45.0, 28.0, 120.0, 210.0, 165.0, 78.0, 52.0],
mark_lines: [%{category: "average"}],
mark_points: [%{category: "max"}]
)
|> LineChart.series_colors(["#73C0DE", "#EE6666"])
|> ChartsEx.render()Pie Chart
Donut chart with inner radius:
alias ChartsEx.PieChart
{:ok, svg} =
PieChart.new()
|> PieChart.title("Browser Market Share")
|> PieChart.sub_title("Global — March 2025")
|> PieChart.theme(:westeros)
|> PieChart.width(500)
|> PieChart.height(400)
|> PieChart.inner_radius(80.0)
|> PieChart.radius(150.0)
|> PieChart.border_radius(4.0)
|> PieChart.add_series("Chrome", [64.7])
|> PieChart.add_series("Safari", [18.6])
|> PieChart.add_series("Firefox", [3.2])
|> PieChart.add_series("Edge", [5.3])
|> PieChart.add_series("Other", [8.2])
|> PieChart.series_colors(["#5470C6", "#91CC75", "#FAC858", "#EE6666", "#73C0DE"])
|> ChartsEx.render()Horizontal Bar Chart
alias ChartsEx.HorizontalBarChart
{:ok, svg} =
HorizontalBarChart.new()
|> HorizontalBarChart.title("Top Programming Languages by Job Postings")
|> HorizontalBarChart.theme(:ant)
|> HorizontalBarChart.width(650)
|> HorizontalBarChart.height(400)
|> HorizontalBarChart.x_axis([
"Elixir", "Go", "Rust", "TypeScript", "Python", "Java"
])
|> HorizontalBarChart.add_series("Job Postings (thousands)", [
8.4, 22.1, 12.7, 58.3, 92.5, 74.0
], label_show: true)
|> HorizontalBarChart.series_colors(["#5470C6"])
|> ChartsEx.render()Radar Chart
Multi-dimensional comparison with indicators:
alias ChartsEx.RadarChart
{:ok, svg} =
RadarChart.new()
|> RadarChart.title("Framework Comparison")
|> RadarChart.sub_title("Phoenix vs. Rails vs. Next.js")
|> RadarChart.theme(:vintage)
|> RadarChart.width(560)
|> RadarChart.height(460)
|> RadarChart.indicators([
%{name: "Performance", max: 100.0},
%{name: "Scalability", max: 100.0},
%{name: "Ecosystem", max: 100.0},
%{name: "Learning Curve", max: 100.0},
%{name: "Realtime", max: 100.0},
%{name: "Deployment", max: 100.0}
])
|> RadarChart.add_series("Phoenix", [92.0, 95.0, 58.0, 55.0, 98.0, 72.0])
|> RadarChart.add_series("Rails", [65.0, 70.0, 90.0, 75.0, 50.0, 80.0])
|> RadarChart.add_series("Next.js", [78.0, 72.0, 95.0, 68.0, 65.0, 88.0])
|> RadarChart.series_colors(["#6C5CE7", "#E17055", "#00B894"])
|> ChartsEx.render()Scatter Chart
alias ChartsEx.ScatterChart
{:ok, svg} =
ScatterChart.new()
|> ScatterChart.title("House Price vs. Square Footage")
|> ScatterChart.sub_title("Austin, TX — 2025 listings")
|> ScatterChart.theme(:walden)
|> ScatterChart.width(650)
|> ScatterChart.height(420)
|> ScatterChart.add_series("3-Bedroom", [
[1200.0, 285.0], [1450.0, 320.0], [1680.0, 375.0],
[1890.0, 410.0], [2100.0, 465.0], [2350.0, 520.0],
[1550.0, 340.0], [1750.0, 390.0], [2000.0, 445.0]
])
|> ScatterChart.add_series("4-Bedroom", [
[1800.0, 420.0], [2050.0, 485.0], [2300.0, 540.0],
[2600.0, 610.0], [2850.0, 675.0], [3100.0, 740.0],
[2200.0, 520.0], [2500.0, 590.0], [2750.0, 650.0]
])
|> ScatterChart.symbol_sizes([6.0, 8.0])
|> ScatterChart.series_colors(["#5470C6", "#EE6666"])
|> ChartsEx.render()Candlestick Chart
Financial OHLC data:
alias ChartsEx.CandlestickChart
{:ok, svg} =
CandlestickChart.new()
|> CandlestickChart.title("AAPL Stock Price")
|> CandlestickChart.sub_title("Weekly — Jan to Mar 2025")
|> CandlestickChart.theme(:dark)
|> CandlestickChart.width(750)
|> CandlestickChart.height(420)
|> CandlestickChart.up_color("#26A69A")
|> CandlestickChart.up_border_color("#26A69A")
|> CandlestickChart.down_color("#EF5350")
|> CandlestickChart.down_border_color("#EF5350")
|> CandlestickChart.x_axis([
"Jan 6", "Jan 13", "Jan 20", "Jan 27",
"Feb 3", "Feb 10", "Feb 17", "Feb 24",
"Mar 3", "Mar 10", "Mar 17", "Mar 24"
])
|> CandlestickChart.add_series("AAPL", [
[243.0, 248.5, 240.1, 250.3],
[248.5, 252.0, 246.2, 254.7],
[252.0, 245.8, 243.5, 253.0],
[245.8, 249.2, 244.0, 251.5],
[249.2, 256.3, 248.0, 258.1],
[256.3, 261.0, 254.5, 263.8],
[261.0, 258.4, 255.2, 262.5],
[258.4, 264.7, 257.0, 266.3],
[264.7, 260.1, 258.3, 266.0],
[260.1, 268.5, 259.0, 270.2],
[268.5, 272.3, 266.8, 275.0],
[272.3, 270.8, 268.5, 274.1]
])
|> ChartsEx.render()Heatmap Chart
Color-coded grid with gradient:
alias ChartsEx.HeatmapChart
{:ok, svg} =
HeatmapChart.new()
|> HeatmapChart.title("Deploy Frequency by Day and Hour")
|> HeatmapChart.theme(:grafana)
|> HeatmapChart.width(700)
|> HeatmapChart.height(340)
|> HeatmapChart.x_axis(["Mon", "Tue", "Wed", "Thu", "Fri"])
|> HeatmapChart.y_axis(["9 AM", "11 AM", "1 PM", "3 PM", "5 PM"])
|> HeatmapChart.series(%{
data: [
# [x_index, y_index, value]
[0, 0, 3], [0, 1, 5], [0, 2, 8], [0, 3, 12], [0, 4, 4],
[1, 0, 7], [1, 1, 11], [1, 2, 15], [1, 3, 9], [1, 4, 2],
[2, 0, 10], [2, 1, 18], [2, 2, 22], [2, 3, 14], [2, 4, 6],
[3, 0, 5], [3, 1, 8], [3, 2, 19], [3, 3, 16], [3, 4, 7],
[4, 0, 2], [4, 1, 6], [4, 2, 13], [4, 3, 10], [4, 4, 1]
],
min: 0,
max: 25,
min_color: "#E0F3F8",
max_color: "#0B3D91"
})
|> ChartsEx.render()Table Chart
SVG-rendered data table with styled headers and alternating rows:
alias ChartsEx.TableChart
{:ok, svg} =
TableChart.new()
|> TableChart.title("Q1 2025 — SaaS Metrics")
|> TableChart.theme(:light)
|> TableChart.width(680)
|> TableChart.height(300)
|> TableChart.data([
["Metric", "January", "February", "March", "Trend"],
["MRR", "$142,300", "$148,900", "$156,200", "+9.8%"],
["Churn Rate", "2.1%", "1.8%", "1.6%", "-0.5pp"],
["New Customers", "87", "104", "119", "+36.8%"],
["NPS Score", "62", "65", "71", "+9pts"],
["Avg. Deal Size", "$4,280", "$4,510", "$4,750", "+11.0%"]
])
|> TableChart.spans([2.0, 1.0, 1.0, 1.0, 1.0])
|> TableChart.text_aligns([:left, :right, :right, :right, :right])
|> TableChart.header_background_color("#4A5568")
|> TableChart.header_font_color("#FFFFFF")
|> TableChart.body_background_colors(["#F7FAFC", "#EDF2F7"])
|> TableChart.border_color("#CBD5E0")
|> TableChart.cell_styles([
%{indexes: [4], font_color: "#38A169"}
])
|> ChartsEx.render()Multi Chart
Combine a bar chart and a line chart into one SVG:
alias ChartsEx.{BarChart, LineChart, MultiChart}
bar =
BarChart.new()
|> BarChart.title("Monthly Revenue ($k)")
|> BarChart.theme(:ant)
|> BarChart.width(600)
|> BarChart.height(300)
|> BarChart.x_axis(["Jan", "Feb", "Mar", "Apr", "May", "Jun"])
|> BarChart.add_series("Product A", [42.0, 48.0, 53.0, 51.0, 60.0, 68.0])
|> BarChart.add_series("Product B", [28.0, 32.0, 35.0, 41.0, 38.0, 45.0])
line =
LineChart.new()
|> LineChart.title("Cumulative Growth (%)")
|> LineChart.theme(:ant)
|> LineChart.width(600)
|> LineChart.height(300)
|> LineChart.smooth(true)
|> LineChart.x_axis(["Jan", "Feb", "Mar", "Apr", "May", "Jun"])
|> LineChart.add_series("Growth", [0.0, 14.3, 25.7, 31.4, 40.0, 61.4])
{:ok, svg} =
MultiChart.new()
|> MultiChart.add_chart(bar)
|> MultiChart.add_chart(line)
|> MultiChart.gap(20.0)
|> MultiChart.margin(%{left: 10, top: 10, right: 10, bottom: 10})
|> ChartsEx.render()Themes
ChartsEx ships with 9 built-in themes. Pass any of them to the .theme/2 function:
| Theme | Description |
|---|---|
:light | Clean white background with soft colors. Default theme. |
:dark | Dark gray background with bright, high-contrast series colors. |
:grafana | Monitoring-dashboard aesthetic -- dark background with greens and oranges. |
:ant | Ant Design-inspired palette with blues and teals on a white background. |
:vintage | Muted, warm tones (burgundy, olive, tan) with a parchment feel. |
:walden | Nature-inspired soft blues and greens, light background. |
:westeros | Subdued earth tones with a medieval cartography vibe. |
:chalk | Bold neon colors on a dark background, chalkboard style. |
:shine | Vibrant, high-saturation gradients on a dark background. |
# List all themes programmatically
ChartsEx.Theme.list()
# => [:light, :dark, :grafana, :ant, :vintage, :walden, :westeros, :chalk, :shine]
# Apply a theme
BarChart.new() |> BarChart.theme(:chalk)Customization
Every chart supports fine-grained control over colors, fonts, margins, legends, and axes. Here is a "fully loaded" example showing many options at once:
alias ChartsEx.BarChart
{:ok, svg} =
BarChart.new()
# Title
|> BarChart.title("Engineering Headcount by Department")
|> BarChart.sub_title("Updated March 2025")
# Dimensions
|> BarChart.width(750)
|> BarChart.height(450)
# Theme as a starting point, then override individual settings
|> BarChart.theme(:light)
|> BarChart.background_color("#FAFAFA")
# Custom series palette
|> BarChart.series_colors(["#6366F1", "#F59E0B", "#10B981"])
# Legend
|> BarChart.legend_align(:right)
|> BarChart.legend_margin(%{top: 5, bottom: 15, left: 0, right: 0})
# Margins
|> BarChart.margin(%{left: 15, top: 20, right: 15, bottom: 15})
# Corner radius on bars
|> BarChart.radius(3.0)
# Dual y-axis config
|> BarChart.y_axis_configs([
%{axis_font_size: 12, axis_font_color: "#333333"},
%{axis_font_size: 12, axis_font_color: "#999999"}
])
# X-axis categories
|> BarChart.x_axis(["Platform", "Backend", "Frontend", "Data", "DevOps", "Security"])
# Bar series
|> BarChart.add_series("Full-Time", [24.0, 38.0, 31.0, 18.0, 12.0, 9.0],
label_show: true,
mark_lines: [%{category: "average"}]
)
|> BarChart.add_series("Contractors", [6.0, 12.0, 8.0, 5.0, 3.0, 2.0])
# Line overlay for growth rate
|> BarChart.add_series("YoY Growth %", [15.0, 22.0, 18.0, 30.0, 25.0, 40.0],
category: "line",
y_axis_index: 1,
stroke_dash_array: "5,3",
mark_points: [%{category: "max"}]
)
|> ChartsEx.render()Summary of shared builder functions
Most chart types support these common setters:
| Function | Purpose |
|---|---|
title/2 | Chart title text |
sub_title/2 | Subtitle below the title |
theme/2 | Apply a built-in theme |
width/2, height/2 | Chart dimensions in pixels |
margin/2 |
Outer margin as %{left: _, top: _, right: _, bottom: _} |
background_color/2 | Background fill color (hex string) |
series_colors/2 | Series palette as a list of hex strings |
legend_align/2 |
Legend position: :left, :center, or :right |
legend_margin/2 | Legend margin map |
x_axis/2 | X-axis category labels |
y_axis_configs/2 | Y-axis configuration list (supports dual axes) |
add_series/3,4 | Add a named data series with optional keyword opts |
Three Input Modes
ChartsEx accepts chart definitions in three formats. All three produce identical SVG output.
1. Builder structs (recommended)
The builder API is the idiomatic way to construct charts. Each chart type has its own module with dedicated setter functions:
alias ChartsEx.LineChart
{:ok, svg} =
LineChart.new()
|> LineChart.title("Monthly Active Users")
|> LineChart.theme(:grafana)
|> LineChart.width(650)
|> LineChart.height(380)
|> LineChart.smooth(true)
|> LineChart.x_axis(["Jul", "Aug", "Sep", "Oct", "Nov", "Dec"])
|> LineChart.add_series("MAU (thousands)", [124.0, 131.0, 148.0, 162.0, 155.0, 178.0])
|> ChartsEx.render()2. Atom-key maps
Pass a plain Elixir map with atom keys. Use the :type key to specify the chart type. This is useful when chart configurations come from a database or config file:
{:ok, svg} =
ChartsEx.render(%{
type: :line,
title_text: "Monthly Active Users",
theme: "grafana",
width: 650,
height: 380,
series_smooth: true,
x_axis_data: ["Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
series_list: [
%{name: "MAU (thousands)", data: [124.0, 131.0, 148.0, 162.0, 155.0, 178.0]}
]
})
Supported :type values: :bar, :horizontal_bar, :line, :pie, :radar, :scatter, :candlestick, :heatmap, :table, :multi_chart.
3. Raw JSON
Pass a JSON string directly. This is handy if you receive chart configs from an API or store them as JSON blobs:
json = ~s({
"type": "line",
"title_text": "Monthly Active Users",
"theme": "grafana",
"width": 650,
"height": 380,
"series_smooth": true,
"x_axis_data": ["Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
"series_list": [
{"name": "MAU (thousands)", "data": [124.0, 131.0, 148.0, 162.0, 155.0, 178.0]}
]
})
{:ok, svg} = ChartsEx.render(json)Phoenix / LiveView
Component usage
ChartsEx ships with a Phoenix function component that renders charts as inline SVG inside a wrapping <div>:
<ChartsEx.Component.chart config={@chart} class="mx-auto max-w-2xl" id="sales-chart" />
The config attribute accepts any of the three input modes (struct, map, or JSON string).
Manual rendering
If you prefer to render the SVG yourself:
{raw(ChartsEx.render!(@chart))}Full LiveView example
A chart that updates dynamically when assigns change:
defmodule MyAppWeb.DashboardLive do
use MyAppWeb, :live_view
alias ChartsEx.BarChart
@impl true
def mount(_params, _session, socket) do
if connected?(socket), do: :timer.send_interval(5_000, self(), :refresh)
{:ok, assign(socket, chart: build_chart(fetch_metrics()))}
end
@impl true
def handle_info(:refresh, socket) do
{:noreply, assign(socket, chart: build_chart(fetch_metrics()))}
end
defp build_chart({labels, values}) do
BarChart.new()
|> BarChart.title("Live Request Volume")
|> BarChart.theme(:grafana)
|> BarChart.width(620)
|> BarChart.height(380)
|> BarChart.x_axis(labels)
|> BarChart.add_series("Requests/min", values)
|> BarChart.series_colors(["#73C0DE"])
end
defp fetch_metrics do
# Replace with your actual data source
labels = ["API", "Web", "WebSocket", "Webhook", "Cron"]
values = Enum.map(labels, fn _ -> :rand.uniform(500) * 1.0 end)
{labels, values}
end
@impl true
def render(assigns) do
~H"""
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">System Dashboard</h1>
<ChartsEx.Component.chart config={@chart} class="mx-auto max-w-3xl" id="req-volume" />
</div>
"""
end
endRaster Output
ChartsEx produces SVG strings. To convert to PNG, JPEG, or other raster formats, use Vix (libvips bindings for Elixir):
# Add to mix.exs: {:vix, "~> 0.30"}
alias ChartsEx.BarChart
{:ok, svg} =
BarChart.new()
|> BarChart.title("Export Example")
|> BarChart.x_axis(["A", "B", "C"])
|> BarChart.add_series("Values", [10.0, 20.0, 15.0])
|> ChartsEx.render()
# SVG -> PNG
{:ok, {image, _flags}} = Vix.Vips.Operation.svgload_buffer(svg)
:ok = Vix.Vips.Image.write_to_file(image, "chart.png")
# SVG -> JPEG (with quality setting)
:ok = Vix.Vips.Image.write_to_file(image, "chart.jpg[Q=90]")Building from Source
By default, ChartsEx uses precompiled NIF binaries (via rustler_precompiled) so you do not need Rust installed. To compile the Rust NIF locally instead:
export CHARTS_EX_BUILD=true
mix deps.compile charts_ex --forceRequirements:
- Rust 1.70+
- A C linker (usually already available on macOS and Linux)
Supported targets: aarch64-apple-darwin, aarch64-unknown-linux-gnu, x86_64-apple-darwin, x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl.
License
MIT