Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Apex Charts Laravel Package

leandrocfe/filament-apex-charts

ApexCharts integration for Filament with an artisan widget generator and support for area, bar, line, pie, donut, radar, heatmap, treemap, funnel, and more chart types for dashboard panels.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Match the package major to your Filament major. This is the first thing to look at, and the one most likely to burn an afternoon:

Filament Package branch
v2.x 2.0.2
v3.x 3.2.1
v4.x 4.0.0
current ^5.0
composer require leandrocfe/filament-apex-charts:"^5.0"

2. Register the plugin in the panel provider(s) where charts should render — you can enable it on some panels and not others:

use Leandrocfe\FilamentApexCharts\FilamentApexChartsPlugin;

public function panel(Panel $panel): Panel
{
    return $panel->plugins([
        FilamentApexChartsPlugin::make(),
    ]);
}

3. Scaffold a widget. Don't hand-write the class first; the generator gives you a working, correctly-shaped example of any of the 20 chart types (Area, Bar, Boxplot, Bubble, Candlestick, Column, Donut, Heatmap, Line, Mixed-LineAndColumn, Pie, PolarArea, Radar, Radialbar, RangeArea, Scatter, TimelineRangeBars, Treemap, Funnel, or Empty):

php artisan make:filament-apex-charts BlogPostsChart

It lands in app/Filament/Widgets/BlogPostsChart.php. The generated file is your best documentation — it shows the exact array shape for that chart type, which is more useful than reading ApexCharts docs cold.

4. Where to look first. The package is deliberately thin: getOptions() returns a raw ApexCharts options array, passed through to the JS library nearly verbatim. So the authoritative reference for 95% of your work is apexcharts.com/docs/options, not the README. The README covers the Filament-side surface only (heading, filters, polling, defer, height, footer, dark mode).

5. First real use case. The common win is replacing a stock ChartWidget with something Filament's built-in charts can't do — a mixed line-and-column trend, a radial KPI gauge, a heatmap of activity by hour/day, or a funnel. Second most common: adding a filter bar (date range, tenant, segment) above a dashboard chart via HasFiltersSchema.

Keep the demo repo open in a tab early on; it answers "how do I make this look like the screenshot" faster than assembling options by trial and error.

Implementation Patterns

Treat getOptions() as a view-model

The method is called on render and on updateOptions(). Keep it declarative: query, shape, return. No side effects, no echo/dump, no mutation of state.

protected function getOptions(): array
{
    $data = Order::query()
        ->whereBetween('created_at', [$start, $end])
        ->selectRaw('DATE(created_at) as d, SUM(total) as revenue')
        ->groupBy('d')
        ->orderBy('d')
        ->pluck('revenue', 'd');

    return [
        'chart' => ['type' => ApexChartTypeEnum::Area, 'height' => 300],
        'series' => [['name' => 'Revenue', 'data' => $data->values()->all()]],
        'xaxis' => ['categories' => $data->keys()->all()],
    ];
}

Aggregate in SQL, not PHP. Charts are usually the heaviest thing on a dashboard, and a 5-second polling interval turns a sloppy query into a steady background load.

Filtering: two distinct mechanisms

Preset period filter — cheapest option, uses Filament's built-in filter dropdown. Set public ?string $filter and define getFilters():

public ?string $filter = 'month';

protected function getFilters(): ?array
{
    return [
        'today' => 'Today',
        'week'  => 'Last week',
        'month' => 'Last month',
        'year'  => 'This year',
    ];
}

Then branch on $this->filter inside getOptions().

Arbitrary filter form — when you need date pickers, tenant selects, or free text. Use HasFiltersSchema, and critically wire the refresh yourself:

use Filament\Widgets\ChartWidget\Concerns\HasFiltersSchema;
use Filament\Forms\Components\DatePicker;
use Filament\Schemas\Schema;

class BlogPostsChart extends ApexChartWidget
{
    use HasFiltersSchema;

    public function filtersSchema(Schema $schema): Schema
    {
        return $schema->components([
            DatePicker::make('date_start')->default(now()->startOfMonth()),
            DatePicker::make('date_end')->default(now()->endOfMonth()),
        ]);
    }

    public function updatedInteractsWithSchemas(string $statePath): void
    {
        $this->updateOptions();
    }
}

Values arrive in $this->filters — note that's an array, so $this->filters['date_start'], not a property. Forget that and you'll get null in your query and a suspiciously empty chart.

Slow queries: defer + polling

protected static bool $deferLoading = true;
protected static ?string $pollingInterval = null; // stop the 5s auto-refresh

protected function getOptions(): array
{
    if (! $this->readyToLoad) {
        return []; // renders the loading indicator
    }

    // heavy aggregate
    return [...];
}

$deferLoading releases the page paint before the query runs; readyToLoad tells you whether you're in the deferred pass. Pair this with Cache::remember() keyed on the filter state for anything over a few hundred milliseconds.

JS-only options: extraJsOptions + RawJs

Anything ApexCharts expresses as a JS closure — axis label formatters, tooltip formatters, dataLabels.formatter — cannot survive json_encode. That's what extraJsOptions() is for:

use Filament\Support\RawJs;

protected function extraJsOptions(): ?RawJs
{
    return RawJs::make(<<<'JS'
    {
        yaxis: { labels: { formatter: (val) => '$' + val.toLocaleString() } },
        tooltip: { y: { formatter: (val) => '$' + val.toFixed(2) } }
    }
    JS);
}

Use it for formatters only, and keep the merge shallow — these are appended after the PHP options, so overlapping keys can produce confusing half-overridden config. Verify in the browser, not by reading the array.

