symfony/ux-chartjs
Symfony UX Chart.js integrates the Chart.js library into Symfony apps, letting you render interactive charts directly from PHP with Twig components and Stimulus controllers, no custom JavaScript required.
Install the package (PHP 8.1+, Symfony 6.4/7.x — v3.6.0 is the current line, replacing the v3.5.x series):
composer require symfony/ux-chartjs
Pin explicitly if you need reproducible builds: composer require symfony/ux-chartjs:^3.6.
Enable the bundle (Flex does this automatically; add it manually only for non-Flex apps):
// config/bundles.php
return [
// ...
Symfony\UX\ChartJsBundle\ChartJsBundle::class => ['all' => true],
];
Install Chart.js — the package ships the Stimulus controller but not the library itself:
composer require chartjs/chart.js # Flex/AssetMapper route
# or
npm install chart.js # Webpack Encore / Vite route
Render a chart (both syntaxes work in UX 3.x — the component() function and the Twig component tag):
<twig:ChartjsChart
type="line"
:data="{
labels: ['Jan', 'Feb', 'Mar'],
datasets: [{ label: 'Sales', data: [10, 20, 30] }]
}"
:options="{ responsive: true }"
/>
Rebuild assets after the upgrade — v3.6.0 bumped the package's own tooling dependencies (#4086), so expect a lockfile refresh on the first npm install / importmap:install after upgrading:
php bin/console importmap:install && php bin/console asset-map:compile
# or
npm install && npm run build
vendor/symfony/ux-chartjs/templates/components/ChartjsChart.html.twig — the canonical attribute list (type, data, options, class).vendor/symfony/ux-chartjs/assets/src/controller.js — the Stimulus controller; this is what you subclass or listen to for live updates.vendor/symfony/ux-chartjs/README.md — note that install snippets now use an X.Y.Z placeholder (#3907); substitute the real version rather than copy-pasting literally.Start with a plain controller + Twig render; reach for LiveComponent only once you need reactivity.
// src/Controller/DashboardController.php
#[Route('/dashboard', name: 'dashboard')]
public function dashboard(SalesRepository $sales): Response
{
return $this->render('dashboard.html.twig', [
'chartData' => [
'labels' => $sales->monthlyLabels(),
'datasets' => [[
'label' => 'Revenue',
'data' => $sales->monthlyTotals(),
'backgroundColor' => 'rgba(59, 130, 246, .5)',
]],
],
]);
}
{# templates/dashboard.html.twig #}
<twig:ChartjsChart type="bar" :data="chartData" :options="{ responsive: true }" />
Data Binding
viewValueChanged.
#[AsLiveComponent]
final class SalesChart extends AbstractController
{
#[LiveProp(writable: true)]
public string $range = '30d';
public function getData(): array { /* ... */ }
}
<twig:SalesChart :range="range" />
chart.data = …; chart.update() rather than round-tripping through PHP.Chart Customization
options object — anything valid in Chart.js is valid here, since it is serialized straight through.
:options="{
plugins: { legend: { position: 'bottom' }, title: { display: true, text: 'Monthly Sales' } },
scales: { y: { beginAtZero: true } }
}"
// assets/app.js
import { Chart, registerables } from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
Chart.register(...registerables, annotationPlugin);
Live Updates via the Stimulus Controller
chartjs:connect event, or subclass the package controller:
// assets/controllers/polling_chart_controller.js
import ChartjsController from '@symfony/ux-chartjs/controller';
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
this.element.addEventListener('chartjs:connect', (event) => {
this.chart = event.detail.chart;
this.timer = setInterval(() => this.refresh(), 30_000);
});
}
async refresh() {
const data = await (await fetch(this.element.dataset.url)).json();
this.chart.data = data;
this.chart.update();
}
disconnect() { clearInterval(this.timer); }
}
Reusable Components
{# templates/components/Chart/Line.html.twig #}
<twig:ChartjsChart type="line" :data="data" :options="{ elements: { line: { tension: 0.4 } } }" />
importmap:require chart.js; the package's own JS is imported via @symfony/ux-chartjs/controller.chart.js to package.json and, for Vite, pre-bundle it:
// vite.config.js
optimizeDeps: { include: ['chart.js', 'chartjs-plugin-annotation'] }
turbo:load automatically because the Stimulus controller reconnects — do not cache chart canvases in Turbo frames without data-turbo-permanent care.<canvas> with aria-label and a fallback <table>; charts are invisible to screen readers otherwise.symfony/asset-mapper 8.2+ conflict — fixed in v3.6.0 (#3919)
<= v3.5.0) conflict with symfony/asset-mapper 8.2 and later, typically surfacing as missing/duplicate importmap entries or a Cannot find asset error at asset-map:compile time.symfony/ux-chartjs ^3.6. If you must stay on an older ux-chartjs, pin symfony/asset-mapper to <8.2.php bin/console cache:clear && php bin/console asset-map:compile
Stale JS after the tooling bump (#4086)
v3.6.0 refreshed the package's dev/tooling dependencies. Vendored asset output is unchanged in behaviour, but a stale importmap.php/node_modules can produce duplicate Chart.js registrations ("line" is not a registered controller or double-drawn tooltips). Delete node_modules/vendor artifacts and reinstall once.Twig component syntax confusion
{{ component('chartjs_chart', {...}) }} and <twig:ChartjsChart … /> are valid; {% use %} is Twig template inheritance and has nothing to do with components. The X.Y.Z placeholder in the README (#3907) is the other common copy-paste trap — substitute real versions.Chart.js version conflicts
[email protected] and other v3-era plugins will silently no-op or throw on tree-shaken registrations. Verify plugin major versions match.Chart.register(LineController, …)), otherwise the chart mounts as an empty canvas with no console error.LiveComponent re-render churn
LiveProp change destroys and recreates the Chart.js instance, losing zoom/pan/legend-toggle state. Use data-symfony--live--… model updates sparingly, or keep the instance and mutate chart.data from Stimulus instead.Serialization mismatches
labels/data. Always array_values() your labels and dataset series.DateTimeInterface objects do not serialize — format to Y-m-d strings in PHP, or use Chart.js' time scale with parsing: false.application.debug = true and look for chartjs:connect firing on the element.{{ dump(component('chartjs_chart', { type: 'line', data: chartData })) }} renders the resolved props without mounting.Chart.getChart(canvasElement) in the console returns the mounted instance, or undefined if nothing mounted — a fast way to separate a JS problem from a data problem.php bin/console debug:asset-map | grep chart confirms the controller is mapped; php bin/console importmap:audit catches version conflicts after an upgrade.data-* attribute (data-symfony--ux-chartjs--chart-view-value) — it is exactly the JSON Chart.js receives, which short-circuits most "why is my chart blank" investigations.@symfony/ux-chartjs/controller to add refresh(), export-to-PNG, or drill-down click handlers; dispatch your own events from it so other controllers can react.assets/app.js so it happens once, not per component.data/options arrays to Twig.chart.data and call chart.update('none') to avoid animation stutter on high-frequency updates.assets/src/, not Resources/assets/; upgrade guides referencing the old path will silently break importmap entries.asset_mapper: when using AssetMapper you do not set framework.asset_mapper: true manually — the bundle's recipe does it, and the 8.2 conflict above is the thing to watch.X.Y.Z since v3.6.0; substituting a placeholder literally produces a Could not find package … X.Y.Z failure that reads like a network problem.<canvas> and inline styles; if you run a strict Content-Security-Policy, add style-src 'unsafe-inline' or Chart.js' resize observer will fail silently.How can I help you explore Laravel packages today?