mukadi/chartjs-bundle
A Laravel bundle for rendering Chart.js charts from PHP. Build chart datasets and options server-side, then output them to Chart.js in your views, keeping chart configuration in your controllers and blade templates instead of hand-written JavaScript.
Read this first: this is a Symfony bundle, not a Laravel package. It ships a DependencyInjection extension, a bundle class and a Twig extension, and its headline feature — "build charts directly from your ORM Entities" — is built around Doctrine ORM. In a Laravel/Eloquent app, the bundle registration and the Doctrine data provider are dead weight. What is actually reusable is the small, framework-agnostic core library it depends on: Mukadi\Chart\Chart, Mukadi\Chart\Dataset and Mukadi\Chart\ChartBuilder (serialize to the Chart.js data/options shape) plus the DataProviderInterface extension point.
So the realistic Laravel play is: use the core classes as a typed chart-config builder, and write your own Eloquent-backed provider.
Minimal steps:
composer require mukadi/chartjs-bundle
In Laravel, do not register the bundle in config/app.php providers — there is no service provider. Pull the classes directly:
use Mukadi\Chart\Chart;
use Mukadi\Chart\Dataset;
$chart = new Chart(Chart::TYPE_LINE);
$chart->setData([
'labels' => ['Jan', 'Feb', 'Mar'],
'datasets' => [
[
'label' => 'Revenue',
'data' => [1200, 1900, 3000],
'backgroundColor' => 'rgba(54, 162, 235, .5)',
],
],
]);
// What you hand to the browser:
$payload = $chart->getData(); // Chart.js `data` object
$options = $chart->getOptions(); // Chart.js `options` object
Where to look in the repo, in order:
src/Chart.php / src/Dataset.php (or the mukadi/chart vendor dir) — the entire public surface is here; it is only a few hundred lines.src/DataProvider/DoctrineORMDataProvider.php — read it as a design template for the provider you will write for Eloquent.src/Resources/views/ and the Twig extension (Mukadi\ChartJSBundle\Twig\...) — you will re-implement these as a Blade component / @push('scripts') block.src/ChartBuilder.php — trivial factory; useful only if you want a container binding.First use case to try: take an existing dashboard where you hand-write a Chart.js JSON blob inside a Blade file, and replace it with a Chart object built in an invokable controller or an App\Support\Charts\* class. If that doesn't feel cleaner than an array literal, you have your answer about adoption.
1. Controller-builds-chart, Blade-echoes-JSON. Keep the wiring dumb; the chart object is just data.
// app/Http/Controllers/DashboardController.php
public function __invoke(): View
{
$chart = (new Chart(Chart::TYPE_BAR))
->setData([
'labels' => Order::query()->distinct()->pluck('status'),
'datasets' => [[ /* ... */ ]],
]);
return view('dashboard', ['chart' => $chart]);
}
<canvas id="orders"></canvas>
@push('scripts')
<script>
new Chart(document.getElementById('orders'), @json([
'type' => $chart->getType(),
'data' => $chart->getData(),
'options' => $chart->getOptions(),
]));
</script>
@endpush
@json is doing the same job as the bundle's Twig renderer. You get XSS-safe escaping and zero Symfony coupling.
2. A Blade component instead of the Twig extension. This is the closest 1:1 port of the bundle's rendering API.
{{-- resources/views/components/chart.blade.php --}}
@props(['chart', 'id'])
<canvas id="{{ $id }}"></canvas>
@push('scripts')
<script>
new Chart(document.getElementById(@js($id)), @js([
'type' => $chart->getType(),
'data' => $chart->getData(),
'options' => $chart->getOptions(),
]));
</script>
@endpush
Usage: <x-chart :chart="$chart" id="orders" />.
3. Custom Eloquent data provider — the real value of the package. The bundle's DataProviderInterface is a tiny contract (roughly: given a chart/dataset config, return labels + numeric series). Implement it once against the query builder and reuse everywhere:
namespace App\Charts;
use Mukadi\Chart\DataProvider\DataProviderInterface;
final class EloquentAggregateProvider implements DataProviderInterface
{
public function provide(array $config): array
{
$q = $config['model']::query()
->selectRaw("{$config['groupBy']} as label, {$config['aggregate']} as value")
->groupBy('label')
->orderBy('label');
foreach ($config['constraints'] ?? [] as $col => $val) {
$q->where($col, $val);
}
return $q->pluck('value', 'label')->all(); // ['label' => value]
}
}
Then a ChartFactory that maps that map into Chart::getData()['labels'] / datasets[0]['data']. This mirrors exactly what the Doctrine provider does (setEntityClass, group-by, aggregate expression) but against Eloquent — and it is where aggregate/date-bucketing logic belongs, not in your controllers.
4. One dataset per series, not per query. For comparison charts (this year vs last year), build the label union once, then array-map each series onto those labels so missing months become 0/null instead of silently shifting the bars. This is the single most common chart bug and the package won't do it for you.
$labels = collect([...$a->keys(), ...$b->keys()])->unique()->sort()->values();
$datasets = [
['label' => '2024', 'data' => $labels->map(fn ($l) => $a[$l] ?? 0)->all()],
['label' => '2025', 'data' => $labels->map(fn ($l) => $b[$l] ?? 0)->all()],
];
5. JSON endpoint for SPA/AJAX dashboards. Skip the view layer entirely:
return response()->json([
'type' => $chart->getType(),
'data' => $chart->getData(),
'options' => $chart->getOptions(),
])->header('Cache-Control', 'private, max-age=60');
This is the only integration point that is genuinely framework-neutral, and it is where I would stop if evaluating the package.
6. Centralise Chart.js defaults once. Chart::defaults()-style global config (fonts, colours, tooltip formatting, plugins.legend.position) belongs in a single config file or a base App\Charts\Chart class, not copied into every chart. Treat getOptions() as the layer you override per chart on top of those defaults.
MukadiChartJSBundle extends Symfony\Component\HttpKernel\Bundle\Bundle; there is no Laravel auto-discovery, no service provider, no facade. Composer will pull in symfony/* transitive deps. If you only want the chart builder, depend on the underlying mukadi/chart package rather than the bundle to keep the dependency tree honest.DQL aggregate expressions and GROUP BY semantics map to selectRaw/groupBy, not to Eloquent's withCount helpers.twig/twig + the Symfony Twig bridge installed, the extension is registered by the bundle's DI extension, which Laravel never runs. Re-implement it as the Blade component above — it's ~10 lines.Resources/public/ — you are responsible for loading a Chart.js version (CDN or Vite/npm). Pin the version; the package predates Chart.js v4's ESM-only distribution and any generated options may assume v2/v3 shapes (e.g. scales.xAxes vs scales.x). Test against your installed Chart.js before trusting getOptions()."1200"), and PDO/Postgres NUMERIC/DECIMAL definitely do. Cast with (float) in your provider or Chart.js silently renders empty. Dates from DATE_TRUNC/DATE_FORMAT need formatting into display labels in PHP, not JS.null labels. GROUP BY over a nullable column yields a null/"" bucket that breaks the label axis. whereNotNull() it or map to "Unknown" explicitly.Cache::remember("chart:revenue:{$year}", 300, ...)); the chart layer adds no caching of its own.composer.json, expect no upstream fixes, and be ready to vendor-copy Mukadi\Chart\* into app/Charts/ if you need to patch it. The classes are small enough that forking is a realistic option — and probably better than a long-lived dependency on an unmaintained Symfony bundle in a Laravel app.DataProviderInterface (your own Eloquent/API/cache-backed providers) and subclassing Chart/Dataset to bake in house defaults. Avoid extending the bundle's DI/Twig layer.$chart->getData() and the final HTML source, not the rendered canvas. 90% of "chart is blank" cases are (a) Chart.js not loaded, (b) data containing strings, or (c) the script block running before the canvas exists — @push('scripts') after @stack('scripts') in the layout, or wrap in DOMContentLoaded.How can I help you explore Laravel packages today?