---
title: Chart
description: Chart component with Chart.js or ApexCharts adapter
visibility: guest
draft: false
related:
  - /components/charts-chartjs
  - /components/charts-apex
  - /dependencies
---

# 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](#license-apexcharts-only) below). Both use the same Blade tag; switching is a single config-line change.

::: warning
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](/components/charts-chartjs) — every chart family Chart.js ships natively, with realistic seed data
- [ApexCharts demo gallery](/components/charts-apex) — 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:

```bash
npm install chart.js
```

Import and register in your `app.js`:

```js
import { Chart, registerables } from 'chart.js';
Chart.register(...registerables);
```

### When using ApexCharts (`charts.library` => `'apexcharts'`)

Install ApexCharts via npm:

```bash
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`:

```js
import ApexCharts from 'apexcharts';
window.ApexCharts = ApexCharts;
```

Then switch the adapter on in `config/wirekit.php`:

```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.

::: tip 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:

```blade
{{-- 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](/dependencies)) registers the `wirekitApexChart` Alpine factory. It contains zero ApexCharts code — the JS library you installed via npm is what does the actual rendering.

::: warning 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](#license-apexcharts-only) for details.
:::

::: tip
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](/getting-started#javascript-bundle-full-vs-core) for setup details.
:::

## Setup

Enable charts in `config/wirekit.php`:

```php
'charts' => [
    'library' => 'chartjs',
],
```

Or `'apexcharts'`:

```php
'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:

```blade
<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:

```blade
<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:

```blade
<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`).

:::preview{title="Latency tooltip rounded to 2 dp with a unit suffix"}
<x-wirekit-chart
    library="apexcharts"
    type="line"
    valueDecimals="2"
    valueSuffix=" ms"
    :labels="['10:00','10:01','10:02','10:03','10:04','10:05']"
    :datasets="[['label' => 'p95 latency', 'data' => [50.523626895740676, 61.20891, 47.8881241, 72.4410067, 58.91002, 65.337781]]]"
    aria-label="Line chart of p95 latency over six minutes; hover a point to see the value rounded to two decimal places with a millisecond suffix"
/>
:::

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](#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.

:::preview{title="Monthly revenue — canonical bar-chart API"}
<x-wirekit-chart
    type="bar"
    :labels="['Jan','Feb','Mar','Apr','May','Jun']"
    :datasets="[['label' => 'Revenue (€K)', 'data' => [42, 58, 71, 89, 104, 124]]]"
    aria-label="Bar chart showing monthly revenue for January through June, ranging from 42K to 124K euro"
/>
:::

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](/components/charts-chartjs) and [ApexCharts Demos](/components/charts-apex) 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](/components/charts-chartjs/bar) | [Bar](/components/charts-apex/bar) / [Column](/components/charts-apex/column) | Chart.js renders vertical by default; ApexCharts splits horizontal (`bar`) and vertical (`column`) into separate types |
| `line` | [Line](/components/charts-chartjs/line) | [Line](/components/charts-apex/line) | Smooth curves on both adapters; tension/curve configurable via `options` |
| `area` | [Area](/components/charts-chartjs/area) | [Area](/components/charts-apex/area) | Filled-line variant. Chart.js uses `fill: true` on the dataset; ApexCharts uses `chart.type: 'area'` |
| `pie` | [Advanced](/components/charts-chartjs/advanced) | [Pie / Donut](/components/charts-apex/pie-donut) | Both adapters expect a single dataset with `data` as a flat numeric array + top-level `labels` per slice |
| `doughnut` | [Advanced](/components/charts-chartjs/advanced) | [Pie / Donut](/components/charts-apex/pie-donut) | Same data shape as `pie`; ring-shape variant |
| `radar` | [Advanced](/components/charts-chartjs/advanced) | [Radar](/components/charts-apex/radar) | Polygon chart for multi-axis comparison |
| `scatter` | [Advanced](/components/charts-chartjs/advanced) | [Scatter / Bubble](/components/charts-apex/scatter-bubble) | XY data points — pass `data: [{x, y}, …]` |
| `bubble` | [Advanced](/components/charts-chartjs/advanced) | [Scatter / Bubble](/components/charts-apex/scatter-bubble) | Scatter + per-point size — pass `data: [{x, y, r}, …]` |
| `mixed` | [Advanced](/components/charts-chartjs/advanced) | [Mixed](/components/charts-apex/mixed) | See dedicated [`<x-wirekit::chart-mixed>`](/components/chart-mixed) wrapper for per-dataset type bindings |
| `sparkline` | (see component) | [Sparklines](/components/charts-apex/sparklines) | See dedicated [`<x-wirekit::sparkline>`](/components/sparkline) wrapper for inline KPI strips |
| `heatmap` | — | [Heatmap](/components/charts-apex/heatmap) | ApexCharts-only |
| `treemap` | — | [Treemap](/components/charts-apex/treemap) | ApexCharts-only |
| `boxplot` | — | [Boxplot](/components/charts-apex/boxplot) | ApexCharts-only |
| `candlestick` | — | [Candlestick](/components/charts-apex/candlestick) | ApexCharts-only |
| `range-bar` | — | [Range Bar](/components/charts-apex/range-bar) | ApexCharts-only |
| `radial-bar` | — | [Radial Bar](/components/charts-apex/radial-bar) | ApexCharts-only |
| `funnel` | — | [Funnel](/components/charts-apex/funnel) | ApexCharts-only |
| `timeline` | — | [Timeline](/components/charts-apex/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](#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

```php
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

```php
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

```php
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

```blade
{{-- 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.

:::preview{title="Revenue vs Costs — bar chart from monthly_stats"}
<x-wirekit-chart
    type="bar"
    :labels="['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']"
    :datasets="[
        ['label' => 'Revenue (EUR)', 'data' => [42000, 58000, 71000, 89000, 104000, 124000, 138000, 152000, 141000, 128000, 144000, 168000]],
        ['label' => 'Costs (EUR)', 'data' => [31000, 38000, 44000, 52000, 61000, 69000, 74000, 81000, 79000, 73000, 80000, 92000]],
    ]"
    aria-label="Bar chart comparing monthly revenue and costs for a representative year"
/>
:::

:::preview{title="Visitor trend — line chart from monthly_stats"}
<x-wirekit-chart
    type="line"
    :labels="['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']"
    :datasets="[['label' => 'Visitors', 'data' => [4200, 5800, 7100, 8900, 10400, 12400, 13800, 15200, 14100, 12800, 14400, 16800]]]"
    aria-label="Line chart showing monthly unique visitor counts trending upward across a representative year"
/>
:::

:::preview{title="Device breakdown — doughnut chart summing all months"}
<x-wirekit-chart
    type="doughnut"
    :labels="['Desktop', 'Mobile', 'Tablet']"
    :datasets="[['data' => [78600, 51300, 9300]]]"
    height="300px"
    aria-label="Doughnut chart showing visitor share across desktop, mobile, and tablet devices"
/>
:::

## Custom Options

Pass native Chart.js options to override defaults:

```blade
<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:

```blade
<x-wirekit-chart
    type="bar"
    :labels="$months"
    :datasets="[
        [
            'label' => 'Revenue',
            'data' => $values,
            'backgroundColor' => 'rgba(59, 130, 246, 0.5)',
            'borderColor' => '#3b82f6',
        ],
    ]"
/>
```

::: tip
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.

::: info
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`:

```php
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:

```php
'charts' => [
    'library' => \App\Charts\ApexChartsAdapter::class,
],
```

## Production

Cache icons for Blade Icons (if using icon components alongside charts):

```bash
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](https://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.js` in any `dist/` artifact.
- WireKit does NOT include ApexCharts source in `composer.json` or `package.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`

```php
'charts' => [
    'library' => 'apexcharts',
    'apex_license' => 'commercial', // 'community' | 'commercial' | 'oem'
],
```

`apex_license` is informational — WireKit gates nothing on its value. It serves two purposes:

1. **Audit trail** — a single source of truth records which tier you've adopted alongside the rest of your config.
2. **Doctor reminder** — `php artisan wirekit:doctor` emits a WARN-level reminder pointing at the ApexCharts license terms when `apex_license` is `'community'` or unset. The reminder is suppressed for `'commercial'` and `'oem'`.

### Doctor output examples

When `apex_license` is `'community'` or unset:

```text
[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'`:

```text
[PASS] charts: ApexCharts adapter active. License tier: commercial.
```

### Pitfalls

- **Don't bundle `apexcharts.min.js` into 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](https://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):** `RuntimeException` with config instructions.
- **Chart.js not installed:** `console.error` in browser with npm install instructions.
- **ApexCharts not installed (adapter bundle loaded):** `console.error` PLUS a visible in-DOM advisory panel inside the chart container — surfaces the install command, the license reminder, and a link to `apexcharts.com/license` so the developer can act without opening DevTools first.
- **Unknown adapter:** `InvalidArgumentException` listing 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.

::: warning
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`:

```blade
<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:

```blade
<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](https://github.com/ashiguruma/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

- [WAI — Complex Images Tutorial](https://www.w3.org/WAI/tutorials/images/complex/)
- [WebAIM — Accessible Charts](https://webaim.org/techniques/images/)

## 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.live` to 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.
