Skip to main content
Copy for LLM

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 config/wirekit.php before mounting 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, 6 and 7, verified in a real browser against 5.16.0, 6.10.0 and 7.1.0 — the same chart suite run once per major. 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.

php artisan wirekit:verify names the version you have and says whether it falls inside that range, so you do not have to come back to this page to find out. A version outside it is a warning rather than an error — it may well work, nobody has measured it yet, and a chart that breaks on a new major breaks silently: the page renders, the markup is correct, and only a browser shows the empty box. If you run one, we would like to hear how it went.

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') }}" defer></script>
{{-- 2. The ApexCharts glue, which registers itself against it — so it comes second. --}}
<script src="{{ asset('vendor/wirekit/wirekit-apex.js') }}" defer></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 must have the @wirekitScripts directive in your layout — without it the canvas mounts and nothing draws on it. 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).

Latency tooltip rounded to 2 dp with a unit suffix

Hover any point: the tooltip reads 58.91 ms instead of 58.910020000000001.

A tooltip.y.formatter or a yaxis.labels.formatter set for every chart through ApexCharts' global options (window.Apex) writes the values of the ApexCharts tooltip, and a tooltip.y.title.formatter the name in front of each value, as they do in ApexCharts; what they return is written as text. valueDecimals, valuePrefix and valueSuffix belong to the chart and come first: where one of them is set, the global formatter does not write that chart's values. The tooltip also reads tooltip.enabledOnSeries, which series it lists at all, tooltip.inverseOrder, which lists them from the last, and tooltip.hideEmptySeries, which gives a value of zero no row, from the chart's options:

{{-- 1. A shared tooltip that lists the second series only, and leaves out a value of zero. --}}
<x-wirekit-chart library="apexcharts" type="line" :labels="$days" :datasets="$series"
    :options="['tooltip' => ['shared' => true, 'enabledOnSeries' => [1], 'hideEmptySeries' => true]]" />

Numbers and dates follow your application's locale (app()->getLocale()), not the reader's browser, like the other WireKit components that format on the client: Chart.js receives it as its locale option, and an ApexCharts chart uses it for the dates in its tooltip, for the values valueDecimals rounds and for its decimal axis ticks, so a German page reads 58,91 ms. Set locale in options to choose another one for a single Chart.js chart.

The ApexCharts tooltip also names the rows a point brings without a series name: Open price, High, Low and Close price for a candlestick, Max, Median and Min for a boxplot, with the quartile symbols Q1 and Q3, and Size for a bubble. These names come from WireKit's translation catalog, so they follow the same locale.

On a date axis ('xaxis' => ['type' => 'datetime']), the ApexCharts tooltip titles each point with its date, whether the point gives x as a timestamp, as a date string or as the first value of a pair, and a timeline writes the two ends of each range the same way. Both follow tooltip.x.format, which is ApexCharts' own dd MMM until you set one such as 'MMM dd, yyyy HH:mm', with month and weekday names in your application's locale. They are written in UTC, as the axis is labeled, unless xaxis.labels.datetimeUTC is false, which writes the axis and the tooltip in the reader's time zone. A tooltip.x.formatter or an xaxis.labels.formatter set for every chart through ApexCharts' global options (window.Apex) titles the point in place of the format, as it does in ApexCharts, and what it returns is written as text. The ends of a range column or a range area are values rather than dates, so the tooltip writes them as values.

A further value in the tooltip

A tooltip can name a value of the point that the chart does not draw, such as the number of receipts of a day beside its revenue, where a series of its own would take an axis and an entry in the legend. tooltipExtras gives a label and one value per point, in the order of labels, and the tooltip names the value of the point it shows under the series' values:

The receipts of a day in the tooltip

Pass a list of them for more than one. Each extra writes its values in your application's locale and takes its own decimals, prefix and suffix, so a count of receipts takes no currency sign from the chart's valuePrefix. A point whose value is null has no row. The values follow a Livewire update with the rest of the data, and the tooltip names them under Chart.js and ApexCharts alike.

Linking data points

