diff --git a/frontend/src/pages/DonutPage.vue b/frontend/src/pages/DonutPage.vue index 59414d0..336c50d 100644 --- a/frontend/src/pages/DonutPage.vue +++ b/frontend/src/pages/DonutPage.vue @@ -66,63 +66,88 @@ const R_FLOOR_INNER = 102 // ↑ (était 78) const R_SOCIAL_LABEL = 88 // ↑ (était 67) — arc de label = 2π·88/12 ≈ 46u const R_CENTER = 55 // ↑ légèrement pour ne pas écraser "LE DONUT" +/** + * Format : chaque entrée est un tableau de string. Un tableau à 1 élément → + * libellé sur une ligne. À 2 éléments → ligne 1 (primaire) + ligne 2 (suite). + * On garde un type uniforme pour simplifier le helper qui les rend. + */ +type Label = readonly string[] + /** 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", +const PLANETARY_BOUNDARIES: readonly Label[] = [ + ['Changement climatique'], + ['Acidification des océans'], + ['Pollution chimique'], + ['Cycles azote & phosphore'], + ['Usage des sols'], + ['Eau douce'], + ['Biodiversité'], + ["Pollution de l'air"], + ["Couche d'ozone"], ] -/** Les 12 dimensions du plancher social de Kate Raworth, placées entre le centre et l'anneau intérieur. */ -const SOCIAL_FOUNDATIONS = [ - 'Alimentation', - 'Santé', - 'Éducation', - 'Revenu & travail', - 'Eau', - 'Énergie', - 'Liens sociaux', - 'Logement', - 'Égalité de genre', - 'Équité sociale', - 'Voix politique', - 'Paix & justice', +/** Les 12 dimensions du plancher social de Raworth. Beaucoup sont sur 2 lignes + * car leur largeur dépasse l'arc disponible à R_SOCIAL_LABEL. */ +const SOCIAL_FOUNDATIONS: readonly Label[] = [ + ['Alimentation'], + ['Santé'], + ['Éducation'], + ['Revenu', 'travail'], + ['Eau'], + ['Énergie'], + ['Liens', 'sociaux'], + ['Logement'], + ['Égalité', 'de genre'], + ['Équité', 'sociale'], + ['Voix', 'politique'], + ['Paix', 'justice'], ] +interface ArcLine { + pathId: string + d: string + text: string +} + /** - * Génère pour chaque label un petit arc de cercle (utilisé par ). - * Le texte épouse alors la courbure de l'anneau au lieu d'être posé en ligne droite. + * Génère pour chaque libellé un (ou plusieurs) petit arc(s) de cercle. + * Le texte épouse alors la courbure de l'anneau via . * - * Astuce de lisibilité : on inverse le sens du path (CW vs CCW) selon la moitié - * du cercle pour que les labels du bas restent lisibles à l'endroit. Sans ça, - * les labels du bas seraient écrits à l'envers ou inversés (lus de droite à gauche). + * Lisibilité haut/bas : on inverse le sens du path (CW vs CCW) selon la moitié + * du cercle. Sans ça, les labels du bas seraient écrits à l'envers. * - * - Moitié haute (angles proches de 0°) : path va de θ-δ à θ+δ, sweep=1 (CW visuellement) - * → direction RIGHT au sommet → texte "left of right" = UP = OUTSIDE - * - Moitié basse (angles proches de 180°) : path va de θ+δ à θ-δ, sweep=0 (CCW visuellement) - * → direction RIGHT au bas → texte "left of right" = UP = vers le CENTRE - * (= lisible à l'endroit pour un lecteur fixe en face de l'écran) + * - Moitié haute : path va de θ-δ à θ+δ, sweep=1 (CW visuellement) + * → direction tangente vers la droite au sommet, "above the path" = OUTSIDE + * - Moitié basse : path va de θ+δ à θ-δ, sweep=0 (CCW visuellement) + * → direction tangente vers la droite au bas (du POV du lecteur fixe en face) * - * @param labels Liste des libellés - * @param radius Rayon de l'arc de chaque label - * @param idPrefix Préfixe pour générer les `id` des paths (chaque path doit être unique en DOM) - * @param halfSpanDeg Demi-largeur angulaire du path (= halfSpan ≈ 90 % du segment alloué au label) + * Multi-lignes : chaque ligne a son propre arc à un rayon décalé. Pour qu'une + * ligne donnée apparaisse toujours "au-dessus" visuellement (que ce soit en + * haut ou en bas du donut), on inverse aussi le décalage radial selon la moitié : + * + * - En haut, ligne 0 = R + offset (loin du centre = haut de l'écran) + * - En bas, ligne 0 = R - offset (proche du centre = haut de l'écran) + * + * @param labels Liste des libellés (chacun = tableau de lignes) + * @param radius Rayon central de l'arc + * @param idPrefix Préfixe pour les `id` des paths (unique en DOM) + * @param halfSpanDeg Demi-largeur angulaire de chaque label + * @param lineSpacing Espacement radial entre 2 lignes (en unités viewBox) */ -function arcLabelPaths(labels: readonly string[], radius: number, idPrefix: string, halfSpanDeg: number) { +function arcLabelPaths( + labels: readonly Label[], + radius: number, + idPrefix: string, + halfSpanDeg: number, + lineSpacing = 9, +): Array<{ key: number; lines: ArcLine[] }> { const n = labels.length const stepDeg = 360 / n - return labels.map((label, i) => { + return labels.map((entry, i) => { const centerDeg = i * stepDeg + stepDeg / 2 const startDeg = centerDeg - halfSpanDeg const endDeg = centerDeg + halfSpanDeg - // Top half ⇨ chemin CW (sweep=1) ; bottom half ⇨ chemin CCW (sweep=0). const normalized = ((centerDeg % 360) + 360) % 360 const isTopHalf = normalized <= 90 || normalized >= 270 @@ -130,11 +155,26 @@ function arcLabelPaths(labels: readonly string[], radius: number, idPrefix: stri const toDeg = isTopHalf ? endDeg : startDeg const sweep = isTopHalf ? 1 : 0 - const [x1, y1] = pt(fromDeg, radius) - const [x2, y2] = pt(toDeg, radius) - const d = `M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${radius} ${radius} 0 0 ${sweep} ${x2.toFixed(2)} ${y2.toFixed(2)}` + const nLines = entry.length + const halfSpan = (nLines - 1) / 2 - return { label, pathId: `${idPrefix}-${i}`, d, key: i } + const lines: ArcLine[] = entry.map((text, lineIdx) => { + // offsetIdx : +halfSpan pour la ligne du haut visuel, -halfSpan pour la ligne du bas + const offsetIdx = isTopHalf + ? halfSpan - lineIdx // top : ligne 0 au plus éloigné du centre + : lineIdx - halfSpan // bot : ligne 0 au plus proche du centre + const r = radius + offsetIdx * lineSpacing + + const [x1, y1] = pt(fromDeg, r) + const [x2, y2] = pt(toDeg, r) + return { + pathId: `${idPrefix}-${i}-${lineIdx}`, + d: `M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${r.toFixed(2)} ${r.toFixed(2)} 0 0 ${sweep} ${x2.toFixed(2)} ${y2.toFixed(2)}`, + text, + } + }) + + return { key: i, lines } }) } @@ -210,6 +250,10 @@ const planetaryArcs = computed(() => arcLabelPaths(PLANETARY_BOUNDARIES, R_PLANE /** Paths d'arc pour les 12 dimensions du plancher social (anneau intérieur). */ const socialArcs = computed(() => arcLabelPaths(SOCIAL_FOUNDATIONS, R_SOCIAL_LABEL, 'sf', 14)) +/** Aplatissement (label × ligne) pour rendre proprement en flat-list dans le SVG. */ +const planetaryArcLines = computed(() => planetaryArcs.value.flatMap(l => l.lines)) +const socialArcLines = computed(() => socialArcs.value.flatMap(l => l.lines)) + interface DotView { project: ProjectListRow x: number @@ -324,14 +368,15 @@ function gotoProject(p: ProjectListRow) { - + @@ -342,11 +387,11 @@ function gotoProject(p: ProjectListRow) { - - {{ p.label }} + + {{ ln.text }} @@ -392,13 +437,15 @@ function gotoProject(p: ProjectListRow) { + - - {{ s.label }} + + {{ ln.text }}