Charts
Apache ECharts behind a declarative HEEx component. The chart is a plain Elixir map - the ECharts option object - so the whole spec lives server-side and travels the LiveView wire as data. Update the assign and the chart animates in place. Colours derive from your design tokens at mount, so one chart definition looks right in light, dark and any theme.
Area line
A smooth line with the soft gradient fade. The spec is a plain Elixir map; colours derive from your theme tokens at mount.
<.chart
id="showcase-chart-line"
height="16rem"
option={
%{
grid: %{left: 8, right: 16, top: 16, bottom: 8, containLabel: true},
xAxis: %{type: "category", boundaryGap: false, data: ~w(Mon Tue Wed Thu Fri Sat Sun)},
yAxis: %{type: "value"},
tooltip: %{trigger: "axis"},
series: [
%{
type: "line",
name: "Visitors",
smooth: true,
areaStyle: %{color: "petal:fade"},
data: [820, 932, 901, 1290, 1330, 1520, 1710]
}
]
}
}
/>
Grouped bars
Two series side by side. Series colours come from the --pc-chart-* palette, falling back to your semantic ramps.
<.chart
id="showcase-chart-bar"
height="16rem"
option={
%{
grid: %{left: 8, right: 8, top: 32, bottom: 8, containLabel: true},
legend: %{top: 0},
tooltip: %{trigger: "axis"},
xAxis: %{type: "category", data: ~w(Q1 Q2 Q3 Q4)},
yAxis: %{type: "value"},
series: [
%{name: "Pro", type: "bar", barGap: 0, data: [320, 402, 391, 534]},
%{name: "Teams", type: "bar", data: [120, 182, 231, 290]}
]
}
}
/>
Donut with a centre total
A pie with an inner radius and a title placed in the hole.
<.chart
id="showcase-chart-donut"
height="16rem"
option={
%{
tooltip: %{trigger: "item"},
legend: %{bottom: 0},
title: %{
text: "960",
subtext: "customers",
left: "center",
top: "33%",
itemGap: 2,
textStyle: %{fontSize: 24, fontWeight: 650},
subtextStyle: %{fontSize: 12}
},
series: [
%{
name: "Plan",
type: "pie",
radius: ["48%", "72%"],
center: ["50%", "44%"],
itemStyle: %{borderRadius: 5, borderWidth: 2},
label: %{show: false},
data: [
%{value: 580, name: "Individual"},
%{value: 260, name: "Team"},
%{value: 120, name: "Legacy"}
]
}
]
}
}
/>
Money axes and tooltips
Named formatters stand in for the JavaScript callbacks ECharts normally wants: petal:currency-compact for the axis, petal:currency for the tooltip.
<.chart
id="showcase-chart-currency"
height="16rem"
option={
%{
grid: %{left: 8, right: 16, top: 16, bottom: 8, containLabel: true},
xAxis: %{type: "category", boundaryGap: false, data: ~w(Jan Feb Mar Apr May Jun)},
yAxis: %{type: "value", axisLabel: %{formatter: "petal:currency-compact:USD"}},
tooltip: %{trigger: "axis", valueFormatter: "petal:currency:USD"},
series: [
%{
type: "line",
name: "MRR",
smooth: true,
areaStyle: %{color: "petal:fade"},
data: [9200, 9840, 10100, 11200, 11900, 12480]
}
]
}
}
/>
Named formatters
ECharts formats numbers with JavaScript callbacks, and a function can't travel the wire
from your server. So anywhere ECharts accepts a formatter
or valueFormatter, pass a string and the hook substitutes the real
Intl.NumberFormat
function on the client: "petal:number"
(1234 becomes "1.2K"), "petal:percent", "petal:currency:USD"
(12480 becomes "$12,480"), and "petal:currency-compact:USD"
for axes ("$12K"). In the same spirit,
areaStyle: %{color: "petal:fade"}
renders the soft gradient fade in the series' own colour.
Bring your own engine
We don't bundle ECharts, the same way we don't bundle Alpine. Add it with
npm i echarts
and expose it in your app.js
(import * as echarts from "echarts"; window.echarts = echarts), or drop in a
script tag. The bundled PetalChart
hook finds it on window.echarts
and warns if it's missing. There's a loading
attr for async data, a group
attr that syncs tooltips and zoom across charts, and ECharts' aria description generation
is on by default.
Chart properties
| Attribute | Type | Default | Description |
|---|---|---|---|
class
|
any |
nil
|
|
group
|
string |
nil
|
charts sharing a group name get connected tooltips/zoom (echarts.connect) |
height
|
string |
"20rem"
|
any CSS height; the chart fills its container's width |
id*
|
string |
||
loading
|
boolean |
false
|
shows a theme-colored loading spinner over the chart while your data is on its way |
option*
|
map |
the ECharts option object as an Elixir map (atom or string keys) | |
renderer
|
string |
"canvas"
|
svg renders crisp at any zoom and is print-friendly; canvas suits large data
one of: "canvas", "svg"
|
rest
|
global |
Sparkline
Not every trend needs a full chart. <.sparkline>
renders inline trend lines as pure server-side SVG - zero JavaScript, so it works in stat
cards, table cells, even email. It inherits currentColor, so a text class sets
the colour, and the stroke keeps its weight at any size.
Trends in stat cards
Pure server-rendered SVG - zero JavaScript. The line inherits currentColor, so a text class sets the colour.
<div class="grid w-full gap-4 sm:grid-cols-3">
<div class="flex items-center justify-between px-5 py-4 border border-gray-200 rounded-xl dark:border-gray-800">
<div>
<div class="text-xs text-gray-400 dark:text-gray-500">MRR</div>
<div class="mt-1 text-xl font-semibold">$12,480</div>
</div>
<.sparkline data={[8, 9, 9, 11, 10, 12, 13, 14]} class="h-10 w-24 text-primary-500" />
</div>
<div class="flex items-center justify-between px-5 py-4 border border-gray-200 rounded-xl dark:border-gray-800">
<div>
<div class="text-xs text-gray-400 dark:text-gray-500">Signups</div>
<div class="mt-1 text-xl font-semibold">1,204</div>
</div>
<.sparkline data={[30, 34, 31, 38, 36, 41, 40, 44]} class="h-10 w-24 text-success-500" />
</div>
<div class="flex items-center justify-between px-5 py-4 border border-gray-200 rounded-xl dark:border-gray-800">
<div>
<div class="text-xs text-gray-400 dark:text-gray-500">Churn</div>
<div class="mt-1 text-xl font-semibold">1.9%</div>
</div>
<.sparkline data={[9, 8, 9, 7, 8, 6, 7, 5]} class="h-10 w-24 text-danger-500" />
</div>
</div>
Line styles
Smooth or linear, with or without the area fill.
<div class="flex flex-wrap items-center justify-center gap-8">
<.sparkline data={[4, 7, 5, 9, 8, 12, 11, 14]} class="h-10 w-32 text-primary-500" />
<.sparkline
data={[4, 7, 5, 9, 8, 12, 11, 14]}
smooth={false}
class="h-10 w-32 text-info-500"
/>
<.sparkline
data={[4, 7, 5, 9, 8, 12, 11, 14]}
fill={false}
class="h-10 w-32 text-secondary-500"
/>
</div>
Sparkline properties
| Attribute | Type | Default | Description |
|---|---|---|---|
class
|
any |
nil
|
size + color, e.g. "w-24 h-8 text-success-500" |
data*
|
list |
a list of numbers (2 or more points) | |
fill
|
boolean |
true
|
shade the area under the line at low opacity |
rest
|
global |
||
smooth
|
boolean |
true
|
curve through the points instead of straight segments |
stroke_width
|
any |
2
|