Chart
The <x-wirekit-chart /> component renders charts with automatic WireKit theming. WireKit ships two built-in adapters — chartjs (default, MIT-licensed) and apexcharts (optional, non-MIT — see License below). Both use the same Blade tag; switching is a single config-line change.
Charts are disabled by default. Enable them in your config before using the component.
Choosing a chart library
| Need | Pick |
|---|---|
| Simple bar / line / pie / radar / scatter charts. MIT license. Smallest bundle. | Chart.js |
| Financial charts (candlestick, OHLC), heatmaps, treemaps, range-bars, sparklines. Polished defaults. | ApexCharts |
| Both — pick per-page via config | Either; the public <x-wirekit-chart> Blade tag is identical for both |
The choice happens once in config/wirekit.php and applies as the application default. Two demo subhierarchies on the docs site walk every chart-type variation each library supports:
- Chart.js demo gallery — every chart family Chart.js ships natively, with realistic seed data
- ApexCharts demo gallery — every ApexCharts chart family including financial charts, sparklines, annotations, real-time streaming
Individual charts can override the default with the library prop — pass library="apexcharts" (or library="chartjs", or a custom adapter FQCN) on a single <x-wirekit-chart> tag and that chart binds to the named adapter regardless of the global config. Useful when one page mixes ApexCharts-only types (boxplot, candlestick, heatmap, treemap) with Chart.js elsewhere in a Chart.js-default app, without flipping the global setting.
Requirements
When using Chart.js (charts.library => 'chartjs')
Install Chart.js via npm:
npm install chart.js
Import and register in your app.js:
import { Chart, registerables } from 'chart.js';
Chart.register(...registerables);
When using ApexCharts (charts.library => 'apexcharts')
Install ApexCharts via npm:
npm install apexcharts
Supported versions: ApexCharts 5 and 6, verified in a real browser against 5.16.0, 6.4.0 and 6.5.0. A major bump on the ApexCharts side does not need a WireKit release to go with it — if you pin the version, pin it for your own reasons rather than out of concern that the adapter has a ceiling.
Import and expose globally in your app.js:
import ApexCharts from 'apexcharts';
window.ApexCharts = ApexCharts;
Then switch the adapter on in config/wirekit.php:
// 1. Load WireKit's ApexCharts glue alongside the main bundle.
// Off by default so an app without charts ships no chart code.
'scripts' => [
'bundle' => 'full',
'apex' => true,
],
@wirekitScripts now emits both tags, in the order the adapter needs.
If the chart area stays empty
A missing adapter fails quietly: the page renders and only the chart is absent. Open the console — wirekitApexChart is not defined means this option is still off.
If you write your own script tags instead of using @wirekitScripts, the adapter goes below the main bundle:
{{-- 1. The library itself, which registers WireKit's Alpine components. --}}
<script src="{{ asset('vendor/wirekit/wirekit.js') }}"></script>
{{-- 2. The ApexCharts glue, which registers itself against it — so it comes second. --}}
<script src="{{ asset('vendor/wirekit/wirekit-apex.js') }}"></script>
The adapter bundle (dist/wirekit-apex.js, size in Dependencies) registers the wirekitApexChart Alpine factory. It contains zero ApexCharts code — the JS library you installed via npm is what does the actual rendering.
License Notice ApexCharts is not MIT-licensed. The free Community License covers personal use, non-profits, education, and organizations under $2 million USD annual revenue. Above that threshold you must purchase a Commercial License directly from ApexCharts. WireKit ships only the adapter glue (MIT); the ApexCharts JS library is your responsibility to install and license. See the License section below for details.
Charts require the @wirekitScripts directive in your layout. Both the full and core bundles include the Chart.js Alpine component. The ApexCharts adapter ships as a separate wirekit-apex.js bundle so developers who don't use ApexCharts pay zero bytes. See Getting Started for setup details.
Setup
Enable charts in config/wirekit.php:
'charts' => [
'library' => 'chartjs',
],
Or 'apexcharts':
'charts' => [
// 1. Switch the active adapter — the Blade tag stays the same.
'library' => 'apexcharts',
// 2. Optional: declare your ApexCharts license tier.
// Acceptable values: 'community' (default — under $2M USD revenue),
// 'commercial' (paid subscription), 'oem' (redistribution license).
// The wirekit:doctor reminder is suppressed for 'commercial' / 'oem'.
'apex_license' => 'community',
],
Usage
Pass labels and datasets as arrays — data can come from Eloquent, an API, or inline values:
<x-wirekit-chart
type="bar"
:labels="$stats->pluck('month')->toArray()"
:datasets="[
['label' => 'Revenue', 'data' => $stats->pluck('revenue')->toArray()],
['label' => 'Costs', 'data' => $stats->pluck('costs')->toArray()],
]"
/>
Or with inline data for quick prototyping:
<x-wirekit-chart
type="bar"
:labels="['Jan', 'Feb', 'Mar', 'Apr']"
:datasets="[
['label' => 'Revenue', 'data' => [12400, 15800, 18200, 16900]],
['label' => 'Costs', 'data' => [8200, 9100, 10400, 11200]],
]"
/>
Width & Layout
The chart fills its parent width and uses the height prop for its vertical dimension (default: 380px). Control width via the parent:
<div class="max-w-xl">
<x-wirekit-chart type="bar" height="250px" :data="$data" />
</div>
Charts are responsive by default — they resize when the parent container changes width.
Tooltip value formatting
ApexCharts tooltips print the raw numeric value at the hovered point — for float data that means the full 64-bit representation (50.523626895740676). The valueDecimals prop rounds it; valuePrefix / valueSuffix wrap it with a currency symbol or unit. All three are no-ops on the Chart.js adapter (its tooltip path is configured separately through options).
Hover any point: the tooltip reads 58.91 ms instead of 58.910020000000001.
Chart Types
<x-wirekit-chart> accepts a type prop that maps to either Chart.js or ApexCharts depending on which library is registered as the active adapter (see Setup above). The two adapters share the WireKit data shape — same labels, same datasets, same options deep-merge — so swapping the active adapter changes the rendering library WITHOUT touching the developer's Blade markup.
The preview above is the canonical <x-wirekit-chart> API in its smallest form: a type=, a labels array, one dataset. The dedicated demo pages under Chart.js Demos and ApexCharts Demos carry the per-type live previews + Blade snippets. The table below maps each canonical chart type to its demo page on each adapter so you can compare the rendering side by side before picking a library.
type= |
Chart.js demo | ApexCharts demo | Notes |
|---|---|---|---|
bar |
Bar | Bar / Column | Chart.js renders vertical by default; ApexCharts splits horizontal (bar) and vertical (column) into separate types |
line |
Line | Line | Smooth curves on both adapters; tension/curve configurable via options |
area |
Area | Area | Filled-line variant. Chart.js uses fill: true on the dataset; ApexCharts uses chart.type: 'area' |
pie |
Advanced | Pie / Donut | Both adapters expect a single dataset with data as a flat numeric array + top-level labels per slice |
doughnut |
Advanced | Pie / Donut | Same data shape as pie; ring-shape variant |
radar |
Advanced | Radar | Polygon chart for multi-axis comparison |
scatter |
Advanced | Scatter / Bubble | XY data points — pass data: [{x, y}, …] |
bubble |
Advanced | Scatter / Bubble | Scatter + per-point size — pass data: [{x, y, r}, …] |
mixed |
Advanced | Mixed | See dedicated <x-wirekit::chart-mixed> wrapper for per-dataset type bindings |
sparkline |
(see component) | Sparklines | See dedicated <x-wirekit::sparkline> wrapper for inline KPI strips |
heatmap |
— | Heatmap | ApexCharts-only |
treemap |
— | Treemap | ApexCharts-only |
boxplot |
— | Boxplot | ApexCharts-only |
candlestick |
— | Candlestick | ApexCharts-only |
range-bar |
— | Range Bar | ApexCharts-only |
radial-bar |
— | Radial Bar | ApexCharts-only |
funnel |
— | Funnel | ApexCharts-only |
timeline |
— | Timeline | ApexCharts-only — built on top of range-bar |
ApexCharts-only types throw Pushery\WireKit\Charts\TypeNotSupportedException when the Chart.js adapter is active — the exception message includes a "switch to apexcharts" hint. See the Choosing a chart library section above.
Database Example
A real-world example showing the complete flow from database to chart — exactly how you'd build a dashboard in your own app.
1. Migration
Schema::create('monthly_stats', function (Blueprint $table) {
$table->id();
$table->string('month', 10); // 'Jan', 'Feb', ..., 'Dec'
$table->unsignedSmallInteger('year'); // 2025
$table->unsignedInteger('revenue'); // Monthly revenue in EUR
$table->unsignedInteger('costs'); // Monthly costs in EUR
$table->unsignedInteger('visitors'); // Unique visitors
$table->unsignedInteger('desktop_visits');
$table->unsignedInteger('mobile_visits');
$table->unsignedInteger('tablet_visits');
$table->timestamps();
$table->unique(['month', 'year']);
});
2. Model
class MonthlyStat extends Model
{
protected $fillable = [
'month', 'year', 'revenue', 'costs', 'visitors',
'desktop_visits', 'mobile_visits', 'tablet_visits',
];
public static function forYear(int $year): Collection
{
$order = ['Jan','Feb','Mar','Apr','May','Jun',
'Jul','Aug','Sep','Oct','Nov','Dec'];
return static::where('year', $year)
->get()
->sortBy(fn ($s) => array_search($s->month, $order))
->values();
}
}
3. Livewire Component
class Dashboard extends Component
{
public function render(): View
{
$stats = MonthlyStat::forYear(2025);
return view('livewire.dashboard', [
'labels' => $stats->pluck('month')->toArray(),
'revenue' => $stats->pluck('revenue')->toArray(),
'costs' => $stats->pluck('costs')->toArray(),
'visitors' => $stats->pluck('visitors')->toArray(),
'devices' => [
$stats->sum('desktop_visits'),
$stats->sum('mobile_visits'),
$stats->sum('tablet_visits'),
],
]);
}
}
4. Blade View
{{-- Revenue vs Costs --}}
<x-wirekit-chart
type="bar"
:labels="$labels"
:datasets="[
['label' => 'Revenue (EUR)', 'data' => $revenue],
['label' => 'Costs (EUR)', 'data' => $costs],
]"
/>
{{-- Visitor trend --}}
<x-wirekit-chart
type="line"
:labels="$labels"
:datasets="[['label' => 'Visitors', 'data' => $visitors]]"
/>
{{-- Device breakdown --}}
<x-wirekit-chart
type="doughnut"
:labels="['Desktop', 'Mobile', 'Tablet']"
:datasets="[['data' => $devices]]"
height="300px"
/>
That's it — three charts powered by one Eloquent query. WireKit handles all Chart.js configuration, theming, and dark mode automatically.
5. Rendered output
The previews below show what those three Blade snippets render against a representative seeded year (12 months of revenue / costs / visitor data, one row per month, summed for the device split). Real apps swap the inline arrays for the $labels / $revenue / $costs / $visitors / $devices variables the Livewire component passes in step 3 — the data shape is identical.
Custom Options
Pass native Chart.js options to override defaults:
<x-wirekit-chart
type="line"
:labels="$months"
:datasets="[['label' => 'Revenue', 'data' => $values]]"
:options="[
'scales' => ['y' => ['beginAtZero' => false, 'min' => 100]],
'plugins' => ['legend' => ['display' => false]],
]"
/>
User options are deep-merged with WireKit defaults. User values win on conflicts.
Manual Dataset Colors
Override automatic theming by setting colors directly:
<x-wirekit-chart
type="bar"
:labels="$months"
:datasets="[
[
'label' => 'Revenue',
'data' => $values,
'backgroundColor' => 'rgba(59, 130, 246, 0.5)',
'borderColor' => '#3b82f6',
],
]"
/>
Datasets with manual backgroundColor or borderColor are excluded from automatic dark mode re-theming. If your manual colors need to adapt to dark mode, use CSS variables or handle theme changes in your own JavaScript.
Livewire Integration
The component includes wire:ignore to prevent Livewire's DOM morphing from destroying the chart. Charts are automatically cleaned up during wire:navigate page transitions.
Reactive chart updates (data changes via Livewire) are planned for a future version. Currently, charts render once on page load.
Custom Adapters
Create a class implementing Pushery\WireKit\Contracts\ChartAdapter:
use Pushery\WireKit\Contracts\ChartAdapter;
class ApexChartsAdapter implements ChartAdapter
{
public function scripts(): array { return []; }
public function normalizeData(string $type, array $labels, array $datasets): array
{
// Transform to ApexCharts format
}
public function defaultOptions(string $type): array
{
// ApexCharts default options
}
public function alpineComponent(): string
{
return 'wirekitApexCharts';
}
}
Set in config:
'charts' => [
'library' => \App\Charts\ApexChartsAdapter::class,
],
Production
Cache icons for Blade Icons (if using icon components alongside charts):
php artisan icons:cache
License (ApexCharts only)
This section applies ONLY when charts.library is 'apexcharts'. Chart.js is MIT-licensed end-to-end with no per-revenue terms; developers using the default 'chartjs' adapter can skip this section.
Three license tiers
ApexCharts is distributed under one of three licenses. The applicable tier is determined by your organization, not by WireKit. WireKit ships only the adapter glue; the ApexCharts JS library is a separate npm package you install yourself.
| Tier | Who qualifies | Cost |
|---|---|---|
| Community (default) | Personal projects, non-profits, education, AND organizations with < $2 million USD annual revenue (consolidated across affiliates and parent/subsidiary entities under common control). | Free |
| Commercial | Organizations with ≥ $2 million USD annual revenue. Includes internal tools, client work, embedded usage. 12-month subscription. | Paid |
| OEM / Redistribution | Required when ApexCharts is embedded into a redistributed product, SDK, platform, or tool. NOT applicable to WireKit's adapter posture (we do not embed). | Paid (separate) |
Source of truth: apexcharts.com/license. The summary above is informational; you are responsible for verifying your own tier against ApexCharts's then-current legal terms.
Why WireKit does not need an OEM license
The OEM/Redistribution tier kicks in when a vendor embeds ApexCharts into a redistributed product. WireKit does NOT embed ApexCharts:
- The adapter glue (
src/Charts/ApexChartsAdapter.php+dist/wirekit-apex.js) maps WireKit's data shape into the ApexCharts options object. - WireKit does NOT bundle
apexcharts.min.jsin anydist/artifact. - WireKit does NOT include ApexCharts source in
composer.jsonorpackage.json. - You install ApexCharts yourself via
npm install apexcharts.
Same posture WireKit holds for Chart.js. The adapter glue is MIT (WireKit's license); the JS library is your responsibility to license correctly.
Recording your tier in config/wirekit.php
'charts' => [
'library' => 'apexcharts',
'apex_license' => 'commercial', // 'community' | 'commercial' | 'oem'
],
apex_license is informational — WireKit gates nothing on its value. It serves two purposes:
- Audit trail — a single source of truth records which tier you've adopted alongside the rest of your config.
- Doctor reminder —
php artisan wirekit:doctoremits a WARN-level reminder pointing at the ApexCharts license terms whenapex_licenseis'community'or unset. The reminder is suppressed for'commercial'and'oem'.
Doctor output examples
When apex_license is 'community' or unset:
[WARN] charts: ApexCharts is non-MIT. Confirm your organization is below the
$2M USD revenue threshold for the Community License, or purchase a
Commercial License at https://apexcharts.com/license/.
When apex_license is 'commercial' or 'oem':
[PASS] charts: ApexCharts adapter active. License tier: commercial.
Pitfalls
- Don't bundle
apexcharts.min.jsinto a deployment artifact you redistribute (e.g. an admin-panel package you ship to multiple clients). That triggers the OEM/Redistribution tier — separate from the per-organization Commercial license. - Don't quote a stale revenue threshold figure to your team. ApexCharts's terms can change; always link to apexcharts.com/license rather than hard-coding the number in your README.
- Don't conflate Community License with public-domain. ApexCharts retains copyright; you're using it under license terms, not as a freely modifiable codebase.
Behavior
- Charts disabled (default):
RuntimeExceptionwith config instructions. - Chart.js not installed:
console.errorin browser with npm install instructions. - ApexCharts not installed (adapter bundle loaded):
console.errorPLUS a visible in-DOM advisory panel inside the chart container — surfaces the install command, the license reminder, and a link toapexcharts.com/licenseso the developer can act without opening DevTools first. - Unknown adapter:
InvalidArgumentExceptionlisting available adapters. - Custom adapter not implementing interface:
InvalidArgumentException.
Troubleshooting: blank chart with no console error
When <x-wirekit-chart library="apexcharts"> (or library="chartjs") renders an empty container with no visible advisory panel and no console.error, the most likely cause is the adapter bundle itself is not loaded — Alpine sees x-data="wirekitApexChart(...)" referencing a factory function that was never registered. Check the browser's network panel for a 404 on dist/wirekit-apex.js (when using ApexCharts) or dist/wirekit.js / dist/wirekit.core.js (when using Chart.js). The visible-fallback panel only renders when the WireKit adapter bundle IS loaded but the underlying chart library (window.ApexCharts / window.Chart) is missing.
The library prop changes the Alpine factory name the chart's x-data references. When the per-instance library differs from your app's default (config('wirekit.charts.library')), you must load BOTH the corresponding adapter bundle AND the underlying chart library. Mixing libraries on the same page is supported but doubles the JS payload — verify against your bundle budget before shipping.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type |
string |
'bar' |
Chart type: bar, line, pie, doughnut, radar, polarArea |
labels |
array |
[] |
X-axis labels |
datasets |
array |
[] |
Data arrays with label and data keys |
options |
array |
[] |
Native Chart.js options (merged with defaults) |
height |
string |
'380px' |
Container height |
inline |
bool |
false |
Render the wrapper as <span style="display: inline-block"> (and the SVG mount as <span>) instead of the default <div>. Required to place a chart inside a <p> — HTML5's parser auto-closes a <p> the moment it encounters a <div> descendant. <x-wirekit::sparkline inline> flips this automatically; pass it directly on <x-wirekit-chart> for inline-chart usage in narrative prose. |
replayable |
bool |
false |
Opt INTO docs.wirekit.app ↻ Replay button surface by emitting data-replayable="true" on the chart root. Set it on charts whose entrance animation is worth re-watching (bar-grow, line-trace, slice-sweep). Auto-enabled whenever wireStream is bound — every streaming chart is replay-worthy because clicking replay resets the live ticker. On a developer app the prop is a no-op (your site presumably has no equivalent button); it only changes the rendered HTML to include the data attribute. |
library |
?string |
null |
Per-instance chart-library override. Accepts the same values as config('wirekit.charts.library') — the built-in keys chartjs / apexcharts, OR a fully qualified class name implementing ChartAdapter. When null (default), the chart uses whatever the global wirekit.charts.library config resolves to. When set, this chart instance binds to the named library regardless of the app default; two charts on the same page can use different libraries. |
valueDecimals |
?int |
null |
ApexCharts only. Rounds raw float y-values in the tooltip to N decimal places. Without it, a value like 50.523626895740676 prints in full; valueDecimals="2" shows 50.52. null (default) keeps the raw value. |
valuePrefix |
?string |
null |
ApexCharts only. Text prepended to each formatted tooltip value (e.g. "€", "$"). |
valueSuffix |
?string |
null |
ApexCharts only. Text appended to each formatted tooltip value (e.g. " ms", "%"). Combine with valueDecimals for "50.52 ms". |
Accessibility
Charts are a well-known accessibility challenge: the visual data is rendered to a <canvas> element, which is opaque to screen readers. WireKit recommends the following patterns:
1. Label the chart as an image
Wrap your chart in a container with role="img" and a descriptive aria-label:
<div role="img" aria-label="Monthly revenue chart, January to December 2025. Peaks at €42,000 in December.">
<x-wirekit-chart type="line" :labels="$months" :datasets="$datasets" />
</div>
The aria-label should summarize what the data shows, including:
- The metric (e.g. "Monthly revenue")
- The time range or categories
- Key takeaways (peaks, trends, comparisons)
This is the minimum A11y contract — every chart should have one.
2. Provide a data table as a fallback
For full keyboard and screen-reader accessibility, render the same data as a native <table> that is visually hidden but available to assistive technology:
<figure>
<figcaption class="sr-only">Monthly revenue for 2025</figcaption>
<x-wirekit-chart type="bar" :labels="$months" :datasets="$datasets" />
<table class="sr-only">
<caption>Monthly revenue 2025 (EUR)</caption>
<thead>
<tr><th scope="col">Month</th><th scope="col">Revenue</th></tr>
</thead>
<tbody>
@foreach($months as $i => $month)
<tr>
<th scope="row">{{ $month }}</th>
<td>€{{ number_format($datasets[0]['data'][$i]) }}</td>
</tr>
@endforeach
</tbody>
</table>
</figure>
Benefits:
- Screen readers can navigate cell-by-cell with arrow keys.
- Keyboard users skip over the canvas to the semantic table.
- Progressive enhancement — if JS fails, the table is the primary content.
3. Do not rely on color alone
Chart.js colors encode categories (e.g. different datasets). Meet WCAG 1.4.1 ("Use of Color") by also distinguishing datasets via:
- Different line styles (
borderDash: [5, 5]) on line charts - Different point shapes (
pointStyle: 'triangle' | 'rect' | 'circle') - Patterns on bar fills (via Chart.js plugins like patternomaly)
4. Include the summary in surrounding text
The best chart A11y pattern is one the user doesn't even need: describe the trend in the paragraph before or after the chart. Screen readers read content linearly — a sentence like "Revenue grew 34% year-over-year, peaking in December" reaches more users than any aria-label.
References
Keyboard Interaction
This component is purely presentational and does not respond to keyboard input.
Pitfalls
- Don't render a chart without a text alternative. WCAG 1.1.1 (Non-text Content) — pair the chart with a
<x-wirekit::data-list>summarizing the trend, or a<x-wirekit::stat>highlighting the headline number. - Don't bind
wire:model.liveto chart data. Each tick re-creates the Chart.js instance. Use Livewire's$wire.set()and refresh on a debounced trigger.
Design Tokens
Charts read their entire palette from WireKit's CSS variables — no hex codes are passed in JS. Override these tokens at :root {} (or inside a .dark { … } block) and every mounted chart picks up the change automatically.
| Token | Used for |
|---|---|
--color-wk-accent |
First dataset color (primary series) |
--color-wk-success |
Positive / "good" series |
--color-wk-warning |
Warning series |
--color-wk-danger |
Negative / "bad" series |
--color-wk-info |
Informational series (rare) |
--color-wk-text |
Legend label text |
--color-wk-text-muted |
Axis-tick labels |
--color-wk-border |
Grid line + axis line color |
--font-wk-sans |
Legend + axis-tick font family |
Dark mode works automatically — when the .dark class toggles on <html>, a MutationObserver re-reads all CSS variables and updates the chart instantly (grid lines, tick labels, legend text, and dataset colors). No page reload needed.