Chart widget
A chart rendered with Chart.js.
ChartWidget::make('Pipeline value') ->type(ChartWidget::DOUGHNUT) ->money('EUR') ->permission('deals.view') ->data(static function (): array { $rows = (new DealModel())->select('stage, COALESCE(SUM(amount), 0) AS value')->groupBy('stage')->findAll();
return [ 'labels' => array_column($rows, 'stage'), 'datasets' => [['label' => 'Amount', 'data' => array_column($rows, 'value')]], ]; });| Method | Default | Description |
|---|---|---|
make(string $label) |
– | heading of the card, also the accessible name of the chart |
type(string $type) |
ChartWidget::BAR |
BAR, LINE, DOUGHNUT, PIE; other values throw |
data(Closure $data) |
– | returns ['labels' => [...], 'datasets' => [['label' => ..., 'data' => [...]], ...]], evaluated when the widget is shown |
description(string $text) |
– | line below the heading |
money(string $currency = 'EUR') |
– | formats values on the axis and in tooltips as money (ISO code of three capital letters, otherwise it throws) |
horizontal(bool $horizontal = true) |
false |
bars (or lines) along the y axis |
stacked(bool $stacked = true) |
false |
stacks the datasets |
height(int $height) |
260 |
height in pixels, 120–800 |
permission(string $permission) |
– | required permission |
span(int $span) |
6 |
width on large screens, 3–12 |
key(string $key) |
from the label | stable key of the layout |
Examples
Section titled “Examples”// Line: requests per month of the last twelve monthsChartWidget::make('Requests per month')->type(ChartWidget::LINE)->key('requests-per-month') ->permission('purchase-requests.view') ->data(function (): array { $rows = $this->resourceByClass(PurchaseRequestResource::class)->query() ->select("DATE_FORMAT(created_at, '%Y-%m') AS month, COUNT(*) AS requests", false) ->where('created_at >=', date('Y-m-01', strtotime('-11 months'))) ->groupBy('month')->orderBy('month')->findAll();
return ['labels' => array_column($rows, 'month'), 'datasets' => [['label' => 'Requests', 'data' => array_column($rows, 'requests')]]]; }),
// Horizontal, stacked bars with two datasetsChartWidget::make('Tasks per team')->horizontal()->stacked()->height(320) ->data(static fn (): array => [ 'labels' => ['IT', 'HR', 'Facility'], 'datasets' => [['label' => 'Open', 'data' => [5, 2, 1]], ['label' => 'Done', 'data' => [12, 9, 4]]], ]),Query through the resource ($panel->resourceByClass(...)->query()) so that
tenant and scopeQuery() apply.
config() normalises what data() returns before it reaches the browser:
- Labels become strings and are shown as text, never as HTML.
- Values may be numeric strings, e.g. the result of
SUM()in SQL; they are converted to numbers. Other values becomenull(a gap). - At most 12 datasets with 500 values each; the rest is cut off.
- Bar, line: one colour per dataset. Doughnut, pie: one colour per slice. The legend is shown for round charts and for more than one dataset.
- Numbers are formatted with the language of the page (
<html lang>). When all values are integers, the axis uses whole steps.
config() is public, so a test can check the data without a browser:
$this->assertSame([4, 7, 2], $chart->config()['datasets'][0]['data']);How charts are rendered
Section titled “How charts are rendered”- The configuration is a JSON data attribute of a
<canvas>(data-anvil-chart); no script is rendered into the page, which keeps the Content Security Policy intact. The height is a CSS class (anvil-chart-h-<n>, rounded down to steps of 20 px), not astyleattribute. - Chart.js is not part of
anvil.js. It is a separate file (anvil-charts.js) thatanvil.jsloads with its integrity hash the first time a page contains a chart, also after htmx swaps. Pages without charts never load it. - Colours come from the theme (
--anvil-primaryand the Bootstrap colours) and follow light and dark mode; charts are redrawn when the user switches the theme. - Every chart also contains its data as a visually hidden table for screen readers.