From 3f2783e3fc15f0d85e49b598cafce4142352e893 Mon Sep 17 00:00:00 2001 From: Jules Neny Date: Mon, 11 May 2026 18:42:06 +0200 Subject: [PATCH] feat(v12-n): Carte O fusion noeud central + palette minimaliste encre/papier/ocre - YAML: fusion 3 noeuds confus (centre + ncs-politique + medecine-corps-social) en 1 seul noeud central 'Contrat social + Medecine du corps social' - Build script: toutes les thematiques rattachees directement au centre (suppression mapping NCS/MDCS), radius central 30px, projets 18px - CarteO.vue palette V1.2: central #0F172A (encre), essais #FFFFFF stroke encre, projets #B45309 (ocre conserve) - Labels: inscrit dans le cercle (blanc) pour central+projets, a droite (encre douce) pour essais - Label central long split sur 2-3 lignes via splitCentralLabel() - Background: #FAFAF7 (papier, raccord colonnes laterales) - Liens: #94A3B8 opacity 0.4 1px 17 nodes / 19 edges. Build SSR 5 pages prerender + server, 0 warning. --- public/data/carte-o-source.yaml | 20 +---- public/data/carte-o.json | 100 +++++++++---------------- scripts/build-carte-o.js | 48 +++--------- src/components/vue/CarteO.vue | 126 +++++++++++++++++++++++++------- 4 files changed, 152 insertions(+), 142 deletions(-) diff --git a/public/data/carte-o-source.yaml b/public/data/carte-o-source.yaml index a70c2ac..3a8dafd 100644 --- a/public/data/carte-o-source.yaml +++ b/public/data/carte-o-source.yaml @@ -8,26 +8,14 @@ version: "1.1" centre: - id: "nouveau-contrat-social" - label: "Nouveau Contrat Social" + id: "contrat-social-medecine-corps-social" + label: "Contrat social + Medecine du corps social" niveau: 0 nature: essai statut: gestation - resume: "Assemblage de tout ce que j'ecris qui s'entrechoque - inventer un nouveau contrat social." + resume: "Manifeste central AEP : inventer un nouveau contrat social et diagnostiquer/soigner les pathologies du corps social." -concepts_force: - - id: "ncs-politique" - label: "Nouveau contrat social" - niveau: 1 - nature: essai - statut: gestation - resume: "L'ecriture politique d'un futur habitable - essai central AEP." - - id: "medecine-corps-social" - label: "Medecine du corps social" - niveau: 1 - nature: essai - statut: gestation - resume: "Diagnostiquer et soigner les pathologies du corps social - concept AEP mdcs." +concepts_force: [] thematiques: - id: "systemique" diff --git a/public/data/carte-o.json b/public/data/carte-o.json index 340867b..f3f6661 100644 --- a/public/data/carte-o.json +++ b/public/data/carte-o.json @@ -1,35 +1,15 @@ { "version": "1.1", - "generatedAt": "2026-05-11T13:04:29.806Z", + "generatedAt": "2026-05-11T16:41:21.600Z", "nodes": [ { - "id": "nouveau-contrat-social", - "label": "Nouveau Contrat Social", + "id": "contrat-social-medecine-corps-social", + "label": "Contrat social + Medecine du corps social", "niveau": 0, "nature": "essai", "statut": "gestation", - "resume": "Assemblage de tout ce que j'ecris qui s'entrechoque - inventer un nouveau contrat social.", - "radius": 28, - "family": "concept" - }, - { - "id": "ncs-politique", - "label": "Nouveau contrat social", - "niveau": 1, - "nature": "essai", - "statut": "gestation", - "resume": "L'ecriture politique d'un futur habitable - essai central AEP.", - "radius": 18, - "family": "concept" - }, - { - "id": "medecine-corps-social", - "label": "Medecine du corps social", - "niveau": 1, - "nature": "essai", - "statut": "gestation", - "resume": "Diagnostiquer et soigner les pathologies du corps social - concept AEP mdcs.", - "radius": 18, + "resume": "Manifeste central AEP : inventer un nouveau contrat social et diagnostiquer/soigner les pathologies du corps social.", + "radius": 30, "family": "concept" }, { @@ -39,7 +19,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -49,7 +29,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -59,7 +39,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -69,7 +49,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -79,7 +59,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -89,7 +69,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -99,7 +79,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -109,7 +89,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -119,7 +99,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -129,7 +109,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -139,7 +119,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -149,7 +129,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -159,7 +139,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -169,7 +149,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -179,7 +159,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -189,77 +169,69 @@ "nature": "projet", "statut": "gestation", "resume": "Transport, mobilite, industrie, politique - projet archi. Exemple de projet archi relie aux thematiques AEP.", - "radius": 14, + "radius": 18, "family": "ressource" } ], "edges": [ { - "source": "nouveau-contrat-social", - "target": "ncs-politique" - }, - { - "source": "nouveau-contrat-social", - "target": "medecine-corps-social" - }, - { - "source": "ncs-politique", + "source": "contrat-social-medecine-corps-social", "target": "systemique" }, { - "source": "ncs-politique", + "source": "contrat-social-medecine-corps-social", "target": "pratiques-collectives" }, { - "source": "medecine-corps-social", + "source": "contrat-social-medecine-corps-social", "target": "art-narration" }, { - "source": "ncs-politique", + "source": "contrat-social-medecine-corps-social", "target": "pouvoir-domination" }, { - "source": "nouveau-contrat-social", + "source": "contrat-social-medecine-corps-social", "target": "medias-critique" }, { - "source": "nouveau-contrat-social", + "source": "contrat-social-medecine-corps-social", "target": "justice-securite" }, { - "source": "medecine-corps-social", + "source": "contrat-social-medecine-corps-social", "target": "sante-globale" }, { - "source": "nouveau-contrat-social", + "source": "contrat-social-medecine-corps-social", "target": "agriculture" }, { - "source": "ncs-politique", + "source": "contrat-social-medecine-corps-social", "target": "post-croissance" }, { - "source": "medecine-corps-social", + "source": "contrat-social-medecine-corps-social", "target": "anthropocene" }, { - "source": "ncs-politique", + "source": "contrat-social-medecine-corps-social", "target": "education" }, { - "source": "nouveau-contrat-social", + "source": "contrat-social-medecine-corps-social", "target": "urbanisme" }, { - "source": "nouveau-contrat-social", + "source": "contrat-social-medecine-corps-social", "target": "geopolitique" }, { - "source": "medecine-corps-social", + "source": "contrat-social-medecine-corps-social", "target": "ia-technologie" }, { - "source": "medecine-corps-social", + "source": "contrat-social-medecine-corps-social", "target": "spiritualite" }, { diff --git a/scripts/build-carte-o.js b/scripts/build-carte-o.js index 170527e..2348dfc 100644 --- a/scripts/build-carte-o.js +++ b/scripts/build-carte-o.js @@ -11,12 +11,12 @@ const REPO_ROOT = path.resolve(__dirname, '..') const SOURCE = path.join(REPO_ROOT, 'public/data/carte-o-source.yaml') const OUTPUT = path.join(REPO_ROOT, 'public/data/carte-o.json') -// radius par niveau + nature +// radius par niveau + nature (V1.2-N palette minimaliste) function getRadius(niveau, nature) { - if (niveau === 0) return 28 - if (niveau === 1) return 18 - if (niveau === 2 && nature === 'projet') return 14 - return 10 + if (niveau === 0) return 30 + if (nature === 'projet') return 18 + if (niveau === 1) return 16 + return 12 } // compat backward : nature -> family @@ -24,30 +24,8 @@ function getFamily(nature) { return nature === 'projet' ? 'ressource' : 'concept' } -// thematiques rattachees directement au centre (ni ncs-politique ni medecine-corps-social) -const CENTRE_THEMATIQUES = new Set([ - 'medias-critique', - 'justice-securite', - 'agriculture', - 'urbanisme', - 'geopolitique', -]) - -const NCS_THEMATIQUES = new Set([ - 'systemique', - 'pratiques-collectives', - 'pouvoir-domination', - 'post-croissance', - 'education', -]) - -const MDCS_THEMATIQUES = new Set([ - 'art-narration', - 'sante-globale', - 'spiritualite', - 'ia-technologie', - 'anthropocene', -]) +// V1.2-N : noeud central fusionne -> toutes les thematiques sont rattachees au centre +// (les anciens groupes NCS_THEMATIQUES / MDCS_THEMATIQUES sont supprimes avec leurs sous-noeuds) async function main() { const raw = await fs.readFile(SOURCE, 'utf-8') @@ -80,20 +58,16 @@ async function main() { const centreId = data.centre.id addNode(data.centre) - for (const cf of data.concepts_force) { + // concepts_force vide en V1.2-N (fusionne dans le centre) + for (const cf of (data.concepts_force || [])) { addNode(cf) addEdge(centreId, cf.id) } + // toutes les thematiques rattachees directement au noeud central for (const th of data.thematiques) { addNode(th) - if (NCS_THEMATIQUES.has(th.id)) { - addEdge('ncs-politique', th.id) - } else if (MDCS_THEMATIQUES.has(th.id)) { - addEdge('medecine-corps-social', th.id) - } else if (CENTRE_THEMATIQUES.has(th.id)) { - addEdge(centreId, th.id) - } + addEdge(centreId, th.id) } for (const proj of data.projets) { diff --git a/src/components/vue/CarteO.vue b/src/components/vue/CarteO.vue index b055fc9..61cfa3e 100644 --- a/src/components/vue/CarteO.vue +++ b/src/components/vue/CarteO.vue @@ -64,32 +64,46 @@ let zoomBehavior: d3.ZoomBehavior | null = null const isMobile = computed(() => width.value < 600) const nodeRadius = computed(() => isMobile.value ? 10 : 14) +// V1.2-N palette : encre / papier / ocre +// - central niveau 0 : fill encre #0F172A, label blanc inscrit dans le cercle +// - essais (niveaux 1-2) : fill papier #FFFFFF, stroke encre, label encre a droite +// - projets : fill ocre #B45309, label blanc inscrit dans le cercle +// - statut "edite" : stroke epaissi (l'epaisseur conserve l'info, pas la couleur) function colorFor(d: SimNode): string { - if (d.nature === 'projet') return '#b45309' - if (d.niveau === 0) return '#1d4ed8' - if (d.niveau === 1) return '#2563eb' - if (d.niveau === 2) return '#60a5fa' - return props.familyColors[d.family] || '#9ca3af' + if (d.nature === 'projet') return '#B45309' + if (d.niveau === 0) return '#0F172A' + return '#FFFFFF' } function getRadius(d: SimNode): number { - return d.radius ?? nodeRadius.value + if (d.radius != null) return d.radius + if (d.niveau === 0) return 30 + if (d.nature === 'projet') return 18 + return nodeRadius.value } function strokeFor(d: SimNode): string { - return d.statut === 'edite' ? '#0f172a' : '#94a3b8' + if (d.nature === 'projet') return '#B45309' + if (d.niveau === 0) return '#0F172A' + return '#0F172A' } function strokeWidthFor(d: SimNode): number { - return d.statut === 'edite' ? 2.5 : 1 + return d.statut === 'edite' ? 2.5 : 1.5 } function labelWeightFor(d: SimNode): string { return d.statut === 'edite' ? 'bold' : 'normal' } +// Label inscrit dans le cercle (fond fonce) pour central + projets +function labelInsideFor(d: SimNode): boolean { + return d.niveau === 0 || d.nature === 'projet' +} + function labelColorFor(d: SimNode): string { - return d.statut === 'edite' ? '#0f172a' : '#6b7280' + if (labelInsideFor(d)) return '#FFFFFF' + return d.statut === 'edite' ? '#0F172A' : '#475569' } function truncate(str: string, max: number): string { @@ -97,6 +111,27 @@ function truncate(str: string, max: number): string { return str.length > max ? str.slice(0, max - 1) + '…' : str } +// Split intelligent du label central sur 2-3 lignes (autour de "+") +function splitCentralLabel(label: string): string[] { + if (!label) return [''] + // priorite : split sur " + " si present + if (label.includes(' + ')) { + const parts = label.split(' + ') + return [parts[0].trim(), '+', parts.slice(1).join(' + ').trim()] + } + // fallback : split a peu pres au milieu sur un espace + if (label.length > 14) { + const mid = Math.floor(label.length / 2) + const left = label.lastIndexOf(' ', mid) + const right = label.indexOf(' ', mid) + const cut = (mid - left <= right - mid && left > 0) ? left : right + if (cut > 0) { + return [label.slice(0, cut).trim(), label.slice(cut).trim()] + } + } + return [label] +} + function buildSimNodes(): SimNode[] { return props.nodes.map(n => ({ ...n })) } @@ -167,14 +202,14 @@ function render() { const fontSize = isMobile.value ? 9 : 11 - // Liens + // Liens — gris doux papier gLinks! .selectAll('line') .data(simLinks) .join('line') - .attr('stroke', '#94a3b8') - .attr('stroke-opacity', 0.45) - .attr('stroke-width', 1.2) + .attr('stroke', '#94A3B8') + .attr('stroke-opacity', 0.4) + .attr('stroke-width', 1) // Noeuds = groupe par node const nodeGroups = gNodes! @@ -202,17 +237,58 @@ function render() { .attr('stroke', d => strokeFor(d)) .attr('stroke-width', d => strokeWidthFor(d)) - // Label - nodeGroups.append('text') - .attr('text-anchor', 'start') - .attr('dominant-baseline', 'central') - .attr('dx', d => getRadius(d) + 4) - .attr('font-size', fontSize) - .attr('font-family', 'system-ui, sans-serif') - .attr('font-weight', d => labelWeightFor(d)) - .attr('fill', d => labelColorFor(d)) - .attr('pointer-events', 'none') - .text(d => truncate(d.label, isMobile.value ? 18 : 30)) + // Label : inscrit dans le cercle pour central + projets, a droite sinon + // Pour le noeud central (label long), on passe en multi-lignes + nodeGroups.each(function (d) { + const g = d3.select(this) + const inside = labelInsideFor(d) + + if (inside && d.niveau === 0) { + // Noeud central : label en 2 lignes inscrites au centre + const parts = splitCentralLabel(d.label) + const lineHeight = isMobile.value ? 10 : 12 + const fs = isMobile.value ? 9 : 11 + const startY = -((parts.length - 1) * lineHeight) / 2 + parts.forEach((line, i) => { + g.append('text') + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'central') + .attr('x', 0) + .attr('y', startY + i * lineHeight) + .attr('font-size', fs) + .attr('font-family', 'system-ui, sans-serif') + .attr('font-weight', 'bold') + .attr('fill', '#FFFFFF') + .attr('pointer-events', 'none') + .text(line) + }) + } else if (inside) { + // Projets : label centre dans le cercle (court) + g.append('text') + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'central') + .attr('x', 0) + .attr('y', 0) + .attr('font-size', fontSize) + .attr('font-family', 'system-ui, sans-serif') + .attr('font-weight', labelWeightFor(d)) + .attr('fill', '#FFFFFF') + .attr('pointer-events', 'none') + .text(truncate(d.label, 6)) + } else { + // Essais : label a droite du cercle + g.append('text') + .attr('text-anchor', 'start') + .attr('dominant-baseline', 'central') + .attr('dx', getRadius(d) + 4) + .attr('font-size', fontSize) + .attr('font-family', 'system-ui, sans-serif') + .attr('font-weight', labelWeightFor(d)) + .attr('fill', labelColorFor(d)) + .attr('pointer-events', 'none') + .text(truncate(d.label, isMobile.value ? 18 : 30)) + } + }) // Tooltip nodeGroups.append('title') @@ -325,7 +401,7 @@ onUnmounted(() => { width: 100%; height: 100%; position: relative; - background: #fafafa; + background: #FAFAF7; overflow: hidden; }