Core
Charts
Line, area and bar charts — where the data comes from, what is drawn in the browser, and the item each series is made of.
Pure-CSS bars: vertical finance, vertical system metrics with grid lines, and horizontal progress.
On this page
- ChartWidget
- Basic Usage
- Chart Types
- Dynamic Data with Closures
- Filter Dropdown
- ChartWidget API
- Convenience Widgets
- Chart.js Options
- BarChartWidget
- Finance bars
- System metrics (vertical, with grid lines)
- System metrics (horizontal)
- How fill height is resolved
- Safe colors
- Validation
- BarChartWidget API
- ChartItem
- ChartItem API
- Related
A chart widget is a query and a shape: the data is assembled on the server, the
drawing happens in the browser, and the boundary between the two decides what a
refresh costs. BarChartWidget is the one that needs no JavaScript at all, which
is why it is a separate class rather than an option.
ChartWidget
Chart widget with Chart.js integration. Supports line, bar, pie, and doughnut charts.
use NyonCode\WireCore\Widgets\ChartWidget;
Requires Chart.js. The widget renders a
<canvas>and initializes it through Alpine. Include Chart.js on the page — via CDN or your bundle — or the canvas stays empty and a console warning is logged. Dataset styling (borderColor,fill,tension, …) is passed straight through to Chart.js.The widget's own Alpine controller needs nothing from you: it ships as a package bundle and the widget fetches it when it renders. Charts are a heavy, optional asset, so it is deliberately not in the always-loaded
@wireStackScriptsset.
Basic Usage
ChartWidget::make() ->heading('Revenue Over Time') ->type('line') ->labels(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']) ->datasets([ [ 'label' => 'Revenue', 'data' => [1200, 1900, 3000, 5000, 2300, 3200], 'borderColor' => '#3B82F6', ], ])
Chart Types
->type('line') // line chart (default)->type('bar') // bar chart->type('pie') // pie chart->type('doughnut') // doughnut chart
Dynamic Data with Closures
Datasets and labels accept Closures. The active filter value is passed as argument:
ChartWidget::make() ->heading('Sales') ->type('bar') ->filter(['2025' => '2025', '2026' => '2026'], '2026') ->labels(fn (?string $filter) => match($filter) { '2025' => ['Q1', 'Q2', 'Q3', 'Q4'], '2026' => ['Q1', 'Q2'], default => [], }) ->datasets(fn (?string $filter) => [ ['label' => 'Sales', 'data' => $filter === '2025' ? [100, 200, 150, 300] : [180, 250]], ])
Filter Dropdown
->filter(array $options, ?string $default = null)
Adds a dropdown on the widget. The selected key is passed to the dataset and label closures — on the server, which is where those closures live.
ChartWidget::make() ->heading('Revenue') ->filter([ 'week' => 'This Week', 'month' => 'This Month', 'year' => 'This Year', ], 'month')
Changing the selection calls filterWidget() on the host, which re-resolves the
closures and answers with this widget's markup alone. The chart's wrapper carries
the active key in its wire:key, so the morph replaces the element rather than
patching it — Alpine never re-evaluates x-data on an element it has already
initialised, so a patched attribute would be read by nobody and Chart.js would
keep drawing the old series. The replacement tears the old chart down through the
controller's destroy() and builds a new one over the new data.
This is new in 2.0. The filter used to be resolved in the browser: an Alpine
updateChart()assignedthis.labelsandthis.datasetsback onto the chart it was constructed with, so changing the selection redrew the identical chart and the closures never ran with anything but their default.
filter() is no longer a chart feature — it is on the widget base, so a list or
a progress board takes the same options map. See
Filters.
ChartWidget API
->type(string $type) // 'line', 'bar', 'pie', 'doughnut'->getType(): string->datasets(array|Closure $datasets) // Chart.js dataset format->getDatasets(): array->labels(array|Closure $labels) // x-axis labels->getLabels(): array->options(array $options) // Chart.js options merged over the type defaults->getOptions(): array
filter(), activeFilter() and their getters are shared by every widget and
documented on the widget base.
Convenience Widgets
Declarative presets over ChartWidget, so a dashboard states intent instead of ->type(...):
use NyonCode\WireCore\Widgets\DoughnutChartWidget;use NyonCode\WireCore\Widgets\LineChartWidget;use NyonCode\WireCore\Widgets\PieChartWidget; LineChartWidget::make()->heading('Revenue')->labels([...])->datasets([...]);PieChartWidget::make()->heading('By Category')->labels([...])->datasets([...]);DoughnutChartWidget::make()->heading('By Status')->labels([...])->datasets([...]);
PieChartWidget and DoughnutChartWidget show the Chart.js legend by default (top position) — pie slices rely on it. Everything else matches ChartWidget.
Chart.js Options
Override any Chart.js option with options(); the array is merged over the type's defaults (responsive: true, maintainAspectRatio: false, plus the pie/doughnut legend), so you only specify what changes:
LineChartWidget::make() ->datasets([...]) ->options([ 'scales' => ['y' => ['beginAtZero' => true]], 'plugins' => ['legend' => ['display' => false]], ])
BarChartWidget
A dependency-free bar chart rendered entirely with Tailwind utility classes — no Chart.js, no <canvas>, no JavaScript. Use it for compact, print-friendly dashboards. It is a distinct widget from ChartWidget; both can live on the same dashboard.
use NyonCode\WireCore\Widgets\BarChartWidget;use NyonCode\WireCore\Widgets\ChartItem;
The widget has three visual modes, picked from type() + variant():
type() |
variant() |
Look |
|---|---|---|
vertical |
finance |
Vertical bars: formatted value above, light max-height track, MM / YYYY caption below |
vertical |
system / default |
Vertical bars on a 0–100% track with an icon + label + percentage header and optional grid lines |
horizontal |
system / default |
Horizontal progress bars: label on the left, value on the right |
Finance bars
BarChartWidget::make() ->heading('Přehled tržeb') ->type('vertical') ->variant('finance') ->items([ ChartItem::make('01 / 2024')->value(125000)->formattedValue('125 000 Kč')->color('blue')->percentage(70), ChartItem::make('02 / 2024')->value(98500)->formattedValue('98 500 Kč')->color('green')->percentage(55), ])
System metrics (vertical, with grid lines)
BarChartWidget::make() ->heading('Přehled systému') ->type('vertical') ->variant('system') ->showGrid() // 0% / 25% / 50% / 75% / 100% guide lines ->showMenu() // a "⋯" options affordance in the card header ->maxValue(100) // percentage mode (0–100 track) ->verticalLabels() // rotate each bar's label vertically beside it (fits long names) ->items([ ChartItem::make('CPU')->value(72)->formattedValue('72 %')->icon('cpu-chip')->color('blue')->percentage(72), ChartItem::make('RAM')->value(54)->formattedValue('54 %')->icon('circle-stack')->color('green')->percentage(54), ChartItem::make('Disk')->value(81)->formattedValue('81 %')->icon('server')->color('orange')->percentage(81), ChartItem::make('GPU')->value(36)->formattedValue('36 %')->icon('bolt')->color('purple')->percentage(36), ])
System metrics (horizontal)
Same items, switch type('horizontal'):
BarChartWidget::make() ->type('horizontal') ->variant('system') ->maxValue(100) ->items([ /* ChartItem… */ ])
How fill height is resolved
Each bar's fill percentage (percentageFor(ChartItem)) is resolved in this order:
- An explicit per-item
->percentage(0–100)wins. - Otherwise the value is scaled against the widget
->maxValue(). - Otherwise (percentage mode with no ceiling) the value is auto-scaled against the largest item.
The result is always clamped to 0–100. The fill size is the only dynamic style, passed as a CSS variable and consumed by Tailwind arbitrary values:
<div class="… h-[var(--value)]" style="--value: 72%"></div>
Safe colors
color() values map through a fixed allow-list (HasColor::getGradientFillClasses() / getFillTextClasses()) — owner-supplied strings can never inject arbitrary classes. Supported chart hues:
| key | fill gradient | accent text |
|---|---|---|
blue |
from-blue-500 to-blue-600 |
text-blue-600 |
green |
from-green-500 to-green-600 |
text-green-600 |
orange |
from-orange-500 to-orange-600 |
text-orange-600 |
purple |
from-purple-500 to-purple-600 |
text-purple-600 |
gray |
from-slate-400 to-slate-500 |
text-slate-600 |
(The brand primary alias and the wider palette vocabulary — red, amber, cyan, pink, … — are accepted too.)
Validation
->type('diagonal'); // throws InvalidArgumentException (allowed: vertical, horizontal)->variant('pie'); // throws InvalidArgumentException (allowed: finance, system, default)ChartItem::make('CPU')->percentage(120); // throws InvalidArgumentException (0–100)
BarChartWidget API
->type(string $type) // 'vertical' | 'horizontal' (validated)->getType(): string->variant(string $variant) // 'finance' | 'system' | 'default' (validated)->getVariant(): string->items(array|Closure $items) // array<ChartItem>, or fn (?string $filter): array — validated either way->getItems(): array->showGrid(bool $show = true) // grid lines (system vertical)->shouldShowGrid(): bool->showMenu(bool $show = true) // card-header options affordance->shouldShowMenu(): bool->maxValue(int|float|null $max) // absolute ceiling; null = percentage mode->getMaxValue(): ?float->height(int $px) // vertical plot height (default 240)->getHeight(): int->verticalLabels(bool $on = true) // rotate each bar's label vertically beside it (vertical charts; fits long names)->hasVerticalLabels(): bool->rounded(string $scale) // card radius: 'lg' | 'xl' | '2xl' (default) | '3xl' | …->getRounded(): string->percentageFor(ChartItem $item): float // resolved 0–100 fill->fillClassesFor(ChartItem $item): string // safe gradient classes->textClassesFor(ChartItem $item): string // safe accent text classes
ChartItem
A single bar in a BarChartWidget.
use NyonCode\WireCore\Widgets\ChartItem;
ChartItem API
ChartItem::make(string $label)->value(int|float $value) // raw numeric value->getValue(): float->formattedValue(?string $formatted) // display string, e.g. '125 000 Kč' / '72 %'->getFormattedValue(): string // falls back to the raw value->color(string|Color|null $color) // safe color key (default 'primary')->getColor(): string->percentage(int|float $percentage) // explicit 0–100 fill (validated)->getPercentage(): ?float->hasPercentage(): bool->icon(string|Icon|null $icon) // icon name (system/horizontal variants)->getIcon(): ?string->getLabel(): string->extraAttributes(array $attrs)
Related
- Widgets — polling, authorization and the shared API
- Stats — a figure where a chart would be too much
- Colors — where a series' colour is resolved
- JavaScript Assets — what a chart puts on the page