Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Most of these modules have been optimized and made compatible for PrestaShop v9.
| Module name | Description | Minimum version |
|----------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|
| [api_module](https://github.com/PrestaShop/example-modules/tree/master/api_module) | This example module demonstrates how to modify PrestaShop's new API. | v9.0.0 |
| [dashexamplechartjs](https://github.com/PrestaShop/example-modules/tree/master/dashexamplechartjs) | PoC for spike #41970: renders the migrated Symfony dashboard zones with Chart.js instead of D3 v3 / NVD3. | v9.2.0 |
| [demo_grid](https://github.com/PrestaShop/example-modules/tree/master/demo_grid) | This module demonstrates how to use Grid in PrestaShop | v9.0.0 |
| [democonsolecommand](https://github.com/PrestaShop/example-modules/tree/master/democonsolecommand) | Example module showing how to implement a Symfony console command | v9.0.0 |
| [democontrollertabs](https://github.com/PrestaShop/example-modules/tree/master/democontrollertabs) | Demo Creating modern Controllers and associate Tabs to them | v9.0.0 |
Expand Down
3 changes: 3 additions & 0 deletions dashexamplechartjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/vendor/
composer.lock
.idea
63 changes: 63 additions & 0 deletions dashexamplechartjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Dashboard example - Chart.js (`dashexamplechartjs`)

PoC for spike [PrestaShop/PrestaShop#41970](https://github.com/PrestaShop/PrestaShop/issues/41970) — validates
**Chart.js** as the replacement for D3 v3 + NVD3 on the migrated (Symfony) Back Office Dashboard, using the
same integration contract as the reference [`dashexample`](../dashexample) module from
[PrestaShop/PrestaShop#41968](https://github.com/PrestaShop/PrestaShop/issues/41968) /
[#254](https://github.com/PrestaShop/example-modules/pull/254).

## What it demonstrates

- Registers on the new dashboard hooks: `displayAdminDashboardZoneOne`, `displayAdminDashboardZoneTwo`,
`displayAdminDashboardToolbar`. No legacy Smarty/`HelperForm`/`Db::getInstance()`.
- Renders hook content through module Twig templates (`$this->get('twig')`, `@Modules/dashexamplechartjs/...`).
- Consumes the global `Chart` (Chart.js v4.5.1, MIT) provided by **core** as its own independent webpack
bundle (`admin-dev/themes/new-theme/js/vendor/chartjs.ts` → `chartjs.bundle.js`, not merged into the
main BO bundle) — this module no longer vendors Chart.js itself. It still loads its own init JS/CSS from
its hook output, exactly like `dashexample` loads its assets — no `actionAdminControllerSetMedia`, no
`get_class($controller)` detection.
- Covers the 4 chart types the current native widgets need:
- Zone One: doughnut chart → `dashactivity` equivalent (traffic sources).
- Zone Two: line/area chart with a previous-period overlay → `dashtrends` equivalent (sales trend),
plus a bar chart → `dashgoals` equivalent (goals vs. actual).
- Sample/static data only (no Doctrine queries) — this is a rendering/integration PoC, not a full
reimplementation of the native widgets' data layer.

## Requirements

- PrestaShop **9.2.0+**, with core PR [PrestaShop/PrestaShop#42077](https://github.com/PrestaShop/PrestaShop/pull/42077)
(Symfony `DashboardController`, routing, Twig layout, feature flag, 5 new hooks) merged.
- Chart.js loaded by core as its own bundle (`chart.js` npm dependency, dedicated `chartjs` webpack entry,
included from `Dashboard/index.html.twig`'s `javascripts` block) — no toggle yet, deferred to a later change.
- The `dashboard` feature flag enabled (*Advanced Parameters > New & Experimental Features*).

## Install

```bash
php bin/console prestashop:module install dashexamplechartjs
```

Then enable the `dashboard` feature flag and open the Dashboard.

## Known issues on the dependency (PrestaShop/PrestaShop#42077)

Building this PoC surfaced two bugs in the core PR this module depends on (reported there):

- `index.html.twig` is missing `|raw` on `toolbarContent`/`topContent`, so any HTML a module returns from
`displayAdminDashboardToolbar` or `displayAdminDashboardTop` renders escaped instead of executing — this
breaks the toolbar-based asset-loading pattern this module uses (and that `dashexample` uses too). Until
fixed upstream, this module's assets won't actually load.
- The date-range calendar form loses its security token on submit (`method="get"` form whose `action` already
contains `?_token=...`, which gets dropped and rebuilt from the form fields alone), redirecting to
`security/compromised`.

## Chart.js benchmark

Chart.js was selected as the primary recommendation among Chart.js / ApexCharts / Billboard.js: MIT license,
~67.5k★ on GitHub, ~54M downloads/month, ~68 KB gzip, no license risk — over ApexCharts (excluded: proprietary
relicensing in June 2025, revenue-gated + no-redistribution clause) and Billboard.js (heavier bundle, smaller
community, keeps a D3 v7 dependency).

**Known gap surfaced by this PoC**: Chart.js renders on `<canvas>`, which is invisible to screen readers by
default. This PoC adds `role="img"` + `aria-label` on each canvas as a minimal baseline, but a real migration
should budget explicit accessibility work per chart.
17 changes: 17 additions & 0 deletions dashexamplechartjs/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "prestashop/dashexamplechartjs",
"description": "Spike #41970 PoC: renders the migrated Symfony dashboard zones with Chart.js instead of D3 v3 / NVD3.",
"license": "AFL-3.0",
"authors": [
{
"name": "PrestaShop Core Team"
}
],
"require": {
"php": ">=8.1"
},
"config": {
"prepend-autoloader": false
},
"type": "prestashop-module"
}
11 changes: 11 additions & 0 deletions dashexamplechartjs/config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>dashexamplechartjs</name>
<displayName><![CDATA[Dashboard example - Chart.js]]></displayName>
<version><![CDATA[1.0.0]]></version>
<description><![CDATA[Spike #41970 PoC: renders the migrated Symfony dashboard zones with Chart.js instead of D3 v3 / NVD3.]]></description>
<author><![CDATA[PrestaShop]]></author>
<tab><![CDATA[administration]]></tab>
<is_configurable>0</is_configurable>
<need_instance>0</need_instance>
</module>
108 changes: 108 additions & 0 deletions dashexamplechartjs/dashexamplechartjs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License version 3.0
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0
*/

declare(strict_types=1);

use Twig\Environment;

if (!defined('_PS_VERSION_')) {
exit;
}

/**
* Spike #41970 PoC: demonstrates replacing D3 v3 + NVD3 with Chart.js on the migrated
* (Symfony) Back Office Dashboard, through the same dedicated hook family used by the
* `dashexample` module (see PrestaShop/PrestaShop#41968 and PrestaShop/example-modules#254).
*
* Chart.js itself is self-hosted (views/js/lib/chart.umd.min.js, v4.5.1, MIT) and loaded
* once from the toolbar hook, exactly like `dashexample` loads its own assets from there.
*/
class DashExampleChartjs extends Module
{
public function __construct()
{
$this->name = 'dashexamplechartjs';
$this->author = 'PrestaShop';
$this->version = '1.0.0';
$this->ps_versions_compliancy = ['min' => '9.2.0', 'max' => '9.99.99'];
$this->need_instance = 0;

parent::__construct();

$this->displayName = $this->l('Dashboard example - Chart.js');
$this->description = $this->l('Spike #41970 PoC: renders the migrated Symfony dashboard zones with Chart.js instead of D3 v3 / NVD3.');
}

public function install(): bool
{
return parent::install()
&& $this->registerHook([
'displayAdminDashboardZoneOne',
'displayAdminDashboardZoneTwo',
'displayAdminDashboardToolbar',
]);
}

/**
* Zone One: doughnut chart (traffic sources) — the dashactivity-equivalent widget.
*/
public function hookDisplayAdminDashboardZoneOne(array $params): string
{
return $this->render('zone_one.html.twig', [
'dateFrom' => $params['date_from'] ?? null,
'dateTo' => $params['date_to'] ?? null,
]);
}

/**
* Zone Two: line/area chart with a "previous period" overlay (dashtrends-equivalent)
* plus a goals-vs-actual bar chart (dashgoals-equivalent).
*/
public function hookDisplayAdminDashboardZoneTwo(array $params): string
{
return $this->render('zone_two.html.twig', [
'dateFrom' => $params['date_from'] ?? null,
'dateTo' => $params['date_to'] ?? null,
'moduleUri' => $this->getPathUri(),
]);
}

/**
* Loads Chart.js (self-hosted) and this module's own JS/CSS once, from the toolbar hook —
* no `actionAdminControllerSetMedia`, no `get_class($controller)` detection.
*/
public function hookDisplayAdminDashboardToolbar(array $params): string
{
return $this->render('toolbar.html.twig', [
'moduleUri' => $this->getPathUri(),
]);
}

/**
* Render one of this module's admin Twig templates.
*/
private function render(string $template, array $params = []): string
{
/** @var Environment $twig */
$twig = $this->get('twig');

return $twig->render(sprintf('@Modules/%s/views/templates/admin/%s', $this->name, $template), $params);
}
}
29 changes: 29 additions & 0 deletions dashexamplechartjs/views/css/dashexamplechartjs.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0).
* It is also available through the world-wide-web at this URL: https://opensource.org/licenses/AFL-3.0
*/

.dashexamplechartjs-toolbar {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
margin-bottom: 15px;
border-radius: 4px;
background-color: #e6f0ff;
color: #2b7dd8;
font-weight: 600;
}

.dashexamplechartjs-card .panel-heading {
display: flex;
align-items: center;
gap: 6px;
}

.dashexamplechartjs-card canvas {
max-width: 100%;
}
115 changes: 115 additions & 0 deletions dashexamplechartjs/views/js/dashexamplechartjs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0).
* It is also available through the world-wide-web at this URL: https://opensource.org/licenses/AFL-3.0
*/

// Spike #41970 PoC — the module manages its own assets and chart initialization,
// loaded from its hook output (see toolbar.html.twig), not via actionAdminControllerSetMedia.
// Colors below approximate the BO's prestakit design tokens ($primary/$secondary/...);
// a real integration would read the actual computed CSS custom properties instead.
(function () {
var PALETTE = {
primary: '#2b7dd8',
secondary: '#6c757d',
success: '#2ecc71',
info: '#3ea8f5',
warning: '#f4b740',
danger: '#e6544a',
};

function readJson(id) {
var el = document.getElementById(id);
return el ? JSON.parse(el.textContent) : null;
}

function initDoughnut() {
var canvas = document.getElementById('dashexamplechartjs-doughnut');
var data = readJson('dashexamplechartjs-doughnut-data');
if (!canvas || !data || typeof Chart === 'undefined') {
return;
}

new Chart(canvas, {
type: 'doughnut',
data: {
labels: data.labels,
datasets: [{
data: data.values,
backgroundColor: [PALETTE.primary, PALETTE.info, PALETTE.warning, PALETTE.secondary],
}],
},
options: {
plugins: { legend: { position: 'bottom' } },
},
});
}

function initLine() {
var canvas = document.getElementById('dashexamplechartjs-line');
var data = readJson('dashexamplechartjs-line-data');
if (!canvas || !data || typeof Chart === 'undefined') {
return;
}

new Chart(canvas, {
type: 'line',
data: {
labels: data.labels,
datasets: [
{
label: 'Current period',
data: data.current,
borderColor: PALETTE.primary,
backgroundColor: 'rgba(43, 125, 216, 0.15)',
fill: true,
tension: 0.3,
},
{
label: 'Previous period',
data: data.previous,
borderColor: PALETTE.secondary,
borderDash: [6, 4],
fill: false,
tension: 0.3,
},
],
},
options: {
plugins: { legend: { position: 'bottom' } },
scales: { y: { beginAtZero: true } },
},
});
}

function initBar() {
var canvas = document.getElementById('dashexamplechartjs-bar');
var data = readJson('dashexamplechartjs-bar-data');
if (!canvas || !data || typeof Chart === 'undefined') {
return;
}

new Chart(canvas, {
type: 'bar',
data: {
labels: data.labels,
datasets: [
{ label: 'Goal (%)', data: data.goal, backgroundColor: PALETTE.secondary },
{ label: 'Actual (%)', data: data.actual, backgroundColor: PALETTE.success },
],
},
options: {
plugins: { legend: { position: 'bottom' } },
scales: { y: { beginAtZero: true } },
},
});
}

document.addEventListener('DOMContentLoaded', function () {
initDoughnut();
initLine();
initBar();
});
})();
20 changes: 20 additions & 0 deletions dashexamplechartjs/views/templates/admin/toolbar.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{#**
# Copyright since 2007 PrestaShop SA and Contributors
# PrestaShop is an International Registered Trademark & Property of PrestaShop SA
#
# This source file is subject to the Academic Free License 3.0 (AFL-3.0).
# It is also available through the world-wide-web at this URL: https://opensource.org/licenses/AFL-3.0
#}

{% trans_default_domain 'Module.Dashexamplechartjs.Admin' %}

{#
# NOTE (spike #41970 finding): this hook's output is NOT rendered with |raw in core's
# index.html.twig (PR #42077), unlike the zone hooks — so the HTML below is displayed
# escaped as text instead of executing. Assets are therefore loaded from zone_two.html.twig
# instead (see that file). Kept here, unfixed, to document the gap for the core PR.
#}
<div class="dashexamplechartjs-toolbar">
<span class="material-icons">insert_chart</span>
<span>{{ 'Spike #41970 — Chart.js proof of concept active.'|trans }}</span>
</div>
Loading