# Filament Apex Charts Filament Apex Charts is a Laravel package that integrates the [ApexCharts](https://apexcharts.com/) JavaScript library into [Filament](https://filamentphp.com/) admin panels as first-class Livewire widget components. It supports Filament v5 (and also v2–v4 via separate tags) and provides 19 chart types — including line, bar, area, pie, donut, heatmap, scatter, radialBar, candlestick, treemap, funnel, and more. The package is registered as a Filament plugin and publishes Alpine.js-powered chart components, Blade views, and language files into the host Laravel application. Every chart is created by extending the `ApexChartWidget` base class and implementing a single `getOptions(): array` method whose structure mirrors the [ApexCharts JavaScript options object](https://apexcharts.com/docs/options) exactly. Widget behaviour — polling intervals, deferred loading, dark mode, filter forms, loading indicators, collapsible headers, footers, and raw JavaScript formatter callbacks — are all controlled through properties and override-able methods on the same class, making chart widgets self-contained Livewire components that can be dropped into any Filament panel dashboard or resource page. --- ## Installation & Plugin Registration Install via Composer and register `FilamentApexChartsPlugin` on every Filament panel that should render charts. ```php // Terminal composer require leandrocfe/filament-apex-charts:"^5.0" // app/Providers/Filament/AdminPanelProvider.php use Leandrocfe\FilamentApexCharts\FilamentApexChartsPlugin; use Filament\Panel; public function panel(Panel $panel): Panel { return $panel ->plugins([ FilamentApexChartsPlugin::make(), ]); } ``` --- ## Artisan Generator — `make:filament-apex-charts` Scaffolds a new widget class pre-filled with a chosen chart type stub, resolved into the correct Filament panel widget directory. ```bash # Interactive (prompts for name, chart type, panel, and optional resource) php artisan make:filament-apex-charts BlogPostsChart # The command will ask: # 1. Chart name → BlogPostsChart # 2. Chart type → Line | Bar | Area | Pie | Donut | Column | Boxplot | # Bubble | Candlestick | Heatmap | Mixed-LineAndColumn | # PolarArea | Radar | Radialbar | RangeArea | Scatter | # TimelineRangeBars | Treemap | Funnel | Empty # 3. Destination panel → [admin] panel (or alongside Livewire components) # 4. Resource (optional) → BlogPostResource # Output: app/Filament/Widgets/BlogPostsChart.php ``` --- ## `ApexChartWidget` — Base Widget Class `Leandrocfe\FilamentApexCharts\Widgets\ApexChartWidget` is the abstract base all chart widgets extend. It wires together Livewire, Filament's polling, Alpine.js, and all the concern traits listed below. Override `getOptions()` to supply chart data; call `updateOptions()` from any Livewire action to push new data to the browser without a full page reload. ```php namespace App\Filament\Widgets; use Leandrocfe\FilamentApexCharts\Widgets\ApexChartWidget; use Leandrocfe\FilamentApexCharts\Enums\ApexChartTypeEnum; class BlogPostsChart extends ApexChartWidget { // Unique DOM id for this chart instance protected static ?string $chartId = 'blogPostsChart'; // Displayed in the widget card header protected static ?string $heading = 'Blog Posts'; protected static ?string $subheading = 'Posts published per month'; protected function getOptions(): array { $data = \App\Models\Post::query() ->selectRaw('MONTH(created_at) as month, COUNT(*) as total') ->whereYear('created_at', now()->year) ->groupBy('month') ->pluck('total', 'month') ->toArray(); return [ 'chart' => [ 'type' => ApexChartTypeEnum::Bar, 'height' => 300, ], 'series' => [ ['name' => 'Posts', 'data' => array_values($data)], ], 'xaxis' => [ 'categories' => ['Jan','Feb','Mar','Apr','May','Jun', 'Jul','Aug','Sep','Oct','Nov','Dec'], ], 'colors' => ['#6366f1'], ]; } } ``` --- ## `ApexChartTypeEnum` A backed string enum that maps friendly PHP names to the string values required by the ApexCharts `chart.type` option, preventing typos and enabling IDE autocompletion. ```php use Leandrocfe\FilamentApexCharts\Enums\ApexChartTypeEnum; // All available cases: ApexChartTypeEnum::Area // 'area' ApexChartTypeEnum::Bar // 'bar' ApexChartTypeEnum::Boxplot // 'boxPlot' ApexChartTypeEnum::Bubble // 'bubble' ApexChartTypeEnum::Candlestick // 'candlestick' ApexChartTypeEnum::Column // 'column' ApexChartTypeEnum::Donut // 'donut' ApexChartTypeEnum::Funnel // 'funnel' ApexChartTypeEnum::Heatmap // 'heatmap' ApexChartTypeEnum::Line // 'line' ApexChartTypeEnum::Pie // 'pie' ApexChartTypeEnum::PolarArea // 'polarArea' ApexChartTypeEnum::Radar // 'radar' ApexChartTypeEnum::Radialbar // 'radialBar' ApexChartTypeEnum::RangeArea // 'rangeArea' ApexChartTypeEnum::Scatter // 'scatter' ApexChartTypeEnum::TimelineRangeBars // 'rangeBar' ApexChartTypeEnum::Treemap // 'treemap' ApexChartTypeEnum::Empty // '' // Usage inside getOptions() 'chart' => ['type' => ApexChartTypeEnum::Line, 'height' => 300], ``` --- ## `getOptions(): array` The single required method on every widget. Returns a PHP array that is serialised directly to the ApexCharts JavaScript options object. Every key documented at is valid here. ```php // Line chart with smooth curve protected function getOptions(): array { return [ 'chart' => ['type' => 'line', 'height' => 300], 'series' => [ ['name' => 'Revenue', 'data' => [2, 4, 6, 10, 14, 7, 2, 9, 10, 15, 13, 18]], ], 'xaxis' => [ 'categories' => ['Jan','Feb','Mar','Apr','May','Jun', 'Jul','Aug','Sep','Oct','Nov','Dec'], 'labels' => ['style' => ['colors' => '#9ca3af', 'fontWeight' => 600]], ], 'yaxis' => ['labels' => ['style' => ['colors' => '#9ca3af']]], 'colors' => ['#6366f1'], 'stroke' => ['curve' => 'smooth'], ]; } // Pie chart (series is a flat array, labels is a top-level key) protected function getOptions(): array { return [ 'chart' => ['type' => 'pie', 'height' => 300], 'series' => [2, 4, 6, 10, 14], 'labels' => ['Jan', 'Feb', 'Mar', 'Apr', 'May'], 'legend' => ['labels' => ['colors' => '#9ca3af', 'fontWeight' => 600]], ]; } // Area chart protected function getOptions(): array { return [ 'chart' => ['type' => 'area', 'height' => 300], 'series' => [['name' => 'Visitors', 'data' => [7,4,6,10,14,7,5,9,10,15,13,18]]], 'xaxis' => ['categories' => ['Jan','Feb','Mar','Apr','May','Jun', 'Jul','Aug','Sep','Oct','Nov','Dec']], 'colors' => ['#6366f1'], 'stroke' => ['curve' => 'smooth'], 'dataLabels' => ['enabled' => false], ]; } ``` --- ## `updateOptions(): void` Triggers a live browser update of the chart's data without a full Livewire re-render. Call this after mutating any state that `getOptions()` depends on (e.g., after a filter change). ```php use Leandrocfe\FilamentApexCharts\Widgets\ApexChartWidget; class SalesChart extends ApexChartWidget { protected static ?string $chartId = 'salesChart'; protected static ?string $heading = 'Sales'; public string $period = 'monthly'; // Livewire action — wire:click="setPeriod('weekly')" public function setPeriod(string $period): void { $this->period = $period; $this->updateOptions(); // pushes new getOptions() result to ApexCharts } protected function getOptions(): array { $data = $this->period === 'weekly' ? [10, 20, 30, 40, 50, 60, 70] : [7, 4, 6, 10, 14, 7, 5, 9, 10, 15, 13, 18]; return [ 'chart' => ['type' => 'bar', 'height' => 300], 'series' => [['name' => 'Sales', 'data' => $data]], ]; } } ``` --- ## `extraJsOptions(): ?RawJs` Merges raw JavaScript (including closures) into the chart options at render time. Use this for formatter functions that cannot be expressed as PHP strings. ```php use Filament\Support\RawJs; protected function extraJsOptions(): ?RawJs { return RawJs::make(<<<'JS' { yaxis: { labels: { formatter: function (val) { return '$' + val.toLocaleString() } } }, tooltip: { x: { formatter: function (val) { return val + '/24' } } }, dataLabels: { enabled: true, formatter: function (val, opt) { return opt.w.globals.labels[opt.dataPointIndex] + ': $' + val }, dropShadow: { enabled: true } } } JS); } ``` --- ## Widget Header — `HasHeader` Trait Controls the card title, subtitle, and collapsibility of the widget. All properties have corresponding override methods. ```php class RevenueChart extends ApexChartWidget { protected static ?string $chartId = 'revenueChart'; // Static property approach protected static ?string $heading = 'Monthly Revenue'; protected static ?string $subheading = 'Compared to last year'; protected static bool $isCollapsible = true; // Or override the methods for dynamic values: protected function getHeading(): string { return 'Revenue — ' . now()->year; } protected function getSubheading(): string { return 'Total: $' . number_format(\App\Models\Order::sum('total')); } protected function isCollapsible(): bool { return auth()->user()->prefersCollapsedWidgets(); } protected function getOptions(): array { return ['chart' => ['type' => 'line', 'height' => 250], 'series' => []]; } } ``` --- ## Widget Footer — `HasFooter` Trait Renders an optional footer below the chart. Accepts a plain string, an `Htmlable` instance (e.g., `HtmlString`), or a Blade view. ```php use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\View\View; use Illuminate\Support\HtmlString; class StatsChart extends ApexChartWidget { protected static ?string $chartId = 'statsChart'; // Plain string protected static ?string $footer = 'Data refreshed every 5 seconds.'; // Or a dynamic HTML string: protected function getFooter(): null|string|Htmlable|View { $avg = \App\Models\Sale::avg('amount'); return new HtmlString( '

