From af169bccc90f389080180fd60927e125d8385ad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Tue, 19 May 2026 19:37:52 +0200 Subject: [PATCH] feat : graphique trafic area chart lisse avec axes (style analytics) --- public/assets/js/admin-stats.js | 122 +++++++++++++++++++++++++------- 1 file changed, 95 insertions(+), 27 deletions(-) diff --git a/public/assets/js/admin-stats.js b/public/assets/js/admin-stats.js index 99ed4db..a0c0214 100644 --- a/public/assets/js/admin-stats.js +++ b/public/assets/js/admin-stats.js @@ -54,41 +54,109 @@ var trendEl = document.getElementById('stats-trend-container'); if (!trendEl || !totals.length) { return; } - var days = totals.length; - var maxV = Math.max.apply(null, totals) || 1; - var W = 1000; // viewBox, s'adapte en CSS - var H = 80; - var padX = 4; - var padY = 8; - var barW = Math.floor((W - 2 * padX) / days) - 2; + var n = totals.length; + var VW = 900, VH = 160; + var ml = 44, mr = 12, mt = 12, mb = 28; // marges pour axes + var W = VW - ml - mr; + var H = VH - mt - mb; - // Dates des jours (index 0 = il y a 13 jours) - var now = new Date(); + // Échelle Y — plafond arrondi à un multiple "propre" + var rawMax = Math.max.apply(null, totals) || 1; + var mag = Math.pow(10, Math.floor(Math.log(rawMax) / Math.LN10)); + var maxV = Math.ceil(rawMax / mag) * mag; + var nTicks = 4; + + // Dates + var now = new Date(); var labels = totals.map(function (_, i) { var d = new Date(now); - d.setDate(d.getDate() - (days - 1 - i)); + d.setDate(d.getDate() - (n - 1 - i)); return d.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }); }); - var bars = totals.map(function (v, i) { - var x = padX + i * (W - 2 * padX) / days + 1; - var bh = Math.max(2, (v / maxV) * (H - padY - 16)); - var y = H - padY - bh; - var lx = x + barW / 2; - var label = labels[i]; - var showLabel = (i === 0 || i === days - 1 || i === Math.floor(days / 2)); - return '' - + '' + label + ' : ' + v + ' vis.' - + (showLabel - ? '' + label + '' - : ''); + // Coordonnées des points + var pts = totals.map(function (v, i) { + return { + x: ml + i * W / (n - 1), + y: mt + H - (v / maxV) * H, + v: v, + l: labels[i] + }; + }); + + // Courbe bezier lisse (tension 0.35) + function smoothPath(points) { + var d = 'M ' + points[0].x.toFixed(1) + ' ' + points[0].y.toFixed(1); + for (var i = 0; i < points.length - 1; i++) { + var p0 = points[i > 0 ? i - 1 : i]; + var p1 = points[i]; + var p2 = points[i + 1]; + var p3 = points[i + 2 < points.length ? i + 2 : i + 1]; + var t = 0.35; + var cp1x = p1.x + t * (p2.x - p0.x) / 2; + var cp1y = p1.y + t * (p2.y - p0.y) / 2; + var cp2x = p2.x - t * (p3.x - p1.x) / 2; + var cp2y = p2.y - t * (p3.y - p1.y) / 2; + d += ' C ' + cp1x.toFixed(1) + ' ' + cp1y.toFixed(1) + + ', ' + cp2x.toFixed(1) + ' ' + cp2y.toFixed(1) + + ', ' + p2.x.toFixed(1) + ' ' + p2.y.toFixed(1); + } + return d; + } + + var linePath = smoothPath(pts); + var areaPath = linePath + + ' L ' + pts[n - 1].x.toFixed(1) + ' ' + (mt + H) + + ' L ' + pts[0].x.toFixed(1) + ' ' + (mt + H) + ' Z'; + + // Grille horizontale + labels Y + var grid = '', yLabels = ''; + for (var t = 0; t <= nTicks; t++) { + var val = Math.round(maxV * t / nTicks); + var gy = (mt + H - (val / maxV) * H).toFixed(1); + grid += ''; + yLabels += '' + val + ''; + } + + // Labels X (toutes les 2 dates + dernière) + var xLabels = ''; + pts.forEach(function (p, i) { + if (i % 2 === 0 || i === n - 1) { + xLabels += '' + p.l + ''; + } + }); + + // Points interactifs (cercle invisible + tooltip natif) + var dots = pts.map(function (p) { + return '' + + '' + esc(p.l) + ' : ' + p.v + ' vis.' + + '' + + ''; }).join(''); - trendEl.innerHTML = '

Trafic total — 14 derniers jours

' - + '' + bars + ''; + trendEl.innerHTML = + '

Trafic total — 14 derniers jours

' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + grid + + '' + + '' + + dots + + yLabels + + xLabels + + ''; } fetch('/trending?period=14d')