feat(donut): texte en arc de cercle + 12 dimensions du plancher social
Limites planétaires + dimensions sociales : labels qui épousent vraiment la
courbure de l'anneau au lieu d'être posés en ligne droite tangente. Implémenté
avec SVG <textPath> + un <path> caché par label.
Petit piège : pour qu'un label en bas du donut reste lisible (pas écrit à
l'envers ou de droite à gauche), il faut INVERSER la direction du path selon
la moitié.
Helper arcLabelPaths() :
- Moitié haute (angles ~0°) : path va de θ-δ vers θ+δ, sweep=1
- Moitié basse (angles ~180°) : path va de θ+δ vers θ-δ, sweep=0
→ la direction du chemin au point central du label est toujours "horizontale
à gauche-droite" du point de vue d'un lecteur fixe.
Ajouts :
- 12 dimensions du plancher social Raworth (Alimentation, Santé, Éducation,
Revenu & travail, Eau, Énergie, Liens sociaux, Logement, Égalité de genre,
Équité sociale, Voix politique, Paix & justice) placées en arc entre le
centre et l'anneau intérieur (rayon 67)
- R_CENTER passé de 56 à 50 pour libérer 6 unités au profit des labels
sociaux sans toucher au "LE DONUT / X projets" au centre
- Suppression des textes centrés "PLAFOND ÉCOLOGIQUE" et "PLANCHER SOCIAL"
sur les anneaux (redondants avec les labels qui les détaillent)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,7 +58,8 @@ const R_CEILING_OUTER = 225
|
|||||||
const R_CEILING_INNER = 200
|
const R_CEILING_INNER = 200
|
||||||
const R_FLOOR_OUTER = 100
|
const R_FLOOR_OUTER = 100
|
||||||
const R_FLOOR_INNER = 78
|
const R_FLOOR_INNER = 78
|
||||||
const R_CENTER = 56
|
const R_SOCIAL_LABEL = 67 // étiquettes des dimensions du plancher social (intérieur)
|
||||||
|
const R_CENTER = 50
|
||||||
|
|
||||||
/** Les 9 limites planétaires de Kate Raworth, placées autour de l'anneau extérieur. */
|
/** Les 9 limites planétaires de Kate Raworth, placées autour de l'anneau extérieur. */
|
||||||
const PLANETARY_BOUNDARIES = [
|
const PLANETARY_BOUNDARIES = [
|
||||||
@@ -73,6 +74,65 @@ const PLANETARY_BOUNDARIES = [
|
|||||||
"Couche d'ozone",
|
"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',
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère pour chaque label un petit arc de cercle (utilisé par <textPath>).
|
||||||
|
* Le texte épouse alors la courbure de l'anneau au lieu d'être posé en ligne droite.
|
||||||
|
*
|
||||||
|
* 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).
|
||||||
|
*
|
||||||
|
* - 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)
|
||||||
|
*
|
||||||
|
* @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)
|
||||||
|
*/
|
||||||
|
function arcLabelPaths(labels: readonly string[], radius: number, idPrefix: string, halfSpanDeg: number) {
|
||||||
|
const n = labels.length
|
||||||
|
const stepDeg = 360 / n
|
||||||
|
return labels.map((label, 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
|
||||||
|
|
||||||
|
const fromDeg = isTopHalf ? startDeg : endDeg
|
||||||
|
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)}`
|
||||||
|
|
||||||
|
return { label, pathId: `${idPrefix}-${i}`, d, key: i }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** deg → rad avec offset -90° (0° en haut). */
|
/** deg → rad avec offset -90° (0° en haut). */
|
||||||
function rad(deg: number): number {
|
function rad(deg: number): number {
|
||||||
return (deg - 90) * Math.PI / 180
|
return (deg - 90) * Math.PI / 180
|
||||||
@@ -139,27 +199,11 @@ const categoryLabels = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Étiquettes des 9 limites planétaires, autour de l'anneau extérieur.
|
/** Paths d'arc pour les 9 limites planétaires (anneau extérieur). */
|
||||||
*
|
const planetaryArcs = computed(() => arcLabelPaths(PLANETARY_BOUNDARIES, R_PLANET_LABEL, 'pb', 18))
|
||||||
* 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).
|
/** Paths d'arc pour les 12 dimensions du plancher social (anneau intérieur). */
|
||||||
*
|
const socialArcs = computed(() => arcLabelPaths(SOCIAL_FOUNDATIONS, R_SOCIAL_LABEL, 'sf', 14))
|
||||||
* - 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 {
|
interface DotView {
|
||||||
project: ProjectListRow
|
project: ProjectListRow
|
||||||
@@ -274,22 +318,31 @@ function gotoProject(p: ProjectListRow) {
|
|||||||
<rect width="6" height="6" fill="#ECECE7" />
|
<rect width="6" height="6" fill="#ECECE7" />
|
||||||
<line x1="0" y1="0" x2="0" y2="6" stroke="#D1D5DB" stroke-width="0.9" />
|
<line x1="0" y1="0" x2="0" y2="6" stroke="#D1D5DB" stroke-width="0.9" />
|
||||||
</pattern>
|
</pattern>
|
||||||
|
<!-- Arcs invisibles pour textPath : un par limite planétaire et par dimension sociale -->
|
||||||
|
<path
|
||||||
|
v-for="p in planetaryArcs" :key="p.pathId"
|
||||||
|
:id="p.pathId" :d="p.d" fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
v-for="s in socialArcs" :key="s.pathId"
|
||||||
|
:id="s.pathId" :d="s.d" fill="none"
|
||||||
|
/>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<!-- 1) ZONE D'OVERSHOOT (au-delà du plafond) -->
|
<!-- 1) ZONE D'OVERSHOOT (au-delà du plafond) -->
|
||||||
<!-- fond cream clair pour soutenir les labels limites planétaires -->
|
<!-- fond cream clair pour soutenir les labels limites planétaires -->
|
||||||
<circle r="270" fill="#F5F2EA" />
|
<circle r="270" fill="#F5F2EA" />
|
||||||
|
|
||||||
<!-- 2) Étiquettes des 9 LIMITES PLANÉTAIRES, autour de l'anneau extérieur -->
|
<!-- 2) Étiquettes des 9 LIMITES PLANÉTAIRES, le long de leur arc -->
|
||||||
<g>
|
<g>
|
||||||
<g
|
<text
|
||||||
v-for="p in planetaryLabels" :key="p.key"
|
v-for="p in planetaryArcs" :key="`pb-text-${p.key}`"
|
||||||
:transform="`translate(${p.x.toFixed(2)} ${p.y.toFixed(2)}) rotate(${p.rotation.toFixed(2)})`"
|
font-size="8" fill="#16A34A" font-weight="600"
|
||||||
>
|
>
|
||||||
<text text-anchor="middle" font-size="8" fill="#22C55E" font-weight="600">
|
<textPath :href="`#${p.pathId}`" startOffset="50%" text-anchor="middle">
|
||||||
{{ p.label }}
|
{{ p.label }}
|
||||||
</text>
|
</textPath>
|
||||||
</g>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- 3) Anneau extérieur PLAFOND ÉCOLOGIQUE (gris strié) -->
|
<!-- 3) Anneau extérieur PLAFOND ÉCOLOGIQUE (gris strié) -->
|
||||||
@@ -311,7 +364,7 @@ function gotoProject(p: ProjectListRow) {
|
|||||||
<circle :r="R_FLOOR_OUTER" fill="url(#ring-stripes)" />
|
<circle :r="R_FLOOR_OUTER" fill="url(#ring-stripes)" />
|
||||||
<circle :r="R_FLOOR_INNER" fill="#FAFAF6" />
|
<circle :r="R_FLOOR_INNER" fill="#FAFAF6" />
|
||||||
|
|
||||||
<!-- 6) Séparateurs radiaux dotted entre secteurs -->
|
<!-- 6) Séparateurs radiaux entre secteurs -->
|
||||||
<g stroke="#FFFFFF" stroke-width="1.5">
|
<g stroke="#FFFFFF" stroke-width="1.5">
|
||||||
<line
|
<line
|
||||||
v-for="d in wedgeDividers" :key="d.key"
|
v-for="d in wedgeDividers" :key="d.key"
|
||||||
@@ -332,11 +385,17 @@ function gotoProject(p: ProjectListRow) {
|
|||||||
>{{ l.cat.name }}</text>
|
>{{ l.cat.name }}</text>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- 8) Labels des anneaux -->
|
<!-- 8) Étiquettes des 12 DIMENSIONS DU PLANCHER SOCIAL, le long de leur arc -->
|
||||||
<text x="0" :y="-(R_CEILING_OUTER + 30)" text-anchor="middle" font-size="9" fill="#16A34A" font-weight="700"
|
<g>
|
||||||
style="letter-spacing:1.5px">PLAFOND ÉCOLOGIQUE</text>
|
<text
|
||||||
<text x="0" :y="-(R_FLOOR_INNER - 5)" text-anchor="middle" font-size="8" fill="#EA580C" font-weight="700"
|
v-for="s in socialArcs" :key="`sf-text-${s.key}`"
|
||||||
style="letter-spacing:1.5px">PLANCHER SOCIAL</text>
|
font-size="6" fill="#EA580C" font-weight="600"
|
||||||
|
>
|
||||||
|
<textPath :href="`#${s.pathId}`" startOffset="50%" text-anchor="middle">
|
||||||
|
{{ s.label }}
|
||||||
|
</textPath>
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
<!-- 9) Points-projets : la classe `dot-circle` corrige le transform-origin -->
|
<!-- 9) Points-projets : la classe `dot-circle` corrige le transform-origin -->
|
||||||
<g>
|
<g>
|
||||||
|
|||||||
Reference in New Issue
Block a user