Visualize Data Without Custom JavaScript
Dashboards and charts in Symfony usually require custom JavaScript: initializing Chart.js, loading data via AJAX, configuring options. The Symfony UX Chart.js package takes over completely, charts are built from PHP objects, embedded as Twig components and initialized automatically by Symfony UX.
Table of Contents
- 1. Why Symfony UX Chart.js instead of manual JavaScript
- 2. Installing symfony/ux-chartjs
- 3. ChartBuilder: building charts from PHP objects
- 4. Configuring datasets and populating them from Doctrine
- 5. Chart types: bar, line, doughnut and more
- 6. Configuring chart options and styling
- 7. Embedding and rendering charts in Twig
- 8. Updating live charts with Turbo Frames
- 9. Symfony UX Chart.js vs. manual implementation
- 10. Summary
- 11. FAQ
1. Why Symfony UX Chart.js instead of manual JavaScript
Anyone building a dashboard with charts in Symfony typically starts with an API endpoint that delivers data as JSON, a custom JavaScript bundle that initializes Chart.js and loads the data, and a configuration file for colors, axes and tooltips. That is at least three separate building blocks that must be kept in sync. If the data schema changes, the API has to be adjusted, the JavaScript has to be updated, and tests must cover both sides. Symfony UX Chart.js eliminates this duality: the chart is configured entirely in PHP and rendered directly as a Twig component.
The symfony/ux-chartjs package is part of the Symfony UX ecosystem and integrates Chart.js 4 as a native Symfony building block. The ChartBuilder service creates chart objects with a PHP API, and the Twig function render_chart(chart) renders the canvas and passes the configuration as JSON attributes. The Stimulus controller JavaScript that initializes Chart.js comes from the package, no custom JavaScript needs to be written. The result: a bar chart with Doctrine data in under 30 lines of PHP and one line of Twig. For PHP teams that already know Symfony, this is the most direct path to a production ready dashboard.
2. Installing symfony/ux-chartjs
Installation happens via Composer, and the Flex recipe takes care of the setup automatically. The package registers itself in config/bundles.php, the Stimulus controller JavaScript is provided via AssetMapper, and the ChartBuilder service is immediately available in the container. With AssetMapper (recommended) no npm build step is needed, the @symfony/ux-chartjs package including Chart.js is delivered via importmap. Anyone using Webpack Encore additionally installs the npm package @symfony/ux-chartjs and runs yarn install.
After installation, check whether the Stimulus controller is registered in the importmap: bin/console debug:asset should list @symfony/ux-chartjs. The ChartBuilder service is automatically injected via dependency injection into a controller, view model or service. The Chart.js version that the package ships with is pinned and tested together with the Symfony package, manual version management for Chart.js is not needed. This is a significant difference from a manual Chart.js integration, where npm versions must be kept up to date manually.
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Repository\OrderRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\UX\Chartjs\Builder\ChartBuilderInterface;
use Symfony\UX\Chartjs\Model\Chart;
final class DashboardController extends AbstractController
{
public function __construct(
private readonly ChartBuilderInterface $chartBuilder,
private readonly OrderRepository $orderRepository,
) {}
#[Route('/admin/dashboard', name: 'admin_dashboard')]
public function index(): Response
{
// Build a bar chart from Doctrine data, no JavaScript needed
$chart = $this->chartBuilder->createChart(Chart::TYPE_BAR);
// Load monthly revenue data from the database
$monthlyRevenue = $this->orderRepository->getMonthlyRevenue(months: 12);
$chart->setData([
'labels' => array_column($monthlyRevenue, 'month'),
'datasets' => [
[
'label' => 'Revenue (EUR)',
'data' => array_column($monthlyRevenue, 'revenue'),
'backgroundColor' => 'rgba(59, 130, 246, 0.7)',
'borderColor' => 'rgba(59, 130, 246, 1)',
'borderWidth' => 1,
],
],
]);
$chart->setOptions(['responsive' => true, 'maintainAspectRatio' => false]);
return $this->render('admin/dashboard.html.twig', ['revenueChart' => $chart]);
}
}
3. ChartBuilder: building charts from PHP objects
The ChartBuilderInterface is the central entry point into Symfony UX Chart.js. The createChart() method accepts one of the Chart.js types as a constant of the Chart class: Chart::TYPE_BAR, Chart::TYPE_LINE, Chart::TYPE_PIE, Chart::TYPE_DOUGHNUT, Chart::TYPE_POLAR_AREA, Chart::TYPE_RADAR and Chart::TYPE_BUBBLE. The returned Chart object has three main methods: setData() for labels and datasets, setOptions() for all Chart.js options, and setPlugins() for Chart.js plugin configuration.
The data structure passed to setData() matches exactly the Chart.js JavaScript API, every Chart.js documentation example translates directly into PHP arrays. This is an important advantage: team members who already know Chart.js can translate the configuration without additional learning effort. Symfony UX Chart.js serializes the PHP array to JSON and passes it as a data-model-value attribute to the canvas, the Stimulus controller reads this attribute and initializes Chart.js. The PHP code always sees and manipulates type safe PHP structures, JavaScript ultimately receives clean JSON.
4. Configuring datasets and populating them from Doctrine
Multiple datasets in a Chart.js chart allow you to compare data series, for example revenue and order count over the same period. Each dataset is an associative array with label, data and styling properties. For line charts, fill, tension and pointRadius are added. The entries in the data array can be plain numbers or object structures for scatter and bubble charts.
Doctrine queries for dashboard data follow a clear pattern: a repository method aggregates the data by period and returns an array of associative arrays. array_column() then extracts the label and data columns for the Chart.js dataset. For more complex aggregations, for example multiple data series from a single query, you transform the result with array_map() or a dedicated transformer method. Performance matters here: dashboard queries should be backed by Doctrine caching or a separate cache layer (for example Symfony Cache), because complex aggregations over large datasets can be expensive.
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Order;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* Provides aggregated order data for dashboard charts.
*/
final class OrderRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Order::class);
}
/**
* Returns monthly revenue and order count for the last N months.
*
* @return array<int, array{month: string, revenue: float, orders: int}>
*/
public function getMonthlyRevenue(int $months = 12): array
{
return $this->createQueryBuilder('o')
->select(
"DATE_FORMAT(o.createdAt, '%Y-%m') AS month",
'SUM(o.total) AS revenue',
'COUNT(o.id) AS orders',
)
->where('o.createdAt >= :from')
->setParameter('from', new \DateTimeImmutable("-{$months} months"))
->groupBy('month')
->orderBy('month', 'ASC')
->getQuery()
->getArrayResult();
}
/**
* Returns per-category revenue for a doughnut chart.
*
* @return array<int, array{category: string, revenue: float}>
*/
public function getRevenueByCategory(): array
{
return $this->createQueryBuilder('o')
->join('o.items', 'i')
->join('i.product', 'p')
->join('p.category', 'c')
->select('c.name AS category', 'SUM(i.price * i.quantity) AS revenue')
->groupBy('c.name')
->orderBy('revenue', 'DESC')
->getQuery()
->getArrayResult();
}
}
5. Chart types: bar, line, doughnut and more
The different Chart.js types cover different visualization needs. Bar charts (TYPE_BAR) are well suited for category comparisons and time series with clear data points. Line charts (TYPE_LINE) show trends over time better, especially with fill: true for area charts. Doughnut and pie charts (TYPE_DOUGHNUT, TYPE_PIE) visualize shares of a whole, for example revenue distribution by category. Radar charts (TYPE_RADAR) are well suited for multi-attribute comparisons, for example performance metrics across multiple dimensions.
Horizontal bar charts are no longer achieved via a dedicated type in Chart.js 4, but via the indexAxis: 'y' option in the TYPE_BAR chart. Mixed chart types, for example bars for absolute values and a line for an average in the same chart, are possible via the type field on the individual dataset: 'type' => 'line' on a dataset of a bar chart produces the overlay. Symfony UX Chart.js passes all of these options through to Chart.js transparently, there is no PHP specific limitation compared to the native Chart.js API.
6. Configuring chart options and styling
The setOptions() method accepts the complete Chart.js options object as a PHP array. You enable responsive design with 'responsive' => true and 'maintainAspectRatio' => false, the latter lets the chart use the full height of its container, which matters for dashboard layouts. Axis labels, gridlines, tick formatting and tooltip formatting are all configured via nested PHP arrays. For callback functions, for example a tooltip that needs currency formatting, you pass JavaScript code strings directly as values, Symfony UX Chart.js marks these as raw JavaScript expressions when serializing.
For consistent color palettes across multiple charts, a central PHP constant or a service that supplies the color arrays is a good idea. That avoids inconsistencies between charts and makes theme switching easier. A Symfony specific detail: the browser dark mode does not automatically affect the canvas rendering, you have to explicitly configure colors for prefers-color-scheme: dark via JavaScript or a custom Stimulus controller. For most admin dashboards with a controlled theme, that is not a problem.
7. Embedding and rendering charts in Twig
Embedding a Symfony UX Chart.js chart in Twig is a single line: {{ render_chart(revenueChart) }}. Behind this function is the rendering of a <canvas> element with the Stimulus controller attribute data-controller="symfony--ux-chartjs--chart" and the chart data as a serialized JSON attribute. The Stimulus JavaScript reads this attribute when the DOM connects and initializes Chart.js with the given configuration. You control the size of the canvas element via HTML attributes or CSS on the wrapping container.
For multiple charts on a page, you pass multiple chart objects to the template and render each separately. This works without conflicts because every Stimulus controller instance gets its own canvas. For accessible charts, a <title> element inside the canvas and a tabular alternative representation are recommended. Symfony UX offers no automatic solution for that, it has to be implemented manually. In the second parameter of render_chart() you can pass HTML attributes: render_chart(chart, {'class': 'w-full h-64'}) sets classes directly on the canvas element.
{# templates/admin/dashboard.html.twig #}
{% extends 'admin/base.html.twig' %}
{% block content %}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 p-6">
{# Revenue bar chart, height controlled by the wrapper div #}
<div class="bg-white rounded-2xl shadow p-6">
<h2 class="text-lg font-bold text-slate-800 mb-4">Revenue last 12 months</h2>
<div style="height: 300px; position: relative;">
{{ render_chart(revenueChart, {'class': 'w-full h-full'}) }}
</div>
</div>
{# Category doughnut chart #}
<div class="bg-white rounded-2xl shadow p-6">
<h2 class="text-lg font-bold text-slate-800 mb-4">Revenue by category</h2>
<div style="height: 300px; position: relative;">
{{ render_chart(categoryChart, {'class': 'w-full h-full'}) }}
</div>
</div>
{# Turbo Frame for a live-updating chart, reloads on user action #}
<turbo-frame id="live-orders-chart" src="{{ path('admin_live_chart') }}">
<div class="bg-white rounded-2xl shadow p-6">
<h2 class="text-lg font-bold text-slate-800 mb-4">Orders today (live)</h2>
{# Content loaded asynchronously via Turbo Frame #}
</div>
</turbo-frame>
</div>
{% endblock %}
{# The chart JavaScript is initialized automatically by the Stimulus controller.
No manual new Chart() call is needed, Symfony UX handles initialization. #}
8. Updating live charts with Turbo Frames
Combining Symfony UX Chart.js with Turbo Frames enables charts that update without a page reload. A Turbo Frame wraps the chart container, a link or timer triggers a fetch request, and Turbo replaces the frame with the new chart. This works because render_chart() renders a canvas with the Stimulus controller, after the frame update, Stimulus initializes the new canvas automatically. No custom JavaScript is needed to destroy the old chart and create a new one.
For period selectors, where the user picks "last 7 days" and the chart updates, you place the selector and the chart in a shared Turbo Frame. The selector link carries the chosen period as a query parameter, the Turbo fetch loads the controller endpoint with the new parameter, and the controller builds a new chart with the current data. The pattern is clear and maintainable: one controller, one Twig template, one frame. No AJAX handler, no JavaScript fetch, no state in JavaScript. Symfony UX Chart.js and Symfony Turbo together solve this common dashboard pattern entirely in PHP and Twig.
9. Symfony UX Chart.js vs. manual implementation
The comparison makes the productivity difference between Symfony UX Chart.js and a manual Chart.js integration clear.
| Task | Manual integration | Symfony UX Chart.js | Benefit |
|---|---|---|---|
| Initializing the chart | new Chart(canvas, config) in JS | render_chart(chart) in Twig | No JavaScript needed |
| Loading data | API endpoint + fetch() in JS | Directly from PHP/Doctrine | No separate API needed |
| Chart update (period) | AJAX + chart.update() in JS | Turbo Frame reload | No JavaScript state |
| Chart.js version | Maintained manually via npm | Managed with the Symfony package | No npm version management |
| Accessibility | Implemented manually | Implemented manually | Both at the same level |
A manual Chart.js integration makes sense when highly complex custom chart types, custom plugins or animations are needed that cannot be configured via the PHP API. For standard dashboard charts with Doctrine data, Symfony UX Chart.js is the clearer solution, less code, fewer layers, simpler debugging.
Mironsoft
Symfony dashboard development, data visualization and Symfony UX integration
A Symfony dashboard with charts, no JavaScript overhead?
We build Symfony dashboards with Symfony UX Chart.js, from Doctrine data aggregation through chart configuration to Turbo Frame integration for live updates without any custom JavaScript.
Dashboard architecture
Chart types, data aggregation and performance optimization for Symfony admin dashboards
Live charts
Turbo Frame integration for dynamic chart updates without AJAX endpoints or JavaScript state
Symfony UX setup
symfony/ux-chartjs, AssetMapper and Stimulus controller integration for existing Symfony projects
10. Summary
Symfony UX Chart.js turns data visualization in Symfony projects into a purely PHP task. The ChartBuilder service creates chart objects from PHP arrays that match the Chart.js API exactly. render_chart() in Twig renders the canvas and initializes Chart.js automatically via the Stimulus controller, no custom JavaScript, no API endpoint, no new Chart(). All Chart.js types, datasets, options and plugins are accessible through the PHP API. Doctrine data flows directly into the chart configuration, with no intermediate JSON API.
The biggest productivity gain lies in the Turbo Frame integration: period selectors, filters and live updates work without JavaScript state management. One frame reload, one new chart, no manual chart.update() call. For PHP teams building Symfony dashboards, Symfony UX Chart.js is the most direct route to maintainable, performant data visualizations, entirely within the familiar PHP Symfony ecosystem.
Symfony UX Chart.js, the essentials at a glance
ChartBuilder
$this->chartBuilder->createChart(Chart::TYPE_BAR) creates a chart object. setData() and setOptions() configure all Chart.js properties as a PHP array.
Twig rendering
{{ render_chart(chart) }} renders the canvas, passes the JSON configuration and initializes Chart.js automatically via Stimulus, one line of Twig is enough.
Doctrine integration
Repository methods aggregate data, array_column() extracts labels and data values. No separate JSON API is needed between Doctrine and Chart.js.
Live updates
A Turbo Frame wraps the chart container, period filters trigger frame reloads, Stimulus initializes the new chart automatically. No JavaScript state.
11. FAQ: Symfony UX Chart.js and Data Visualization
1What is symfony/ux-chartjs?
2Install Chart.js separately?
3Which chart types?
type field. Horizontal bars via indexAxis: 'y'.4Load Doctrine data into a chart?
array_column() extracts labels and values, passed directly to setData(). No JSON API or AJAX needed.5Fully usable Chart.js options?
setOptions() accepts the complete Chart.js options object as a PHP array. All documentation examples translate directly.6Update a chart without a page reload?
7Control the size of the canvas?
style="height: 300px; position: relative;". Canvas with w-full h-full. Options: responsive: true plus maintainAspectRatio: false.8Webpack Encore or AssetMapper?
npm install @symfony/ux-chartjs and an import in the Encore entry are needed.9Multiple charts on one page?
render_chart() calls. Each chart gets its own canvas, Stimulus instances are independent.10Test controllers with charts?
ChartBuilderInterface. Integration tests via WebTestCase can check the canvas element via a CSS selector.