happenv-com/filament-enhanced-charts
Composer 安装命令:
composer require happenv-com/filament-enhanced-charts
包简介
Apache ECharts integration for Filament
README 文档
README
Apache ECharts integration for Filament: dashboard widgets and table columns driven by a fully typed, fluent PHP option model.
class OrdersChart extends EnhancedChartWidget { protected static ?string $heading = 'Orders per day'; protected function getOption(): Option { return Option::cartesian() ->xAxis(CategoryAxis::make()->data(['Mon', 'Tue', 'Wed'])) ->series(LineSeries::make()->name('Orders')->smooth()->data([12, 20, 15])); } }
- Typed option model — the ECharts option tree mirrored as fluent PHP builders. If you know an
ECharts option, you know the method. Everything not (yet) modelled stays reachable through a
->raw()escape hatch on every node. - 22 series types — line, bar, pie, scatter, effectScatter, candlestick, boxplot, heatmap,
radar, gauge, funnel, sankey, sunburst, treemap, tree, graph, parallel, themeRiver, pictorialBar,
map, lines, and fully custom
renderItemseries. - Every coordinate system — cartesian grids, polar, radar, geo/maps, calendar, singleAxis, parallel, matrix — plus datasets with transforms, visual maps, data zoom, toolbox, brush and graphic elements.
- Filament-native — panel theming, automatic dark mode, Filament
Colorpalettes,RawJsfor client-side callbacks, Livewire polling, deferred loading, filters. - Table columns — sparklines, candlesticks and donut pies inside table cells via
EChartColumn, or any custom per-record chart.
Requirements
- PHP
^8.4 - Filament
^4.0or^5.0
Installation
composer require happenv-com/filament-enhanced-charts
Register the plugin on each Filament panel that should render charts:
use Happenv\FilamentEnhancedCharts\FilamentEnhancedChartsPlugin; public function panel(Panel $panel): Panel { return $panel ->plugins([ FilamentEnhancedChartsPlugin::make(), ]); }
After installing or upgrading, republish the compiled JavaScript assets:
php artisan filament:assets
Upgrading from 2.x? See UPGRADE.md.
Your first widget
A chart widget is a regular Filament widget: extend EnhancedChartWidget and return an Option from
getOption(). That is the whole story.
use Happenv\FilamentEnhancedCharts\Option\Axis\CategoryAxis; use Happenv\FilamentEnhancedCharts\Option\Option; use Happenv\FilamentEnhancedCharts\Option\Series\BarSeries; use Happenv\FilamentEnhancedCharts\Option\Series\LineSeries; use Happenv\FilamentEnhancedCharts\Widgets\EnhancedChartWidget; class BlogPostsChart extends EnhancedChartWidget { protected static ?string $heading = 'Blog posts'; protected function getOption(): Option { return Option::cartesian() ->xAxis(CategoryAxis::make()->data(['Jan', 'Feb', 'Mar'])) ->series( LineSeries::make()->name('Posts')->smooth()->data([10, 15, 8]), BarSeries::make()->name('Comments')->data([45, 60, 32]), ); } }
Two entry points exist on Option:
Option::make()— a blank slate.Option::cartesian()— a batteries-included preset for the common case: an axis-triggered tooltip and a top legend, ready for->xAxis()+->series().
When an x-axis is set and no y-axis is given, a ValueAxis is defaulted for you. Set
->yAxis(...) explicitly when you need something else (e.g. a CategoryAxis for a heatmap).
The option model
Everything under Happenv\FilamentEnhancedCharts\Option\* is a fluent builder that serializes to the
exact ECharts option shape. This package deliberately
does not re-document every chart type — the ECharts docs and
examples gallery are the reference; the builders mirror
them method-for-method.
| Kind | Classes |
|---|---|
| Root | Option |
| Axes | Axis\CategoryAxis, Axis\ValueAxis, Axis\TimeAxis, Axis\LogAxis |
| Series | Series\LineSeries, BarSeries, PieSeries, ScatterSeries, EffectScatterSeries, CandlestickSeries, BoxplotSeries, HeatmapSeries, RadarSeries, GaugeSeries, FunnelSeries, SankeySeries, SunburstSeries, TreemapSeries, TreeSeries, GraphSeries, ParallelSeries, ThemeRiverSeries, PictorialBarSeries, MapSeries, LinesSeries, CustomSeries |
| Components | Component\Legend, Tooltip, Title, Grid, DataZoom, VisualMap, Toolbox (+ Toolbox\* features), AxisPointer, Brush, Polar, AngleAxis, RadiusAxis, Radar, Geo, Calendar, SingleAxis, Parallel, ParallelAxis, Matrix, Dataset, Transform, Graphic (+ Graphic\* elements) |
| Styles | Style\ItemStyle, LineStyle, AreaStyle, Label, Emphasis |
| Marks | Mark\MarkLine, MarkPoint, MarkArea |
| Data | DataPoint, SankeyNode, SankeyLink |
Conventions that hold across the whole tree:
::make()+ fluent setters. Every builder is created with::make()and configured by chaining; setters return$this.- One or many. Slots that ECharts accepts as "an object or an array of objects" are variadic:
->grid(...),->xAxis(...),->title(...),->series(...),->visualMap(...), … Pass one or several; the emitted JSON follows ECharts' single-vs-array convention automatically. - Builders or arrays. Sub-config setters accept the matching builder or a plain array:
->itemStyle(ItemStyle::make()->borderRadius(4))and->itemStyle(['borderRadius' => 4])are equivalent. - Conditional building. All builders use Laravel's
Conditionable:->when($this->filter === 'year', fn (Option $o) => $o->dataZoom(...)). - Per-point configuration. Wherever a data item can be more than a value, pass a
DataPoint:DataPoint::make(1048)->name('Search')->itemStyle(ItemStyle::make()->color('#c23531')).
The raw() escape hatch
Every node (Option, each series, axis, component, DataPoint, …) accepts
->raw(array $options). Raw options are deep-merged over the typed output, raw wins — so you
can reach any ECharts option the typed API doesn't model yet without abandoning the builders:
LineSeries::make() ->data($values) ->raw(['universalTransition' => true]);
To skip the typed model entirely, return a raw tree from a blank Option:
protected function getOption(): Option { return Option::make()->raw([ 'xAxis' => ['type' => 'category', 'data' => ['A', 'B']], 'series' => [['type' => 'bar', 'data' => [5, 7]]], ]); }
Prefer the typed methods — raw() is the last resort, not the default.
Formatters and JS callbacks
Strings passed to formatter()-style methods are treated as literal
ECharts templates and pass through
unchanged. For a real client-side function, pass Filament's RawJs: it is serialized as a marker
and revived into a genuine JS function in the browser.
use Filament\Support\RawJs; use Happenv\FilamentEnhancedCharts\Option\Axis\ValueAxis; use Happenv\FilamentEnhancedCharts\Option\Component\Tooltip; Option::make() ->tooltip(Tooltip::make()->trigger('axis')->formatter('{b}: {c} pcs')) // template string ->yAxis(ValueAxis::make()->axisLabel( RawJs::make('(value) => (value / 1000) + "k"'), // executable JS ));
RawJs works anywhere in the tree — renderItem on a CustomSeries, symbolSize callbacks,
animationDelay functions, labelLayout, colors-by-callback, and so on. Inside these callbacks
the enhanced-charts global is available, so gallery snippets using enhanced-charts.format.addCommas(...) or
enhanced-charts.graphic.clipRectByRect(...) port verbatim.
Numbers
Anywhere a number is accepted you may pass a native \BcMath\Number instead of
int|float|string; it is serialized as an exact numeric literal rather than a lossy float.
Feeding data from Eloquent
A series' data() and an axis' data() take any iterable, so a Collection works — but the
shape matters:
-
A positional list (a bare
pluckor an array) → pass it straight todata(). Keys are dropped (the series/axis is positional), so a keyed Collection never mis-serializes to an object:$totals = Order::query()->orderBy('day')->pluck('total'); // [120, 90, 140, …] LineSeries::make()->data($totals);
-
A keyed map where the key is the label (
pluck('total', 'channel'), agroupBycount) → useChartData::fromPairs()to split it into labels + values:use Happenv\FilamentEnhancedCharts\Data\ChartData; $data = ChartData::fromPairs(Order::query()->pluck('total', 'channel')); // ['B2B' => 60, …] Option::make() ->series(PieSeries::make()->data($data->toDataPoints())); // named slices // or, for a cartesian chart: Option::cartesian() ->xAxis(CategoryAxis::make()->data($data->labels())) ->series(BarSeries::make()->data($data->values()));
-
Tabular rows for multiple series → build a
Datasetand let each seriesencode()its columns.Dataset::fromModels()projects a query result to just the chart columns (never the full attribute bag) — don't hand aCollection<Model>todata():Option::make() ->dataset(Dataset::fromModels( Order::query()->selectRaw('day, sum(total) total, channel')->get(), columns: ['day', 'total', 'channel'], )) ->series(LineSeries::make()->encode(x: 'day', y: 'total'));
Time series
For "per day/week/month" aggregations, flowframe/laravel-trend
is the recommended companion: it handles the per-driver date bucketing (Postgres/MySQL/MariaDB/
SQLite) and gap-fills empty periods with zeros. It is an optional suggest — install it only if you
need time series:
composer require flowframe/laravel-trend
Feed its result straight to ChartData::fromTimeSeries():
use Flowframe\Trend\Trend; use Happenv\FilamentEnhancedCharts\Data\ChartData; $data = ChartData::fromTimeSeries( Trend::query($this->scopedOrders()) ->between(now()->subDays(30), now()) ->perDay() ->sum('total'), ); Option::cartesian() ->xAxis(CategoryAxis::make()->data($data->labels())) ->series(LineSeries::make()->smooth()->data($data->values()));
ChartData::fromTimeSeries() reads date/aggregate off each row with data_get(), so it is
decoupled from the trend package — any Collection of rows carrying a label and a value column
works (pass labelKey:/valueKey: for different column names).
Shortcut: a chart from just data + a type
When the chart is a single series of one of the common types, the HasChartData trait writes
getOption() for you — declare the data and (optionally) the type, nothing else:
use Happenv\FilamentEnhancedCharts\Concerns\HasChartData; use Happenv\FilamentEnhancedCharts\Data\ChartData; use Happenv\FilamentEnhancedCharts\Enums\ChartType; use Happenv\FilamentEnhancedCharts\Widgets\EnhancedChartWidget; class SalesByChannelChart extends EnhancedChartWidget { use HasChartData; protected static ?string $heading = 'Sales by channel'; protected function getData(): ChartData { return ChartData::fromPairs(Order::query()->pluck('total', 'channel')); } protected function chartType(): ChartType { return ChartType::Bar; // Line (default), Area, Bar, or Pie } }
ChartType is a curated set — Line, Area, Bar, Pie — that maps cleanly onto the
labels + values (or, for pie, the named DataPoint) shape. The trait builds a cartesian option
(category x-axis + value y-axis) for the axis types and an axis-less option for pie. The moment you
want more — a second series, custom styling, marks, a scatter's x/y pairs — implement getOption()
directly with the typed builders; this trait is only the shallow-end shortcut.
Colors
Color setters (color(), backgroundColor(), borderColor(), shadowColor(), …) accept:
- any CSS color string (
'#7c3aed','rgb(124, 58, 237)'), - an ECharts gradient/pattern array,
- a Filament palette —
Color::Amberresolves to its 500 shade, and Filament'soklch(...)values are converted torgb()so ECharts can both paint them and derive hover shades:
use Filament\Support\Colors\Color; BarSeries::make() ->color(Color::Emerald) // 500 shade ->data($values); LineSeries::make() ->color(Color::Amber[600]) // an explicit shade ->data($values);
Dark mode
Dark mode is automatic. Charts render transparent over the Filament card, and when the panel is in dark mode the package overlays a theme layer client-side — axis lines/labels, split lines, legend, title, tooltip, visual maps, parallel axes and outside series labels are all lightened for readability. The theme reapplies live when the user toggles light/dark. You write the option once; both modes just work.
Tip: skip forced backgroundColor and hardcoded text colors in your options — inherit the card
and let the theme adapt.
Page scrolling
By default the mouse wheel scrolls the page, not the chart: inside data zoom stops
wheel-zooming (drag-to-pan is kept) and any roam is downgraded to drag-pan. That way a dashboard
full of charts never traps the user's scroll.
Opt a chart back into wheel-zoom (e.g. a full-bleed map) on the widget:
protected static bool $scrollable = false;
or per option, which wins over the widget default:
Option::make()->scrollable(false);
Widget configuration
protected static ?string $heading = 'Revenue'; // or override getHeading() protected static ?string $subheading = 'Last 30 days'; // or getSubheading() protected static bool $isCollapsible = true; // or isCollapsible() protected static int $contentHeight = 400; // px, default 300; or getContentHeight() protected static ?string $footer = 'Updated hourly'; // or getFooter() (string|Htmlable|View) protected static string $chartId = 'revenueChart'; // stable DOM id, optional protected static string $renderer = 'canvas'; // or 'svg'; or getRenderer() protected int | string | array $columnSpan = 'full';
The header disappears entirely when no heading, subheading or filters are set.
Deferred loading
Don't hold up the page for a slow query:
protected static bool $deferLoading = true; protected function getOption(): Option { if (! $this->readyToLoad) { return Option::make(); } return Option::cartesian()->series(/* the expensive part */); }
Loading indicator
protected static ?string $loadingIndicator = 'Loading…'; // or getLoadingIndicator() returning a View
Live updating (polling)
Widgets poll every 5 seconds by default. Change or disable it with an instance property:
protected ?string $pollingInterval = '10s'; protected ?string $pollingInterval = null; // disable
Updates are diffed server-side — the chart only re-renders when the resolved options actually changed.
Filtering chart data
Single select
Return options from getFilters() and read the active value from $this->filter:
public ?string $filter = 'month'; protected function getFilters(): ?array { return [ 'week' => 'Last week', 'month' => 'Last month', 'year' => 'This year', ]; } protected function getOption(): Option { $data = match ($this->filter) { 'week' => $this->weekly(), 'month' => $this->monthly(), 'year' => $this->yearly(), }; return Option::cartesian()->series(LineSeries::make()->data($data)); }
A select appears in the widget header and the chart updates live on change.
Filter schema
For a richer filter form, implement the package's HasFiltersSchema contract and use
Filament's chart-widget filters trait; filter values arrive in $this->filters:
use Filament\Forms\Components\DatePicker; use Filament\Schemas\Schema; use Filament\Widgets\ChartWidget\Concerns\HasFiltersSchema; use Happenv\FilamentEnhancedCharts\Contracts\HasFiltersSchema as HasFiltersSchemaContract; use Happenv\FilamentEnhancedCharts\Widgets\EnhancedChartWidget; class BlogPostsChart extends EnhancedChartWidget implements HasFiltersSchemaContract { use HasFiltersSchema; public function filtersSchema(Schema $schema): Schema { return $schema->components([ DatePicker::make('date_start')->default(now()->subMonth()), DatePicker::make('date_end')->default(now()), ]); } public function updatedInteractsWithSchemas(string $statePath): void { parent::updatedInteractsWithSchemas($statePath); $this->updateOptions(); } protected function getOption(): Option { $start = $this->filters['date_start'] ?? null; $end = $this->filters['date_end'] ?? null; // ... } }
Implementing the contract is what makes the filter dropdown render — don't skip it.
Maps (GeoJSON)
A MapSeries or geo component needs its map registered client-side. Return name => url pairs
from getMaps(); each is fetched and passed to enhanced-charts.registerMap() before first paint:
use Happenv\FilamentEnhancedCharts\Option\Component\VisualMap; use Happenv\FilamentEnhancedCharts\Option\Series\MapSeries; /** @return array<string, string> */ public function getMaps(): array { return ['world' => asset('geo/world.json')]; } protected function getOption(): Option { return Option::make() ->visualMap(VisualMap::continuous()->min(0)->max(100)->calculable()) ->series(MapSeries::make()->map('world')->roam()->data($rows)); }
Charts in table cells
EChartColumn renders a small chart per row. Three presets cover the common cases; chart()
takes over for anything else. Return null/empty data to render nothing for that row.
use Happenv\FilamentEnhancedCharts\Columns\EChartColumn; EChartColumn::make('trend') ->sparkline(fn (Product $record): array => $record->daily_sales) ->fill(), // area fill; ->bars() for bar sparklines EChartColumn::make('ohlc') ->width(160) // px, '100%', or a Closure; height(int) too ->candles(fn (Product $record): array => $record->ohlc_rows), // [open, close, low, high] rows EChartColumn::make('mix') ->pie(fn (Order $record): array => ['B2B' => 60, 'B2C' => 40]), // donut EChartColumn::make('custom') ->chart(fn (Server $record): ?Option => Option::make() ->series(GaugeSeries::make()->data([DataPoint::make($record->cpu)]))),
Cell charts ship with a sensible baseline (tooltip escaping the cell, no legend, tight margins)
and are disposed cleanly as rows leave the DOM. ->renderer('svg') is available per column.
Custom ->chart() closures can start from the same baseline via
EChartColumn::cellOption() (optionally passing the tooltip trigger, e.g. 'axis').
Publishing views / translations / config
php artisan vendor:publish --tag="filament-enhanced-charts-views" php artisan vendor:publish --tag="filament-enhanced-charts-translations" php artisan vendor:publish --tag="filament-enhanced-charts-config"
Testing
The package registers a Livewire Testable mixin with chart assertions for your app's tests:
use function Pest\Livewire\livewire; livewire(RevenueChart::class) ->assertChartSeriesCount(2) ->assertChartHasSeries('line') ->assertChartOptions(fn (array $options): bool => $options['series'][0]['smooth'] === true);
To run the package's own test suite:
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
- elemind
- Strongly inspired by Leandro Ferreira's Apex Charts plugin
- All Contributors
License
The MIT License (MIT). Please see License File for more information.
happenv-com/filament-enhanced-charts 适用场景与选型建议
happenv-com/filament-enhanced-charts 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 0 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 07 月 15 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「laravel」 「filamentphp」 「filament-echarts」 「happenv」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 happenv-com/filament-enhanced-charts 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 happenv-com/filament-enhanced-charts 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 happenv-com/filament-enhanced-charts 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Apache ECharts integration for Filament
A Filament plugin that integrates tabler icons, allowing you to use them seamlessly across Filament forms, tables, actions, and more.
jitone-ai is a powerful FilamentPHP plugin that integrates AI-powered features directly into your Filament forms.
Send Notification to discord channel Webhook using native FilamentPHP Notification Facade class
Alfabank REST API integration
Filament administration, page editing, recovery, settings, and operations for Capell CMS.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-15