Presentation surface

protected static ?string $heading    = 'Revenue';
protected static ?string $subheading = 'Last 30 days';
protected static ?string $footer     = 'Excludes refunds.';
protected static ?int    $contentHeight = 300;
protected static bool    $isCollapsible = true;
protected static ?string $chartId    = 'revenueChart';

getFooter() accepts string|Htmlable|View, so a blade partial is the natural way to render a "last updated" or "view report" link beneath the chart. Every static property has a method equivalent (getHeading(), getContentHeight(), getFooter(), isCollapsible()) for dynamic values.

Dashboard composition

Put widgets on a panel dashboard via getWidgets(), or on a resource page with getHeaderWidgets() / getFooterWidgets(). For a multi-chart dashboard, standardise on a project base class:

abstract class BaseChart extends ApexChartWidget
{
    protected function getOptions(): array
    {
        return array_replace_recursive([
            'chart' => ['fontFamily' => 'inherit', 'height' => 300],
            'xaxis' => ['labels' => ['style' => ['fontFamily' => 'inherit', 'fontWeight' => 600]]],
            'yaxis' => ['labels' => ['style' => ['fontFamily' => 'inherit']]],
            'colors' => ['#f59e0b'],
        ], $this->chartOptions());
    }

    abstract protected function chartOptions(): array;
}

Subclasses implement chartOptions() only. This keeps spacing, fonts, and palette consistent and gives you one place to change branding.

Gotchas and Tips

Empty getOptions() means two different things. [] renders "No chart data available" and is the defer-loading placeholder. If you show a user-facing "no results for this date range", you can't distinguish it from a chart still loading. Consider returning ['series' => []] explicitly with an empty-state message in the heading/subheading, or only use defer when loading state is unambiguous.

The 5-second polling default is a real cost. Every chart widget re-queries on that interval. Five KPI charts on a dashboard = five queries every 5s per open tab. On a finance dashboard with three joins and a GROUP BY this is how your DB dashboard gets sad. Set protected static ?string $pollingInterval = null; on anything expensive, and only poll charts where staleness actually matters.

PHP closures cannot go in the options array. Symptom: the chart renders axes but no labels/tooltips, or the whole chart silently fails. Fix: move formatter logic into extraJsOptions().

Non-sequential array keys break ApexCharts. json_encode turns [0 => 1, 2 => 3] into an object, and ApexCharts expects an array for series[].data. Any time you filter(), pluck() from a keyed collection, or array_filter() before assigning to data, call ->values()->all():

'data' => $rows->filter()->values()->all(),   // not ->all() alone

Numeric types matter. MySQL PDO may return DECIMAL as a string; SQLite is loose about it. 'data' => ['1200.00', '980.50'] renders but formatters concatenate instead of adding ('$' + '1200.00' gives oddities), and tooltips look wrong. Cast explicitly — ->map(fn ($v) => (float) $v) or a selectRaw('CAST(... AS REAL)').

Chart IDs must be unique per page. protected static ?string $chartId is what the JS uses to mount. Copy-pasting a widget on the same dashboard gives you one chart rendering and the other blank. Convention: derive it from the class name and the filter context if you ever render two instances.

Hardcoding theme.mode kills dark mode. Dark mode is supported and enabled by default. Setting 'theme' => ['mode' => 'light'] in getOptions() opts that chart out of the panel's theme toggle permanently, and it looks broken next to its neighbours. Only set it deliberately, and prefer CSS-variable-driven colors so one palette works in both modes.

After changing data, call updateOptions(). The package caches options (per changelog 0.1.4). If you mutate state outside the filter flow, the rendered chart won't reflect it until the next call. Same applies to any custom action button you add — $this->updateOptions() inside the handler.

Version drift is the #1 install failure. The HasFiltersSchema trait paths and Schema API in the README are contemporary Filament (v4/v5 lineage). Pinning ^5.0 on a Filament v3 app gives you a class-not-found on the trait and a plugin registration that no-ops. Pin both sides in composer.json and read the branch matching your Filament version if anything doesn't resolve.

Height is set in two places. $contentHeight controls the widget wrapper; chart.height controls the canvas. A mismatch produces a clipped chart or dead whitespace. Set both to the same number, or omit chart.height entirely and let the widget drive.

Publish views when you need structural changes. php artisan vendor:publish --tag="filament-apex-charts-views" exposes the wrapper markup — useful for adding cards, badges, or a "refresh" control around the chart. Publishing translations (filament-apex-charts-translations) is what you want for localising "No chart data available" and the loading indicator.

Debugging checklist when a chart renders blank:

  1. Browser console — any Apex error (bad option shape, formatter throwing) appears here; the PHP side looks fine.
  2. dd($this->getOptions()) temporarily, or log it — check the array is what you think, especially series[0].data being a list.
  3. Check chart.type matches your series shape (e.g. Mixed-LineAndColumn needs per-series type keys).
  4. Confirm the widget is registered on the panel and $chartId is unique.

Testing. The chart is client-side, so assert what you can in PHP: subclass the widget in a test to expose getOptions() as public, then assert the series array, category count, and that filter state produces the expected query scope. Assert the Livewire component renders with a 200 and that the chart ID appears in the HTML. Don't try to test pixel output.

Extension points worth knowing: custom base class (above), getOptions() override at the base for shared theme, custom footer/loading-Indicator blade views, and extraJsOptions() as the escape hatch for anything the PHP array can't express. That's the whole surface — this package is intentionally thin, which is a feature: everything you learn transfers directly to ApexCharts docs and examples.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core