diff --git a/components/CartePensees.vue b/components/CartePensees.vue index fb402ba..42ade66 100644 --- a/components/CartePensees.vue +++ b/components/CartePensees.vue @@ -1,10 +1,201 @@ - \ No newline at end of file + }) + + // ---- LIENS APPARTENANCE (couche 4) ---- + const gLinks = g.append('g').attr('class', 'links-appartenance') + d3LinkSel = gLinks.selectAll('line').data(links).join('line') + .attr('stroke', 'rgba(150,150,150,0.28)').attr('stroke-width', 1.2) + + // ---- EDGE LABELS - sous-courants (couche 4b) ---- + const subcourantLinks = links.filter((l: any) => l.isSubcourant) + d3EdgeLabelSel = gLinks.selectAll('text.pensees-edge-label') + .data(subcourantLinks) + .join('text') + .attr('class', 'pensees-edge-label') + + // ---- NODES AUTEURS (couche 5) ---- + const gAuteurs = g.append('g').attr('class', 'auteurs') + d3NodeSel = gAuteurs.selectAll('g').data(auteurNodes).join('g') + .style('cursor', (d: any) => d.ingere ? 'pointer' : 'default') + .call(d3.drag() + .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('end', (e: any, d: any) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null })) + .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 + d3NodeSel.append('circle') + .attr('r', (d: any) => d.r) + .attr('fill', (d: any) => d.ingere ? (d.color + 'cc') : '#bbbbbb') + .attr('stroke', (d: any) => d.ingere ? d.color : '#999999') + .attr('stroke-width', 1.5) + .attr('opacity', (d: any) => d.ingere ? 1 : 0.35) + + // ---- LABELS AUTEURS (couche 6 - drop-shadow blanc) ---- + d3NodeSel.append('text') + .attr('class', 'pensees-auteur-label') + .text((d: any) => d.nom.split(' ').pop() ?? d.nom) + .attr('text-anchor', 'middle') + .attr('dy', (d: any) => -(d.r + 4)) + .style('pointer-events', 'none') + .style('opacity', (d: any) => d.ingere ? 1 : 0.3) + .style('fill', (d: any) => d.ingere ? '#1a1a1a' : '#777777') + + d3NodeSel + .on('mouseenter', (e: any, d: any) => { + if (!tooltipRef.value) return + let tooltipHtml = '' + if (d.ingere) { + const rawBio = d.bio_courte || '' + const bio = rawBio.length > 90 ? rawBio.slice(0, 87) + '...' : rawBio + tooltipHtml = `${d.nom} ${d.dates}
${bio || 'Dans le RAG ATIS.'}` + } else { + tooltipHtml = `${d.nom} ${d.dates}
Présent dans Bonpote, pas encore ingéré dans le RAG ATIS.` + } + tooltipRef.value.innerHTML = tooltipHtml + tooltipRef.value.style.opacity = '1' + }) + .on('mousemove', (e: any) => { + if (!tooltipRef.value || !svgEl) return + const rect = (svgEl as HTMLElement).getBoundingClientRect() + tooltipRef.value.style.left = (e.clientX - rect.left + 14) + 'px' + tooltipRef.value.style.top = (e.clientY - rect.top - 10) + 'px' + }) + .on('mouseleave', () => { if (tooltipRef.value) tooltipRef.value.style.opacity = '0' }) + + simulation.on('tick', () => { + d3LinkSel + .attr('x1', (d: any) => d.source.x).attr('y1', (d: any) => d.source.y) + .attr('x2', (d: any) => d.target.x).attr('y2', (d: any) => d.target.y) + + d3EdgeLabelSel + .attr('x', (d: any) => (d.source.x + d.target.x) / 2) + .attr('y', (d: any) => (d.source.y + d.target.y) / 2) + .text((d: any) => { + const targetId = typeof d.target === 'object' ? d.target.id : d.target + return targetId + }) + + d3NodeSel.attr('transform', (d: any) => `translate(${d.x},${d.y})`) + }) +} + +watch(() => props.active, (val) => { + if (val && import.meta.client && props.data) + requestAnimationFrame(() => requestAnimationFrame(() => initGraph())) +}) +watch(() => props.data, (val) => { + if (val && props.active && import.meta.client) + requestAnimationFrame(() => requestAnimationFrame(() => initGraph())) +}) +onMounted(async () => { + if (import.meta.client && props.data && props.active) { + await nextTick() + initGraph() + } +}) +onUnmounted(() => { if (simulation) simulation.stop() }) + +function triggerResize() { + if (simulation) { + simulation.alpha(0.3).restart() + } else if (import.meta.client && props.data && props.active) { + initGraph() + } +} +defineExpose({ triggerResize }) + + +