From 8dd9cffaed3b9368e574dbed92fdbc2e67cdf6e3 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Thu, 28 May 2026 22:10:37 +0200 Subject: [PATCH] =?UTF-8?q?fix(donut):=20hover=20anti-jitter=20+=20couleur?= =?UTF-8?q?s=20cat=C3=A9gories=20+=20limites=20plan=C3=A9taires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Jitter au survol (bug visible) : `transform: scale(1.5)` en SVG prend pour origine le (0,0) du viewBox, pas le centre du . Le scale "déplaçait" donc le cercle vers le centre de la viewport → la souris n'était plus dessus → mouseleave → retour → mouseenter → re-déplacé. Boucle infinie de tremblement. Fix : classe `.dot-circle` avec transform-box: fill-box; transform-origin: center; Le scale prend désormais pour pivot le centre de la forme. Stable. 2. Couleurs des secteurs thématiques (par la maquette d'origine) : Réintroduction des wedges colorés dans la zone sûre, opacité 10 % sur le fill + 20 % sur le stroke. Reste sobre, garde la lisibilité. 3. Limites planétaires (de Kate Raworth) : Les 9 limites planétaires (Changement climatique, Acidification des océans, Biodiversité, Eau douce, Cycles N&P, Pollution chimique, Pollution de l'air, Couche d'ozone, Usage des sols) sont placées en étiquettes tangentes autour de l'anneau extérieur, avec flip 180° en moitié basse pour rester lisibles. Cosmétique : - viewBox 560×560 (au lieu de 520) pour caser les labels extérieurs - Labels catégorie déplacés AU MILIEU de leur wedge (sur la zone sûre) plutôt qu'à l'extérieur (où ils chevauchaient les limites planétaires) - Séparateurs radiaux passés en blanc pour mieux contraster avec les wedges colorés Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/pages/DonutPage.vue | 160 ++++++++++++++++++++++++++----- 1 file changed, 137 insertions(+), 23 deletions(-) diff --git a/frontend/src/pages/DonutPage.vue b/frontend/src/pages/DonutPage.vue index 204ecd8..328d6df 100644 --- a/frontend/src/pages/DonutPage.vue +++ b/frontend/src/pages/DonutPage.vue @@ -52,13 +52,27 @@ async function load() { } onMounted(load) -// --- Géométrie (en unités du viewBox -260..260) --- -const R_CEILING_OUTER = 230 +// --- Géométrie (en unités du viewBox) --- +const R_PLANET_LABEL = 252 // étiquettes des limites planétaires (extérieur) +const R_CEILING_OUTER = 225 const R_CEILING_INNER = 200 const R_FLOOR_OUTER = 100 const R_FLOOR_INNER = 78 const R_CENTER = 56 +/** Les 9 limites planétaires de Kate Raworth, placées autour de l'anneau extérieur. */ +const PLANETARY_BOUNDARIES = [ + 'Changement climatique', + 'Acidification des océans', + 'Pollution chimique', + 'Cycles azote & phosphore', + 'Usage des sols', + 'Eau douce', + 'Biodiversité', + "Pollution de l'air", + "Couche d'ozone", +] + /** deg → rad avec offset -90° (0° en haut). */ function rad(deg: number): number { return (deg - 90) * Math.PI / 180 @@ -75,6 +89,33 @@ function strHash(s: string): number { return Math.abs(h) } +/** Construit le path SVG d'un anneau-secteur (donut wedge) entre 2 rayons. */ +function wedgePath(startDeg: number, endDeg: number, rInner: number, rOuter: number): string { + const [x1, y1] = pt(startDeg, rOuter) + const [x2, y2] = pt(endDeg, rOuter) + const [x3, y3] = pt(endDeg, rInner) + const [x4, y4] = pt(startDeg, rInner) + const largeArc = endDeg - startDeg > 180 ? 1 : 0 + return [ + `M ${x1.toFixed(2)} ${y1.toFixed(2)}`, + `A ${rOuter} ${rOuter} 0 ${largeArc} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}`, + `L ${x3.toFixed(2)} ${y3.toFixed(2)}`, + `A ${rInner} ${rInner} 0 ${largeArc} 0 ${x4.toFixed(2)} ${y4.toFixed(2)}`, + 'Z', + ].join(' ') +} + +/** Secteurs colorés (tint subtil) pour chaque catégorie, dans la zone sûre. */ +const thematicWedges = computed(() => { + const n = categories.value.length || 6 + const step = 360 / n + return categories.value.map((cat, i) => ({ + cat, + path: wedgePath(i * step, (i + 1) * step, R_FLOOR_OUTER, R_CEILING_INNER), + key: cat.id, + })) +}) + /** Lignes radiales de séparation des secteurs (entre plancher et plafond). */ const wedgeDividers = computed(() => { const n = categories.value.length || 6 @@ -87,17 +128,39 @@ const wedgeDividers = computed(() => { }) }) -/** Position du label catégorie (à l'extérieur du plafond). */ +/** Position du label catégorie (juste sous l'anneau écologique, sur la zone sûre). */ const categoryLabels = computed(() => { const n = categories.value.length || 6 const step = 360 / n return categories.value.map((cat, i) => { const center = i * step + step / 2 - const [x, y] = pt(center, R_CEILING_OUTER + 18) + const [x, y] = pt(center, (R_CEILING_INNER + R_FLOOR_OUTER) / 2) return { cat, x, y } }) }) +/** Étiquettes des 9 limites planétaires, autour de l'anneau extérieur. + * + * Texte tangent à la circonférence + flip 180° dans la moitié inférieure + * pour rester lisible (sinon le label serait à l'envers en bas du donut). + * + * - angle 0 (haut) : rotation 0 → texte horizontal lisible + * - angle 90 (droite) : rotation 90 → texte vertical lisible de haut en bas + * - angle 180 (bas) : rotation 180+180 = 0 → texte horizontal lisible + * - angle 270 (gauche) : rotation 270 → texte vertical lisible de bas en haut + */ +const planetaryLabels = computed(() => { + const n = PLANETARY_BOUNDARIES.length + const step = 360 / n + return PLANETARY_BOUNDARIES.map((label, i) => { + const angle = i * step + step / 2 + const [x, y] = pt(angle, R_PLANET_LABEL) + let rotation = angle + if (angle > 90 && angle < 270) rotation += 180 + return { label, x, y, rotation, key: i } + }) +}) + interface DotView { project: ProjectListRow x: number @@ -204,49 +267,78 @@ function gotoProject(p: ProjectListRow) {
- + + - - + + - + + + + + + + + + {{ p.label }} + + + + + - + + + + + + - - + + - + {{ l.cat.name }} - - PLAFOND ÉCOLOGIQUE - PLANCHER SOCIAL + + PLAFOND ÉCOLOGIQUE + PLANCHER SOCIAL - + - + LE DONUT @@ -360,4 +452,26 @@ function gotoProject(p: ProjectListRow) { .fade-enter-from, .fade-leave-to { opacity: 0; } + +/* Fix anti-jitter sur les points-projets. + * + * Sans ces 2 propriétés, `transform: scale()` en SVG prend pour origine le (0,0) + * du viewBox -> le cercle scalé est déplacé vers le centre, la souris n'est plus + * dessus, mouseleave -> retour à la position d'origine -> mouseenter -> re-déplacé. + * Boucle infinie de jitter. + * + * `transform-box: fill-box` change le référentiel à la bounding box de la forme, + * et `transform-origin: center` met l'origine au centre de cette box. + * Le scale se fait alors "sur place". + */ +.dot-circle { + cursor: pointer; + transform-box: fill-box; + transform-origin: center; + transition: transform 0.15s ease; +} +.dot-circle:hover, +.dot-circle.is-hover { + transform: scale(1.6); +}