A point can lead somewhere: the report behind a day's revenue, the list behind a category. Give a dataset urls, one address per point in the order of its data, with null for a point that leads nowhere. A point written as an object, the way a treemap or a bubble chart writes its points, takes its own url instead, and where both name one the point's own wins.

Bars that lead into their reports

A click or a tap on a point follows its address the way a link does, and over a point with an address the pointer becomes a hand:

  • An address on your site goes through Livewire's navigation where the page runs Livewire, as a wire:navigate link does. Another site, or a mailto: address, goes through the browser.
  • Ctrl, Cmd or Shift with the click, or the middle button, opens it in a new tab.
  • An address that is only a fragment, #revenue-mon, jumps within the page.
  • An address whose scheme could run script, such as javascript:, is dropped, so a point from your data never runs code in the page.

With ApexCharts a click follows the point its tooltip shows at that moment. A point a stream adds through wireStream carries no address.

A treemap whose cells lead into their lists

A point drawn on a canvas, or inside the chart's image, cannot take the focus. So the chart lists the links of its points after itself, each named by what it shows: its label and value, as in "Mon: €1,240", led by the dataset's name where the chart has more than one. The list is out of sight until Tab reaches a link in it, and then stands under the chart until the focus leaves it. A screen reader reads it after the chart's name. valueDecimals, valuePrefix and valueSuffix write the values in the list under either library.

A point written as a pair is named the way its library draws it. [x, y] is named by its x and its value. The pair of a Chart.js bar is a floating bar, named by its two ends, as in "Q1: 3 – 7". On a date axis, an ApexCharts datetime axis or a Chart.js time scale, a point is named by its date, written the way the tooltip writes one without a format of your own, as in "Jan 01, 2026: 3". A tooltip.x.format of your own changes the tooltip, not the list.

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.