Average sale: $' . number_format($avg, 2) . '

' ); } // Or a Blade view: // protected function getFooter(): View // { // return view('widgets.chart-footer', ['period' => 'Q1 2025']); // } protected function getOptions(): array { return ['chart' => ['type' => 'bar', 'height' => 250], 'series' => []]; } } ``` --- ## Content Height — `HasContentHeight` Trait Sets a fixed pixel height for the chart content area, overriding the height defined inside `getOptions()['chart']['height']` at the widget wrapper level. ```php class TallChart extends ApexChartWidget { protected static ?string $chartId = 'tallChart'; // Static property (pixels) protected static ?int $contentHeight = 500; // Or via method: protected function getContentHeight(): ?int { return request()->is('*/dashboard') ? 400 : 250; } protected function getOptions(): array { return [ 'chart' => ['type' => 'area', 'height' => $this->getContentHeight() ?? 300], 'series' => [['name' => 'Data', 'data' => [1,3,5,7,9]]], ]; } } ``` --- ## Dark Mode — `HasDarkMode` Trait Dark mode is enabled by default. Disable it per widget via a static property, or manually control the ApexCharts theme inside `getOptions()`. ```php class LightOnlyChart extends ApexChartWidget { protected static ?string $chartId = 'lightChart'; // Disable automatic dark mode handling protected static bool $darkMode = false; // Or force a specific theme inside getOptions(): protected function getOptions(): array { return [ 'theme' => ['mode' => 'light'], // or 'dark' 'chart' => ['type' => 'bar', 'height' => 300], 'series' => [['name' => 'Data', 'data' => [5, 10, 15]]], ]; } } ``` --- ## Deferred Loading — `CanDeferLoading` Trait Delays data fetching until after the page is rendered. The widget displays a loading indicator immediately, then fires a Livewire request to fetch chart data. Ideal for slow queries. ```php class HeavyQueryChart extends ApexChartWidget { protected static ?string $chartId = 'heavyChart'; protected static bool $deferLoading = true; protected static ?string $loadingIndicator = 'Loading chart data…'; protected function getOptions(): array { // Guard: return empty array while still loading if (! $this->readyToLoad) { return []; } // Expensive query runs only after the page has loaded $data = \App\Models\AnalyticsEvent::query() ->selectRaw('DATE(created_at) as date, COUNT(*) as hits') ->groupBy('date') ->orderBy('date') ->limit(30) ->pluck('hits', 'date'); return [ 'chart' => ['type' => 'line', 'height' => 300], 'series' => [['name' => 'Page Hits', 'data' => $data->values()->toArray()]], 'xaxis' => ['categories' => $data->keys()->toArray()], ]; } } ``` --- ## Loading Indicator — `HasLoadingIndicator` Trait Customises the loading placeholder shown while data is being fetched (used with deferred loading or polling). ```php use Illuminate\Contracts\View\View; class CustomLoadingChart extends ApexChartWidget { protected static ?string $chartId = 'customLoadingChart'; protected static bool $deferLoading = true; // Simple string protected static ?string $loadingIndicator = 'Fetching data…'; // Or a Blade view: protected function getLoadingIndicator(): null|string|View { return view('widgets.chart-skeleton'); // resources/views/widgets/chart-skeleton.blade.php: //
} protected function getOptions(): array { if (! $this->readyToLoad) { return []; } return ['chart' => ['type' => 'bar', 'height' => 300], 'series' => []]; } } ``` --- ## Live Polling — `CanPoll` Trait (via Filament) Chart data refreshes automatically on a configurable interval. Set `$pollingInterval` to a duration string or `null` to disable. ```php class LiveSalesChart extends ApexChartWidget { protected static ?string $chartId = 'liveSalesChart'; // Default is '5s'. Override with any valid interval string: protected static ?string $pollingInterval = '30s'; // Disable polling entirely: // protected static ?string $pollingInterval = null; protected function getOptions(): array { return [ 'chart' => ['type' => 'line', 'height' => 300], 'series' => [ ['name' => 'Orders', 'data' => \App\Models\Order::query() ->selectRaw('COUNT(*) as c') ->groupByRaw('HOUR(created_at)') ->pluck('c')->toArray()], ], ]; } } ``` --- ## Single-Select Filter — `CanFilter` Trait (`getFilters()` / `$filter`) Renders a `