coroowicaksono/chart-js-integration
A Laravel Nova package that integrates Chart.js into dashboards, providing ready-to-use chart cards and widgets for building responsive line, bar, pie, and doughnut visualizations with simple PHP configuration and customizable datasets.
Install and register
composer require coroowicaksono/chart-js-integration
No migrations, no config file, no publish step required for the default install — the service provider registers the compiled Vue card (dist/js/card.js) with Nova and exposes the PHP chart classes.
Drop a card into app/Providers/NovaServiceProvider.php::cards() (or a custom App\Nova\Dashboards\* class):
use Coroowicaksono\ChartJsIntegration\BarChart;
public function cards()
{
return [
(new BarChart())
->title('Orders per Month')
->model(\App\Models\Order::class)
->series([
['label' => 'Orders'],
])
->options([
'xaxis' => ['categories' => ['Jan','Feb','Mar','Apr','May','Jun']],
'showTotal' => true,
])
->width('1/2'),
];
}
Where to look first
vendor/coroowicaksono/chart-js-integration/src/ — each chart type (BarChart, LineChart, StackedChart, AreaChart, DoughnutChart, PieChart) is a thin subclass of a shared base card, so you can read all six in ten minutes.dist/js/card.js (or the resources/js source if present) — the options([...]) array is passed almost verbatim into the Chart.js config plus a few package-specific keys (showTotal, sum, uom, dataLabels, queryFilter, xaxis.categories, barType). When a key "does nothing", this file is the source of truth for what is actually consumed.First use case that pays off: replace a hand-rolled Nova metric with a one-series bar chart of a filtered model count over time. It is a two-minute change and gives you the Chart.js look without writing a Vue component.
Filtered multi-series (the 80% case) — each entry in series() is its own aggregate query:
(new BarChart())
->title('Revenue: Paid vs Pending')
->model(\App\Models\Order::class)
->series([
['label' => 'Paid', 'filter' => ['key' => 'status', 'value' => 'paid']],
['label' => 'Pending', 'filter' => ['key' => 'status', 'value' => 'pending']],
])
->options([
'sum' => 'total', // sum this column instead of counting rows
'uom' => 'USD',
'showTotal' => true,
])
->width('1/2'),
Scoping per user / tenant — filter only does an exact-match where(). Anything richer goes through a closure in options:
->options([
'queryFilter' => function ($query) {
return $query->where('team_id', auth()->user()->current_team_id)
->where('created_at', '>=', now()->subMonths(6));
},
])
Keep this closure cheap and index-friendly; it runs on every dashboard hit.
Stacked status breakdown — StackedChart with one series per status and a status/date grouping is the classic "ops board" card. Pair it with a DoughnutChart of the same data at width('1/3') for a current-slice view.
Doughnut / Pie — model these as "one series per slice", each with a filter; the legend labels come from label. If you already have precomputed counts (a nightly rollup table), point ->model() at the rollup model rather than the transactional table — it turns a 20-query dashboard into 20 indexed SUMs.
Custom dashboard + authorization — group cards in a Dashboard class and gate expensive ones with canSee(fn ($request) => $request->user()->can('viewRevenue')). Nova evaluates canSee server-side, which means blocked cards don't run their queries.
Caching pattern — the package does not cache. Two options that work well in practice:
dashboard_snapshots table on a schedule and have charts read that model.Cache::remember("chart:{$key}", 300, ...). The classes are small, plain PHP, so this is a ~20-line subclass rather than a fork.Composition tip — the width fractions ('1/3', '1/2', '1/1') should be consistent per row; mixing 1/2 and 1/3 in the same dashboard row leaves awkward gaps in Nova's grid.
laravel/nova or verify on a branch before upgrading; if you're on Nova 5, plan to fork the JS component or use Nova's native metrics instead.TypeError: ... must be of type Countable, array, stdClass given is the error fixed in v0.4.3 — it fires when the series/summary data handed to the card (or to a subclass overriding the data method) comes back as an object rather than an array, e.g. a DB::select()-style result or a cached JSON payload that was never ->toArray()ed. If you hit it, either upgrade to 0.4.3 or cast to array at the boundary; upgrading is the safer fix since the card internals now tolerate the object form.filter columns and the timestamp column you group by, or move to a snapshot table as soon as a table passes a few hundred thousand rows.filter is exact-match only. No operators, no IN, no ranges. Ranges, whereIn, and joins belong in queryFilter.created_at uses the database session timezone while xaxis.categories are your hand-written labels — for users near month boundaries the last bucket can look wrong. Normalise with ->options(['queryFilter' => fn ($q) => $q->whereBetween(...)]).options([...]) (e.g. sumTotal instead of sum) produces a default-looking chart, not an exception. If a chart renders but ignores your configuration, inspect dist/js/card.js for the key names it actually reads.chart-js-integration component means the provider assets weren't published or Nova's asset version is stale — re-run your publish step and php artisan nova:publish, then hard-refresh. Note that a patch release also bumps the asset version hash, so a hard refresh is mandatory after updating even for a one-line PHP fix.showTotal while developing so you can distinguish "no data" from "broken chart".Order excludes soft-deleted rows automatically — usually what you want, occasionally not. Use queryFilter with withTrashed() if a "total ever" card is required.auth() inside queryFilter is request-context only. Fine on dashboard load, but these cards cannot be rendered from a console command or a queued export. Extract the closure body into a query scope so both paths share logic.Chart object and cause legend/tooltip weirdness. Standardise on one charting approach per dashboard.stdClass results — pre-0.4.3 it would throw, and array-normalised output keeps you compatible with both paths.Trend, Value, and Partition metrics cover line, big-number, and pie/doughnut respectively, are maintained by Laravel, and cache well. Reach for this package specifically for stacked bars and area charts — those are the gaps in Nova's native metrics. With zero dependents and infrequent, bugfix-only releases, budget for the possibility that you maintain your fork.How can I help you explore Laravel packages today?