Monthly revenue — canonical bar-chart API

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 Vertical on both adapters. ApexCharts additionally accepts column, which maps onto bar and renders identically; rotate the bars there with plotOptions.bar.horizontal
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 / Timeline ApexCharts-only. The Timeline demo is this type with plotOptions.bar.horizontal — there is no timeline type of its own
radial-bar — Radial Bar ApexCharts-only
funnel — Funnel ApexCharts-only

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.

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']);
});
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();
    }
}
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'),
            ],
        ]);
    }
}
{{-- 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.

Revenue vs Costs — bar chart from monthly_stats
Visitor trend — line chart from monthly_stats
Device breakdown — doughnut chart summing all months

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.

With ApexCharts, the labels, the dataset names and the goals of a bar (their name, value and color) reach the legend and the tooltip as text, so a name from your data that holds < or & shows those characters, and markup in it never runs. ApexCharts writes what a formatter returns as HTML. A legend or tooltip formatter you set for every chart through ApexCharts' global options (window.Apex) stays in place, and it has to escape any value from your data it returns.

Colors

A chart draws its series in one of two palettes, chosen with palette:

  • semantic, the default: the accent, then the danger, success, warning and info colors, then violet, pink and orange. These colors carry meaning elsewhere in WireKit, so a second series reads as "danger" and a third as "success". Use this palette where that meaning is intended, such as profit against loss.
  • categorical: eight pastels from the --color-wk-chart-* tokens, for series that are simply different things, such as sales channels, regions or products. Every pair stays apart for normal vision and under simulated protanopia, deuteranopia and tritanopia, in light and in dark mode.
Three sales channels in the categorical palette

Which series takes which color

Each series of the categorical palette has a fill and an edge of the same hue. The edge carries the data: it draws a line and its points, and it outlines a bar, a slice and a segment. It stands at 3:1 or more on the page in both modes, so the shape holds the contrast WCAG 1.4.11 asks of a graphic even where the pastel fill does not. The fill colors bars, slices and segments, and the area under a line.

Series Fill Edge Hue
1 --color-wk-chart-1 --color-wk-chart-1-edge blue
2 --color-wk-chart-2 --color-wk-chart-2-edge yellow
3 --color-wk-chart-3 --color-wk-chart-3-edge sky
4 --color-wk-chart-4 --color-wk-chart-4-edge coral
5 --color-wk-chart-5 --color-wk-chart-5-edge sand
6 --color-wk-chart-6 --color-wk-chart-6-edge mint
7 --color-wk-chart-7 --color-wk-chart-7-edge lavender
8 --color-wk-chart-8 --color-wk-chart-8-edge orchid

The series take the colors in the order of their datasets, and a ninth series starts over at the first. The order keeps the first colors far apart for a chart of few series: the first two and the first four are the most distinct sets of their size. To give one thing the same color in every chart, a channel or a region, keep it at the same position in every chart's datasets.

A label drawn on a fill, the value on an ApexCharts bar, slice or heatmap cell, takes --color-wk-chart-label, which reads at 4.5:1 or more on every fill in both modes. A label beside a line stays in the text color.

Setting the palette for every chart

// config/wirekit.php
'components' => [
    'chart' => ['palette' => 'categorical'],
],

A chart's own palette still wins over the config. <x-wirekit::chart-mixed> takes the prop as well. A dataset with its own backgroundColor or borderColor (Chart.js) or color (ApexCharts) keeps it in either palette, see Manual Dataset Colors.

Sequential and diverging scales

Two further sets of tokens color a value rather than a series. --color-wk-chart-seq-1 to --color-wk-chart-seq-7 run from faint to strong in the blue of the first series, for a quantity such as activity or load. --color-wk-chart-div-1 to --color-wk-chart-div-7 run from blue through a neutral middle to orange, for a deviation from a target or an average. Step 1 is the faintest beside the page in both modes, and neighboring steps stay apart under the same three deficiencies.

No chart takes them by itself. Name them with var() where a chart asks for a scale, such as the ranges of an ApexCharts heatmap's color scale, and the adapter resolves them for the current mode. The heatmap page shows one on the sequential scale.

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 chart root carries wire:ignore, so Livewire's DOM morphing never tears down a drawn chart, and charts are cleaned up during wire:navigate page transitions.

Filtering and re-rendering

When your Livewire component renders new labels or datasets, the chart follows them in place: the labels and series change, the chart stays the same instance, and it animates from the old values to the new ones rather than being built again. An aria-label that describes the data follows with it. A filter above a chart needs nothing extra:

// 1. The filter the reader changes
public string $period = 'week';

public function render(): View
{
    // 2. Recompute the data for the chosen period on every render
    [$labels, $orders] = $this->ordersFor($this->period);

    return view('livewire.reports.orders', [
        'labels' => $labels,
        'datasets' => [['label' => 'Orders', 'data' => $orders]],
    ]);
}
<x-wirekit::select label="Period" wire:model.live="period" :options="['week' => 'This week', 'month' => 'This month']" />

<x-wirekit-chart type="bar" :labels="$labels" :datasets="$datasets" aria-label="Orders by period" />

The chart's type and options stay as it first rendered them. A different type is a different chart: give it a wire:key that changes with the type.

Streaming is the other direction: a chart subscribes to a Livewire-dispatched event and appends points as they arrive, with a FIFO cap so a long-running page cannot grow without bound. The attributes and the two modes are documented under wire:streaming live updates. Use it for a feed that adds points; use the re-render above when the whole series changes.

Loading the chart library on demand

Neither Chart.js nor ApexCharts is bundled by WireKit, so both sit in your own entry chunk — and on a site where most pages carry no chart at all, that is weight every visitor downloads to look at a page without one. The usual answer is to import the library only once a chart actually appears, and for that you need one thing from the markup: a signal that says a chart renders here.

Every rendered chart carries it:

<div data-wk-chart="chartjs" x-data="wirekitChart({…})" role="img" aria-label="…">

The value is the identifier of the adapter that rendered this chart — chartjs, apexcharts, or the slug your own adapter returns from name(). It follows the library prop rather than the app default, so a page whose charts use different libraries tells you which is which:

// Import the library only when a chart that needs it is on the page.
const needsApex = document.querySelector('[data-wk-chart="apexcharts"]');

if (needsApex) {
    const { default: ApexCharts } = await import('apexcharts');
    window.ApexCharts = ApexCharts;
}

A chart that rendered before the import resolved waits for it: it shows nothing until the library is on window, and builds itself the moment it is — no re-initialization on your side. Only a library that is still missing a few seconds after the page has loaded gets the console hint and the advisory panel described under Behavior, and if it arrives even later, the chart replaces the panel.

Three properties of the marker are worth relying on:

  • One per chart. The replayable form wraps the chart in a second element, and chart-mixed renders through this same component — neither adds a second marker, so counting [data-wk-chart] counts charts.
  • The value is stable. It is the adapter's library identifier, the same string you write in config/wirekit.php, and it does not change within a major version. Do not key on the Alpine factory's name instead: that is an internal detail, and renaming it would not be a breaking change, so a selector built on it would quietly stop matching.
  • The debug placeholder does not carry it. When no adapter is configured and APP_DEBUG is on, the component renders a placeholder instead of a chart. No chart is there, so no library should be fetched for it — and a loader keyed on this marker will correctly ignore it.

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

'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 reports the value back to you. Which severity it reports it at depends on the value, and there are three outcomes rather than two:
apex_license wirekit:doctor reports Under --fail-on=warning
'commercial' or 'oem' a pass, naming the tier back green
'community' an info line — the reminder still prints on every run green
unset, or a value that is none of the three a warning red until a recognized tier is recorded

The split between the middle row and the last one is what keeps --fail-on=warning usable as a build gate, and it is deliberate. The community line is true on every run of a correctly configured install and will never stop being true, so counted as a warning it would hold the exit code at 1 forever — and a check that is permanently red is one a team learns to pass with || true, taking the config drift the flag exists to catch with it. An undeclared tier is the opposite shape: recording one is a single entry in config/wirekit.php, after which the warning is gone for good and a 'community' value settles into the info line. The same pair is documented from the flag's side in the CLI Reference, under wirekit:verify.

Doctor output examples

These are the lines wirekit:doctor prints, one per outcome. The marker in front of each is the severity: ✓ for a pass, i for info, ! for a warning.

'commercial' or 'oem' — the tier is named back and nothing else is said:

  ✓ ApexCharts license tier declared: commercial

'community' — info, so it does not move the exit code:

  i Declared tier: community. This reminder stays — the $2M USD revenue threshold for the ApexCharts Community License is a continuing condition, not an install step. Purchase a Commercial License at https://apexcharts.com/license/ once you pass it.

Unset — a warning, and the one line that tells you which entry to make:

  ! 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/. Record your tier via `charts.apex_license` in config/wirekit.php (values: community / commercial / oem).

Recorded but unrecognized — a warning that names the value back, so a typo is visible without opening the config. It counts as undeclared until it matches one of the three:

  ! Unrecognized ApexCharts license tier `comercial` in `charts.apex_license`. Accepted values: community / commercial / oem. Until it matches one of those it is treated as undeclared.

Licensing 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 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 the browser with the npm install instructions, PLUS a visible advisory panel where the chart would have been.
  • 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.
  • Either library loaded after the chart mounted: nothing at all. Both adapters keep looking, and the hint and the panel above wait until the page has loaded and a few more seconds have passed — a library loaded on demand is picked up instead.
  • 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 still missing a few seconds after the page has loaded.

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. A dataset without a label is named "Series 1", "Series 2" and so on in the application's language. The single dataset of a pie, doughnut or polarArea chart stays unnamed, because its slices carry the names. urls, one address per point, and url on a point object make points links, see Linking data points
options array [] Native Chart.js options (merged with defaults)
height string '380px' Container height
wireStream ?string null Livewire event name to subscribe to for real-time appending. When set, the Alpine factory listens for the event and pipes each incoming point payload into the chart through the library's imperative API — Chart.js pushes onto data.datasets[i].data, ApexCharts calls appendData(). null renders a normal chart with no streaming behavior. Setting it also auto-enables replayable. Worked examples: Streaming
wireStreamMode string 'strict' 'strict' caps each dataset at wireStreamCap points and drops the oldest first (FIFO). 'stream' lets the series grow without a bound — that is the developer's memory to manage, and a page left open overnight is the case to think about.
wireStreamCap int 100 Maximum data points per dataset under 'strict' mode. Ignored when wireStreamMode is 'stream'.
annotations array [] Annotation specs threaded into the chart options. ApexCharts maps them onto options.annotations. Chart.js needs chartjs-plugin-annotation installed; without it the chart still renders and the annotations are skipped with a console.warn rather than an error, so a missing plugin looks like annotations that silently do nothing. Worked examples: Annotations
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, and in the cells of a treemap that sets dataLabels.wkShowValue, 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, and to a treemap cell's value (e.g. "€", "$").
valueSuffix ?string null ApexCharts only. Text appended to each formatted tooltip value, and to a treemap cell's value (e.g. " ms", "%"). Combine with valueDecimals for "50.52 ms".
tooltipExtras array [] Further values the tooltip names for each point without a series of their own: ['label' => 'Receipts', 'data' => [48, 37]], or a list of them, each with an optional decimals, prefix and suffix. See A further value in the tooltip
palette ?string null The colors the series take: semantic or categorical, see Colors. null takes config('wirekit.components.chart.palette'), which ships as semantic.

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

Pass a descriptive aria-label to the chart itself. The component already renders role="img" on its root, so the label lands on the element that carries the role:

<x-wirekit-chart
    type="line"
    :labels="$months"
    :datasets="$datasets"
    aria-label="Monthly revenue chart, January to December 2025. Peaks at €42,000 in December."
/>

Do not wrap the chart in a second role="img" container. Two nested images are one image to a screen reader: the outer element wins, and the chart's own role and name are dropped from the accessibility tree.

Without an aria-label the chart falls back to the single word "Chart", which tells a reader that something graphical is here and nothing about the data in it. With APP_DEBUG=true the component writes a log line pointing at the chart that needs one.

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)

The categorical palette keeps every pair of series apart under the three common color vision deficiencies. That helps a reader tell the series apart, and it is no substitute for a second cue: a legend, a label at the end of each line, or a pattern.

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

A chart responds to no key itself. A chart whose points lead somewhere lists their links after itself, and Tab reaches each of them; the list shows under the chart while one of them has the focus, see Linking data points.

ApexCharts 7 brings a keyboard navigation of its own over the data points. The component leaves it switched off: the drawing is hidden from assistive technology, and a tab stop inside it would take the keyboard where a screen reader is told not to go. The data stays reachable through the text alternative described above.

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 give a chart a wire:key that changes with its data. A changing key replaces the chart and replays its entrance animation on every update. The chart follows new labels and datasets by itself, see Filtering and re-rendering.

Design Tokens

Charts read their palette from WireKit's CSS variables. 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
--color-wk-surface The surface every series is checked against: the page, or the card or panel that holds the chart
--color-wk-chart-1 to --color-wk-chart-8, each with an -edge The series of the categorical palette, fill and edge
--color-wk-chart-label A label on a fill in the categorical palette
--font-wk-sans Legend + axis-tick font family

The chart paints no background of its own: it shows the surface it stands on, the page or the card or panel that holds it.

In the semantic palette every series holds 3:1 against that surface, the contrast WCAG 1.4.11 asks of a graphic a reader needs to understand. Where a token is gray, or falls under that, the chart draws the series in its own shade of the same hue instead: a 600 shade in light mode, a 200 shade in dark mode. The token itself stays as it is for everything else that reads it. The sixth to eighth series have no token and use violet, pink and orange in the same two shades. The categorical palette takes its tokens as they are, as their edges hold the 3:1 themselves.

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.

A chart inside a .dark or .light subtree draws in that subtree's theme: the mode and every token come from the nearest scope, so a light panel on a dark page keeps a light chart. The chart reads its scope when it mounts and whenever the page's own theme changes; switching a subtree's class later does not repaint it.

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store