Skip to content

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);

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; $nameKey names each slice.
  • ->doughnut($dataKey, $nameKey?, $name?, $color?, $innerRadius = '60%') — a pie with a hole; $innerRadius sizes 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).

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);

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);

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);

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);

->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);

->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);

Gauges ignore the grid and axes, so ->categoryKey() and ->categoryFormat() have no effect — ->valueFormat() drives the center label and tooltip.

->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());

Distribution bars render as plain markup and size to their content, so ->height(), the grid, and the axes have no effect.

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.

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);

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.

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);

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);