Charts
Chart::make($title) renders a data chart. You feed it rows with ->data(), name the category (X)
axis with ->categoryKey(), then declare one or more series — each series plots one data key as a
line, bar, area, or pie. The PHP builder serializes to a typed node the renderer draws with
Recharts.
Chart::make('Signups', 'signups-chart') ->description('New users per month') ->categoryKey('month') ->data([ ['month' => 'Jan', 'free' => 240, 'pro' => 90], ['month' => 'Feb', 'free' => 300, 'pro' => 140], ['month' => 'Mar', 'free' => 280, 'pro' => 180], ['month' => 'Apr', 'free' => 360, 'pro' => 240], ['month' => 'May', 'free' => 420, 'pro' => 320], ]) ->line('free', 'Free') ->line('pro', 'Pro') ->height(260);- categoryFormat:null
- categoryKey:"month"
- data:[{"free":240,"month":"Jan","pro":90},{"free":300,"month":"Feb","pro":140},{"free":280,"month":"Mar","pro":180},{"free":360,"month":"Apr","pro":240},{"free":420,"month":"May","pro":320}]
- description:"New users per month"
- grid:true
- height:260
- legend:true
- title:"Signups"
- tooltip:true
- valueFormat:null
- xAxis:true
- yAxis:true
Data and series
Section titled “Data and series”A chart is ->data() — a list of rows — plus one or more series. Each series names the row key it
plots, with an optional display name and color:
->line($dataKey, $name?, $color?)— a line.->bar($dataKey, $name?, $color?, $stackId?)— a bar; stackable via$stackId.->area($dataKey, $name?, $color?, $stackId?)— a filled area; stackable via$stackId.->pie($dataKey, $nameKey?, $name?, $color?)— a pie;$nameKeynames each slice.->doughnut($dataKey, $nameKey?, $name?, $color?, $innerRadius = '60%')— a pie with a hole;$innerRadiussizes it.->gauge($dataKey, $nameKey?, $name?, $color?, $maxValue?, $innerRadius = '70%')— a semicircle gauge; one ring per row, scaled against$maxValue.->distribution($dataKey, $nameKey?, $name?, $color?)— a proportional segmented bar; one segment per row.
->categoryKey($key) names the row field used for the X axis. ->height($px) sets the plot height
(default 320).
Bar charts
Section titled “Bar charts”Two ->bar() series render grouped side by side:
Chart::make('Orders by channel', 'orders-chart') ->categoryKey('week') ->data([ ['week' => 'W1', 'online' => 120, 'store' => 80], ['week' => 'W2', 'online' => 150, 'store' => 70], ['week' => 'W3', 'online' => 170, 'store' => 90], ['week' => 'W4', 'online' => 210, 'store' => 110], ]) ->bar('online', 'Online') ->bar('store', 'In-store') ->height(260);- categoryFormat:null
- categoryKey:"week"
- data:[{"online":120,"store":80,"week":"W1"},{"online":150,"store":70,"week":"W2"},{"online":170,"store":90,"week":"W3"},{"online":210,"store":110,"week":"W4"}]
- description:null
- grid:true
- height:260
- legend:true
- title:"Orders by channel"
- tooltip:true
- valueFormat:null
- xAxis:true
- yAxis:true
Stacking
Section titled “Stacking”Give bars — or areas — the same stackId to stack them into one column instead:
Chart::make('Monthly recurring revenue', 'mrr-chart') ->description('Stacked by revenue type') ->categoryKey('month') ->data([ ['month' => 'Jan', 'new' => 1200, 'expansion' => 300], ['month' => 'Feb', 'new' => 1500, 'expansion' => 450], ['month' => 'Mar', 'new' => 1800, 'expansion' => 600], ['month' => 'Apr', 'new' => 2100, 'expansion' => 780], ]) ->bar('new', 'New', stackId: 'mrr') ->bar('expansion', 'Expansion', stackId: 'mrr') ->height(260);- categoryFormat:null
- categoryKey:"month"
- data:[{"expansion":300,"month":"Jan","new":1200},{"expansion":450,"month":"Feb","new":1500},{"expansion":600,"month":"Mar","new":1800},{"expansion":780,"month":"Apr","new":2100}]
- description:"Stacked by revenue type"
- grid:true
- height:260
- legend:true
- title:"Monthly recurring revenue"
- tooltip:true
- valueFormat:null
- xAxis:true
- yAxis:true
Area and composed charts
Section titled “Area and composed charts”Series types mix freely in one chart. Declaring an ->area() and a ->line() together layers the
line over the filled band:
Chart::make('Revenue vs forecast', 'revenue-forecast-chart') ->description('Actuals as a line over the forecast band') ->categoryKey('month') ->data([ ['month' => 'Jan', 'forecast' => 26000, 'revenue' => 28000], ['month' => 'Feb', 'forecast' => 30000, 'revenue' => 32000], ['month' => 'Mar', 'forecast' => 34000, 'revenue' => 36500], ['month' => 'Apr', 'forecast' => 37000, 'revenue' => 34000], ['month' => 'May', 'forecast' => 39500, 'revenue' => 41500], ]) ->area('forecast', 'Forecast') ->line('revenue', 'Revenue') ->height(260);- categoryFormat:null
- categoryKey:"month"
- data:[{"forecast":26000,"month":"Jan","revenue":28000},{"forecast":30000,"month":"Feb","revenue":32000},{"forecast":34000,"month":"Mar","revenue":36500},{"forecast":37000,"month":"Apr","revenue":34000},{"forecast":39500,"month":"May","revenue":41500}]
- description:"Actuals as a line over the forecast band"
- grid:true
- height:260
- legend:true
- title:"Revenue vs forecast"
- tooltip:true
- valueFormat:null
- xAxis:true
- yAxis:true
Pie charts
Section titled “Pie charts”A ->pie() series plots one value per row as slices. nameKey names each slice; give a row a color
field to color its slice, or let the theme palette pick:
Chart::make('Revenue by channel', 'channel-mix-chart') ->description('Share of total revenue') ->data([ ['channel' => 'Direct', 'amount' => 42000, 'color' => '#2563eb'], ['channel' => 'Partner', 'amount' => 27000, 'color' => '#16a34a'], ['channel' => 'Marketplace', 'amount' => 19000, 'color' => '#f59e0b'], ['channel' => 'Retail', 'amount' => 12000, 'color' => '#dc2626'], ]) ->pie('amount', nameKey: 'channel') ->height(260);- categoryFormat:null
- categoryKey:null
- data:[{"amount":42000,"channel":"Direct","color":"#2563eb"},{"amount":27000,"channel":"Partner","color":"#16a34a"},{"amount":19000,"channel":"Marketplace","color":"#f59e0b"},{"amount":12000,"channel":"Retail","color":"#dc2626"}]
- description:"Share of total revenue"
- grid:true
- height:260
- legend:true
- title:"Revenue by channel"
- tooltip:true
- valueFormat:null
- xAxis:true
- yAxis:true
Doughnut
Section titled “Doughnut”->doughnut() is a pie with a hole in the middle — same data shape, same nameKey and coloring. It
takes an optional $innerRadius (default '60%') that sets the size of the hole:
Chart::make('Revenue by channel', 'channel-mix-doughnut-chart') ->description('Share of total revenue') ->data([ ['channel' => 'Direct', 'amount' => 42000, 'color' => '#2563eb'], ['channel' => 'Partner', 'amount' => 27000, 'color' => '#16a34a'], ['channel' => 'Marketplace', 'amount' => 19000, 'color' => '#f59e0b'], ['channel' => 'Retail', 'amount' => 12000, 'color' => '#dc2626'], ]) ->doughnut('amount', nameKey: 'channel') ->height(260);- categoryFormat:null
- categoryKey:null
- data:[{"amount":42000,"channel":"Direct","color":"#2563eb"},{"amount":27000,"channel":"Partner","color":"#16a34a"},{"amount":19000,"channel":"Marketplace","color":"#f59e0b"},{"amount":12000,"channel":"Retail","color":"#dc2626"}]
- description:"Share of total revenue"
- grid:true
- height:260
- legend:true
- title:"Revenue by channel"
- tooltip:true
- valueFormat:null
- xAxis:true
- yAxis:true
Gauges
Section titled “Gauges”->gauge() plots each row as a radial ring sweeping a semicircle — a value read against a maximum.
$maxValue sets the scale; omit it to scale against the largest row value. nameKey labels each
ring, and $innerRadius (default '70%') sets the ring thickness. A single-row gauge prints its
formatted value in the center of the arc; multiple rows render as concentric rings sharing one scale:
Chart::make('CPU usage', 'cpu-gauge-chart') ->description('Current utilization') ->data([ ['label' => 'CPU', 'value' => 72], ]) ->gauge('value', nameKey: 'label', maxValue: 100) ->valueFormat(NumberFormat::make()->unit(NumberFormatUnit::Percent)) ->height(260);- categoryFormat:null
- categoryKey:null
- data:[{"label":"CPU","value":72}]
- description:"Current utilization"
- grid:true
- height:260
- legend:true
- title:"CPU usage"
- tooltip:true
- valueFormat:{"currency":null,"kind":"number","maximumFractionDigits":null,"minimumFractionDigits":null,"notation":"standard","unit":"percent"}
- xAxis:true
- yAxis:true
Gauges ignore the grid and axes, so ->categoryKey() and ->categoryFormat() have no effect —
->valueFormat() drives the center label and tooltip.
Distribution bars
Section titled “Distribution bars”->distribution() plots each row as a segment of one proportional bar — the linear sibling of a
pie. Segments size themselves against the row total, the legend prints each share as a percentage,
and hovering a segment shows its exact value formatted with ->valueFormat(). Rows with zero or
negative values are skipped:
Chart::make('Revenue by channel', 'channel-distribution-chart') ->description('Share of total revenue') ->data([ ['channel' => 'Direct', 'amount' => 42000], ['channel' => 'Partner', 'amount' => 27000], ['channel' => 'Marketplace', 'amount' => 19000], ['channel' => 'Retail', 'amount' => 12000], ]) ->distribution('amount', nameKey: 'channel') ->valueFormat(NumberFormat::currency('USD')->compact());- categoryFormat:null
- categoryKey:null
- data:[{"amount":42000,"channel":"Direct"},{"amount":27000,"channel":"Partner"},{"amount":19000,"channel":"Marketplace"},{"amount":12000,"channel":"Retail"}]
- description:"Share of total revenue"
- grid:true
- height:320
- legend:true
- title:"Revenue by channel"
- tooltip:true
- valueFormat:{"currency":"USD","kind":"number","maximumFractionDigits":null,"minimumFractionDigits":null,"notation":"compact","unit":null}
- xAxis:true
- yAxis:true
Distribution bars render as plain markup and size to their content, so ->height(), the grid, and
the axes have no effect.
Colors and theming
Section titled “Colors and theming”Omit a series color and Lattice cycles a palette built on your theme tokens —
--lt-primary, --lt-success, --lt-info, --lt-warning, --lt-danger, --lt-muted-fg — so
charts follow light and dark mode automatically. Pass an explicit color to override — a colour
name ('success', Color::success()) resolves to the matching theme token, and any CSS colour
('#2563eb', Color::hex('#2563eb')) is used as-is. Give a CSS colour a ->dark() counterpart to
swap it in dark mode:
Chart::make('Signups') ->line('total', color: Color::hex('#2563eb')->dark('#60a5fa'));For pies, gauges, and distribution bars, a per-row color data field — the same named colours or
CSS colours — wins over the series color.
Formatting values and categories
Section titled “Formatting values and categories”By default the axes print raw values. Attach a format to render them locale- and timezone-aware —
the same Intl formatting the tables use. The value axis (always numeric) takes a NumberFormat; the
category axis takes either a NumberFormat or a DateFormat.
Chart::make('Revenue', 'formatting-chart') ->description('Compact currency on the value axis, month labels on the category axis') ->categoryKey('month') ->data([ ['month' => '2026-01-01', 'revenue' => 28000], ['month' => '2026-02-01', 'revenue' => 32000], ['month' => '2026-03-01', 'revenue' => 36500], ['month' => '2026-04-01', 'revenue' => 41500], ]) ->line('revenue', 'Revenue') ->categoryFormat(DateFormat::monthYear()) // raw dates → 'Jan 2026', localized ->valueFormat(NumberFormat::currency('USD')->compact()) // 28000 → $28K ->height(260);- categoryFormat:{"dateStyle":null,"kind":"date","month":"short","timeStyle":null,"year":"numeric"}
- categoryKey:"month"
- data:[{"month":"2026-01-01","revenue":28000},{"month":"2026-02-01","revenue":32000},{"month":"2026-03-01","revenue":36500},{"month":"2026-04-01","revenue":41500}]
- description:"Compact currency on the value axis, month labels on the category axis"
- grid:true
- height:260
- legend:true
- title:"Revenue"
- tooltip:true
- valueFormat:{"currency":"USD","kind":"number","maximumFractionDigits":null,"minimumFractionDigits":null,"notation":"compact","unit":null}
- xAxis:true
- yAxis:true
NumberFormat mirrors the numeric table columns: ->decimals($min, $max?), ->compact(),
NumberFormat::currency($code), and ->unit(NumberFormatUnit::Percent). For dates,
DateFormat::date(), ::time(), and ::dateTime() take a DateTimeStyle (Full/Long/Medium/Short),
while DateFormat::month() and ::monthYear() render just the month (Jan) or month and year
(Jan 2026) — pass long: true for the full month name. All format with the active locale and
timezone, so you feed the chart raw dates instead of pre-translated labels.
Chrome
Section titled “Chrome”Every part of the chart frame toggles independently. All default to on:
->legend(bool)— the series legend.->tooltip(bool)— the hover tooltip.->grid(bool)— the background grid.->xAxis(bool)/->yAxis(bool)— the axes.->description($text)— a subtitle under the title.
Chart::make('Signups') ->data($rows) ->categoryKey('month') ->line('total') ->legend(false) ->grid(false);Dynamic series
Section titled “Dynamic series”When the series aren’t known ahead of time, build ChartSeries value objects
(typed by ChartSeriesType) and hand them to ->series([...]) instead of the per-type helpers. The static factories
mirror the fluent methods:
use Lattice\Ui\Values\ChartSeries;
$series = collect($metrics) ->map(fn (string $key): ChartSeries => ChartSeries::line($key, ucfirst($key))) ->all();
Chart::make('Metrics') ->data($rows) ->categoryKey('day') ->series($series);