Components Charts

Petal Pro is the full SaaS app this is built for

Auth, billing, admin, and Claude Code integration included. One purchase, unlimited projects.

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.

heex
<.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.

heex
<.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.

heex
<.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.

heex
<.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.

MRR
$12,480
Signups
1,204
Churn
1.9%
heex
<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.

heex
<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