feat(media): transposition 1:1 Bonpote V2 + Voronoi blur + grisage (Phase 8.D)

- Positions x_hint/y_hint repos depuis OCR vision Sonnet sur PDF Bonpote V2
- Couleurs ecoles pastel Bonpote-aligned (10 clusters)
- Labels Bonpote V2 longs : Ecologies libertaires + Ecologies anti-industrielles
  (ids JSON eco-anarchisme/technocritique inchanges, compat code)
- CSS .voronoi-bg filter:blur(10px) + labels separes sur calque non-blurre
- Grisage auteurs ingere:false : #bbb opacity 0.35 non-cliquables
- Tooltip non-ingeres : "Present dans Bonpote, pas encore ingere dans le RAG ATIS."
- D3 sim ajustee pour 171 auteurs : linkDistance 85, charge -30, forceXY 0.15
- corpusCount = auteurs ingeres uniquement (32, pas 171 total)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jules Neny
2026-05-12 17:57:30 +02:00
parent fdd9d02859
commit 89608d894c
3 changed files with 83 additions and 53 deletions

View File

@@ -14,7 +14,7 @@
<script setup lang="ts"> <script setup lang="ts">
interface EcoleData { id: string; label: string; description: string; color: string; x_hint: number; y_hint: number } interface EcoleData { id: string; label: string; description: string; color: string; x_hint: number; y_hint: number }
interface LivreRag { slug: string; titre: string; annee: number; couches: string[] } interface LivreRag { slug: string; titre: string; annee: number; couches: string[] }
interface AuteurData { id: string; nom: string; dates: string; ecoles: string[]; ecole_principale: string; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string } interface AuteurData { id: string; nom: string; dates: string; ecoles: string[]; ecole_principale: string; ingere: boolean; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string; bio_courte_provisoire?: string }
interface PenseesData { ecoles: EcoleData[]; auteurs: AuteurData[] } interface PenseesData { ecoles: EcoleData[]; auteurs: AuteurData[] }
// Liens d'influence inter-ecoles (Phase 7 - matrice de filiation) // Liens d'influence inter-ecoles (Phase 7 - matrice de filiation)
@@ -79,8 +79,11 @@ async function initGraph() {
const delaunay = Delaunay.from(points) const delaunay = Delaunay.from(points)
const voronoi = delaunay.voronoi([0, 0, W, H]) const voronoi = delaunay.voronoi([0, 0, W, H])
// Groupe Voronoi (fond, couche 1) // Groupe Voronoi : separation Phase 8.D
// - gVoronoi : cells colorees, BLURRED via CSS .voronoi-bg
// - gVoronoiLabels : labels ecoles, NOT blurred (lisibilite 17px)
const gVoronoi = g.append('g').attr('class', 'voronoi-bg') const gVoronoi = g.append('g').attr('class', 'voronoi-bg')
const gVoronoiLabels = g.append('g').attr('class', 'voronoi-labels')
ecolesArr.forEach((ecole, i) => { ecolesArr.forEach((ecole, i) => {
const cellPath = voronoi.renderCell(i) const cellPath = voronoi.renderCell(i)
@@ -105,12 +108,12 @@ async function initGraph() {
}) })
.on('mouseleave', () => { if (tooltipRef.value) tooltipRef.value.style.opacity = '0' }) .on('mouseleave', () => { if (tooltipRef.value) tooltipRef.value.style.opacity = '0' })
// Label ecole dans la cellule (centroid du polygone) // Label ecole dans la cellule (centroid du polygone) - calque non-blurre
if (poly && poly.length > 0) { if (poly && poly.length > 0) {
const centroid = d3.polygonCentroid(poly as [number, number][]) const centroid = d3.polygonCentroid(poly as [number, number][])
if (centroid && !isNaN(centroid[0]) && !isNaN(centroid[1])) { if (centroid && !isNaN(centroid[0]) && !isNaN(centroid[1])) {
const words = ecole.label.split(' ') const words = ecole.label.split(' ')
const labelEl = gVoronoi.append('text') const labelEl = gVoronoiLabels.append('text')
.attr('class', 'voronoi-cell-label') .attr('class', 'voronoi-cell-label')
.attr('x', centroid[0]) .attr('x', centroid[0])
.attr('y', centroid[1]) .attr('y', centroid[1])
@@ -171,7 +174,10 @@ async function initGraph() {
const ecole = ecoleMap.get(a.ecole_principale) const ecole = ecoleMap.get(a.ecole_principale)
const jitter = () => (Math.random() - 0.5) * 80 const jitter = () => (Math.random() - 0.5) * 80
return { return {
id: a.id, type: 'auteur', nom: a.nom, dates: a.dates, bio_courte: a.bio_courte, id: a.id, type: 'auteur', nom: a.nom, dates: a.dates,
bio_courte: a.bio_courte,
bio_provisoire: a.bio_courte_provisoire ?? '',
ingere: a.ingere,
ecole_principale: a.ecole_principale, ecole_principale: a.ecole_principale,
color: ecole?.color ?? '#888', r: 11, color: ecole?.color ?? '#888', r: 11,
x: W * (ecole?.x_hint ?? 0.5) + jitter(), x: W * (ecole?.x_hint ?? 0.5) + jitter(),
@@ -198,25 +204,26 @@ async function initGraph() {
const allNodes = [...ecoleFixedNodes, ...auteurNodes] const allNodes = [...ecoleFixedNodes, ...auteurNodes]
if (simulation) simulation.stop() if (simulation) simulation.stop()
// Phase 8.D : sim ajustee pour 171 auteurs (vs 28 v2.1, densite 6x)
simulation = d3.forceSimulation(allNodes) simulation = d3.forceSimulation(allNodes)
.force('link', d3.forceLink(links).id((d: any) => d.id).distance(110).strength((d: any) => d.strength ?? 0.5)) .force('link', d3.forceLink(links).id((d: any) => d.id).distance(85).strength((d: any) => d.strength ?? 0.5))
.force('charge', d3.forceManyBody().strength(-45)) .force('charge', d3.forceManyBody().strength(-30))
.force('center', d3.forceCenter(W / 2, H / 2).strength(0.02)) .force('center', d3.forceCenter(W / 2, H / 2).strength(0.02))
.force('collision', d3.forceCollide().radius((d: any) => d.type === 'auteur' ? 14 : 0)) .force('collision', d3.forceCollide().radius((d: any) => d.type === 'auteur' ? 12 : 0))
.force('forceX', d3.forceX<any>((d: any) => { .force('forceX', d3.forceX<any>((d: any) => {
if (d.type === 'auteur') { if (d.type === 'auteur') {
const pos = ecolePositions.get(d.ecole_principale) const pos = ecolePositions.get(d.ecole_principale)
return pos ? pos.tx : W / 2 return pos ? pos.tx : W / 2
} }
return W / 2 return W / 2
}).strength(0.12)) }).strength(0.15))
.force('forceY', d3.forceY<any>((d: any) => { .force('forceY', d3.forceY<any>((d: any) => {
if (d.type === 'auteur') { if (d.type === 'auteur') {
const pos = ecolePositions.get(d.ecole_principale) const pos = ecolePositions.get(d.ecole_principale)
return pos ? pos.ty : H / 2 return pos ? pos.ty : H / 2
} }
return H / 2 return H / 2
}).strength(0.12)) }).strength(0.15))
// ---- LIENS APPARTENANCE (couche 4) ---- // ---- LIENS APPARTENANCE (couche 4) ----
const gLinks = g.append('g').attr('class', 'links-appartenance') const gLinks = g.append('g').attr('class', 'links-appartenance')
@@ -234,18 +241,24 @@ async function initGraph() {
// ---- NODES AUTEURS (couche 5) ---- // ---- NODES AUTEURS (couche 5) ----
const gAuteurs = g.append('g').attr('class', 'auteurs') const gAuteurs = g.append('g').attr('class', 'auteurs')
d3NodeSel = gAuteurs.selectAll('g').data(auteurNodes).join('g') d3NodeSel = gAuteurs.selectAll('g').data(auteurNodes).join('g')
.style('cursor', 'pointer') .style('cursor', (d: any) => d.ingere ? 'pointer' : 'default')
.call(d3.drag<any, any>() .call(d3.drag<any, any>()
.on('start', (e: any, d: any) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y }) .on('start', (e: any, d: any) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y })
.on('drag', (e: any, d: any) => { d.fx = e.x; d.fy = e.y }) .on('drag', (e: any, d: any) => { d.fx = e.x; d.fy = e.y })
.on('end', (e: any, d: any) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null })) .on('end', (e: any, d: any) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null }))
.on('click', (e: any, d: any) => { e.stopPropagation(); emit('select-auteur', d.id) }) .on('click', (e: any, d: any) => {
if (!d.ingere) return
e.stopPropagation()
emit('select-auteur', d.id)
})
// Phase 8.D : grisage conditionnel auteurs non-ingeres (ingere:false)
d3NodeSel.append('circle') d3NodeSel.append('circle')
.attr('r', (d: any) => d.r) .attr('r', (d: any) => d.r)
.attr('fill', (d: any) => d.color + 'cc') .attr('fill', (d: any) => d.ingere ? (d.color + 'cc') : '#bbbbbb')
.attr('stroke', (d: any) => d.color) .attr('stroke', (d: any) => d.ingere ? d.color : '#999999')
.attr('stroke-width', 1.5) .attr('stroke-width', 1.5)
.attr('opacity', (d: any) => d.ingere ? 1 : 0.35)
// ---- LABELS AUTEURS (couche 6 - fix 7.1 : drop-shadow blanc) ---- // ---- LABELS AUTEURS (couche 6 - fix 7.1 : drop-shadow blanc) ----
d3NodeSel.append('text') d3NodeSel.append('text')
@@ -258,8 +271,14 @@ async function initGraph() {
d3NodeSel d3NodeSel
.on('mouseenter', (e: any, d: any) => { .on('mouseenter', (e: any, d: any) => {
if (!tooltipRef.value) return if (!tooltipRef.value) return
const bio = d.bio_courte.length > 90 ? d.bio_courte.slice(0, 87) + '...' : d.bio_courte let tooltipHtml = ''
tooltipRef.value.innerHTML = `<strong>${d.nom}</strong> <span style="opacity:0.6;font-size:0.7rem;">${d.dates}</span><br><span style="opacity:0.75;font-size:0.72rem;">${bio}</span>` if (d.ingere) {
const bio = d.bio_courte.length > 90 ? d.bio_courte.slice(0, 87) + '...' : d.bio_courte
tooltipHtml = `<strong>${d.nom}</strong> <span style="opacity:0.6;font-size:0.7rem;">${d.dates}</span><br><span style="opacity:0.75;font-size:0.72rem;">${bio}</span>`
} else {
tooltipHtml = `<strong>${d.nom}</strong> <span style="opacity:0.6;font-size:0.7rem;">${d.dates}</span><br><span style="opacity:0.65;font-size:0.72rem;font-style:italic;">Présent dans Bonpote, pas encore ingéré dans le RAG ATIS.</span>`
}
tooltipRef.value.innerHTML = tooltipHtml
tooltipRef.value.style.opacity = '1' tooltipRef.value.style.opacity = '1'
}) })
.on('mousemove', (e: any) => { .on('mousemove', (e: any) => {
@@ -325,14 +344,23 @@ defineExpose({ triggerResize })
pointer-events: none; pointer-events: none;
} }
/* ---- Voronoi cellules ---- */ /* ---- Voronoi cellules (Phase 8.D : blur 10px aquarelle Bonpote) ---- */
.voronoi-bg {
filter: blur(10px);
opacity: 0.65;
}
.voronoi-cell { .voronoi-cell {
stroke: rgba(255,255,255,0.4); stroke: rgba(255, 255, 255, 0.3);
stroke-width: 1.5px; stroke-width: 1px;
cursor: default; cursor: default;
} }
/* ---- Labels ecoles dans cellules Voronoi ---- */ /* ---- Labels ecoles : calque separe NON-blurre (Phase 8.D) ---- */
.voronoi-labels {
pointer-events: none;
}
.voronoi-cell-label { .voronoi-cell-label {
fill: rgba(40,40,40,0.52); fill: rgba(40,40,40,0.52);
font-size: 17px; font-size: 17px;

View File

@@ -143,7 +143,8 @@ const splitRatio = ref(DEFAULT_SPLIT_RATIO)
const carteFlexBasis = computed(() => `${splitRatio.value * 100}%`) const carteFlexBasis = computed(() => `${splitRatio.value * 100}%`)
const chatbotFlexBasis = computed(() => `${(1 - splitRatio.value) * 100}%`) const chatbotFlexBasis = computed(() => `${(1 - splitRatio.value) * 100}%`)
const corpusCount = computed(() => penseesData.value?.auteurs.length ?? 0) // Phase 8.D : compteur = auteurs ingere:true uniquement (32 reels, pas 171 total)
const corpusCount = computed(() => penseesData.value?.auteurs.filter(a => a.ingere).length ?? 0)
// Logique poignee draggable // Logique poignee draggable
let dragStartY = 0 let dragStartY = 0

View File

@@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"version": "3.0", "version": "3.1",
"source": "FRACAS Bonpote V2 oct 2024 + LightRAG corpus 12/05/2026 (v3.0 sync)", "source": "FRACAS Bonpote V2 oct 2024 + LightRAG corpus 12/05/2026 (v3.0 sync)",
"corpus_ingere": 141, "corpus_ingere": 141,
"auteurs_count": 171, "auteurs_count": 171,
@@ -10,80 +10,81 @@
"note_v2_1": "Phase 6 refonte Bonpote-aligned : 11 ecoles (fusion Marxismes-ecologiques -> Ecosocialisme, Marx+Saito migres), palette pastel, positions x_hint/y_hint Bonpote-aligned, labels affiches renommes (eco-anarchisme -> Eco-anarchisme, technocritique, ethique-env singulier)", "note_v2_1": "Phase 6 refonte Bonpote-aligned : 11 ecoles (fusion Marxismes-ecologiques -> Ecosocialisme, Marx+Saito migres), palette pastel, positions x_hint/y_hint Bonpote-aligned, labels affiches renommes (eco-anarchisme -> Eco-anarchisme, technocritique, ethique-env singulier)",
"updated": "2026-05-12", "updated": "2026-05-12",
"auteurs_ingeres_count": 32, "auteurs_ingeres_count": 32,
"note_v3_0": "Phase 8.A sync corpus unifie : ~140 auteurs Bonpote integres, flag ingere:true/false selon LightRAG VPS. Auteurs non-ingeres = entrees minimales (bio provisoire, livres_rag vide), a enrichir lors de PRG-4/PRG-5." "note_v3_0": "Phase 8.A sync corpus unifie : ~140 auteurs Bonpote integres, flag ingere:true/false selon LightRAG VPS. Auteurs non-ingeres = entrees minimales (bio provisoire, livres_rag vide), a enrichir lors de PRG-4/PRG-5.",
"note_v3_1": "Phase 8.D transposition 1:1 Bonpote V2 : positions+couleurs Bonpote-aligned via OCR vision Sonnet, blur Voronoi, grisage auteurs non-ingeres"
}, },
"ecoles": [ "ecoles": [
{ {
"id": "ecosocialisme", "id": "ecosocialisme",
"label": "Écosocialisme", "label": "Écosocialisme",
"description": "Synthèse du marxisme et de l'écologie. Articule la critique du capitalisme et la crise écologique comme deux faces d'un même système. Inclut Marx, Saito, Gorz, Klein, Malm, Löwy.", "description": "Synthèse du marxisme et de l'écologie. Articule la critique du capitalisme et la crise écologique comme deux faces d'un même système. Inclut Marx, Saito, Gorz, Klein, Malm, Löwy.",
"color": "#d99c9c", "color": "#e0a8a8",
"x_hint": 0.45, "x_hint": 0.5,
"y_hint": 0.2 "y_hint": 0.22
}, },
{ {
"id": "eco-anarchisme", "id": "eco-anarchisme",
"label": "Éco-anarchisme", "label": "Écologies libertaires",
"description": "Filiation des traditions du socialisme ouvrier anglais et de l'anarchisme. Les dominations de l'homme sur l'homme, sur la femme et sur la nature ne peuvent être prises séparément. Éco-communautés, institutions autogérées, démocratie radicale, municipalisme libertaire.", "description": "Filiation des traditions du socialisme ouvrier anglais et de l'anarchisme. Les dominations de l'homme sur l'homme, sur la femme et sur la nature ne peuvent être prises séparément. Éco-communautés, institutions autogérées, démocratie radicale, municipalisme libertaire.",
"color": "#a3c4a8", "color": "#a8c8b8",
"x_hint": 0.2, "x_hint": 0.22,
"y_hint": 0.25 "y_hint": 0.14
}, },
{ {
"id": "decroissance", "id": "decroissance",
"label": "Décroissance", "label": "Décroissance",
"description": "Critique radicale de la croissance économique comme horizon. Pour une réduction volontaire de la production et de la consommation. Inclut la collapsologie (Servigne, Diamond, Randers) comme sous-courant.", "description": "Critique radicale de la croissance économique comme horizon. Pour une réduction volontaire de la production et de la consommation. Inclut la collapsologie (Servigne, Diamond, Randers) comme sous-courant.",
"color": "#ecc09c", "color": "#d8d0c0",
"x_hint": 0.42, "x_hint": 0.38,
"y_hint": 0.45 "y_hint": 0.45
}, },
{ {
"id": "ecofeminismes", "id": "ecofeminismes",
"label": "Écoféminismes", "label": "Écoféminismes",
"description": "Connexions entre la domination des femmes et la domination de la nature. Féminisme de la subsistance, critique du développement, commons.", "description": "Connexions entre la domination des femmes et la domination de la nature. Féminisme de la subsistance, critique du développement, commons.",
"color": "#e8b9a8", "color": "#d8b8d0",
"x_hint": 0.32, "x_hint": 0.5,
"y_hint": 0.65 "y_hint": 0.75
}, },
{ {
"id": "technocritique", "id": "technocritique",
"label": "Technocritique", "label": "Écologies anti-industrielles",
"description": "Rejet du productivisme et de l'hyper-mécanisation issus de l'ère industrielle. Approche technocritique : critique du gigantisme productif et de l'État, refus de l'idéologie du Progrès. Considérer la technique comme un système avec ses logiques propres.", "description": "Rejet du productivisme et de l'hyper-mécanisation issus de l'ère industrielle. Approche technocritique : critique du gigantisme productif et de l'État, refus de l'idéologie du Progrès. Considérer la technique comme un système avec ses logiques propres.",
"color": "#b8bfbf", "color": "#c8c8c8",
"x_hint": 0.18, "x_hint": 0.14,
"y_hint": 0.5 "y_hint": 0.48
}, },
{ {
"id": "ecologies-decoloniales", "id": "ecologies-decoloniales",
"label": "Écologies décoloniales", "label": "Écologies décoloniales",
"description": "Articulation des luttes écologiques et des luttes anticoloniales. Critique de l'extractivisme comme continuation du colonialisme.", "description": "Articulation des luttes écologiques et des luttes anticoloniales. Critique de l'extractivisme comme continuation du colonialisme.",
"color": "#d8a691", "color": "#b8c8c8",
"x_hint": 0.2, "x_hint": 0.2,
"y_hint": 0.78 "y_hint": 0.82
}, },
{ {
"id": "ethiques-environnementales", "id": "ethiques-environnementales",
"label": "Éthique environnementale", "label": "Éthique environnementale",
"description": "Philosophies de la nature : deep ecology, écocentrisme, droits des non-humains. Valeur intrinsèque du vivant.", "description": "Philosophies de la nature : deep ecology, écocentrisme, droits des non-humains. Valeur intrinsèque du vivant.",
"color": "#9cc4bf", "color": "#d8c8a8",
"x_hint": 0.72, "x_hint": 0.8,
"y_hint": 0.62 "y_hint": 0.82
}, },
{ {
"id": "pensees-vivant", "id": "pensees-vivant",
"label": "Pensées du vivant", "label": "Pensées du vivant",
"description": "Anthropologie et ontologies de la nature. Dépasser le dualisme nature/culture. Sympoïèse, multi-espèces, éthologie politique.", "description": "Anthropologie et ontologies de la nature. Dépasser le dualisme nature/culture. Sympoïèse, multi-espèces, éthologie politique.",
"color": "#b5c9b6", "color": "#c8d8c0",
"x_hint": 0.45, "x_hint": 0.56,
"y_hint": 0.7 "y_hint": 0.58
}, },
{ {
"id": "capitalisme-vert", "id": "capitalisme-vert",
"label": "Capitalisme vert", "label": "Capitalisme vert",
"description": "Théoriciens du capitalisme qui intègrent la dimension environnementale aux échanges marchands (taxes, compensation, technologies vertes). Certains accélèrent la dynamique capitaliste, voulant contrôler le Système-Terre sans nuire aux intérêts de la classe possédante. Famille critiquée par toutes les autres.", "description": "Théoriciens du capitalisme qui intègrent la dimension environnementale aux échanges marchands (taxes, compensation, technologies vertes). Certains accélèrent la dynamique capitaliste, voulant contrôler le Système-Terre sans nuire aux intérêts de la classe possédante. Famille critiquée par toutes les autres.",
"color": "#c4d1c4", "color": "#b8d8d8",
"x_hint": 0.8, "x_hint": 0.8,
"y_hint": 0.25, "y_hint": 0.18,
"corpus_status": "non_ingere", "corpus_status": "non_ingere",
"note_editoriale": "Famille intégrée pour fidélité à la carte FRACAS Bonpote. Pas d'auteurs ingérés dans le RAG ATIS (critique éditoriale assumée)." "note_editoriale": "Famille intégrée pour fidélité à la carte FRACAS Bonpote. Pas d'auteurs ingérés dans le RAG ATIS (critique éditoriale assumée)."
}, },
@@ -91,9 +92,9 @@
"id": "ecofascismes", "id": "ecofascismes",
"label": "Écofascismes", "label": "Écofascismes",
"description": "Émergés à bas bruit depuis les années 1980, fragmentés. En Europe : éco-différentialisme, séparation des « races »/civilisations adaptées à leur environnement. Aux USA : néo-malthusianisme, xénophobie, apologie de la wilderness, logiques survivalistes. Famille critiquée par toutes les autres.", "description": "Émergés à bas bruit depuis les années 1980, fragmentés. En Europe : éco-différentialisme, séparation des « races »/civilisations adaptées à leur environnement. Aux USA : néo-malthusianisme, xénophobie, apologie de la wilderness, logiques survivalistes. Famille critiquée par toutes les autres.",
"color": "#a89890", "color": "#d8b0a8",
"x_hint": 0.85, "x_hint": 0.8,
"y_hint": 0.82, "y_hint": 0.52,
"corpus_status": "non_ingere", "corpus_status": "non_ingere",
"note_editoriale": "Famille intégrée pour fidélité à la carte FRACAS Bonpote. Pas d'auteurs ingérés dans le RAG ATIS (critique éditoriale assumée)." "note_editoriale": "Famille intégrée pour fidélité à la carte FRACAS Bonpote. Pas d'auteurs ingérés dans le RAG ATIS (critique éditoriale assumée)."
} }
@@ -3062,4 +3063,4 @@
"ingere": false "ingere": false
} }
] ]
} }