feat(v11-c): carte-o rendering refonte niveau/nature/statut + contextmenu positionne

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jules Neny
2026-05-11 15:13:46 +02:00
parent 1e1c56db2f
commit d8d3af28a0
4 changed files with 192 additions and 244 deletions

View File

@@ -10,6 +10,11 @@ interface CarteNode {
id: string
label: string
family: string
niveau?: number
nature?: 'essai' | 'projet'
statut?: 'gestation' | 'edite'
resume?: string | null
radius?: number
intention?: string
slug?: string
theme?: string
@@ -36,7 +41,7 @@ const props = withDefaults(defineProps<{
})
const emit = defineEmits<{
'node-click': [node: CarteNode]
'node-click': [payload: { node: CarteNode; x: number; y: number }]
}>()
// Refs
@@ -59,8 +64,32 @@ let zoomBehavior: d3.ZoomBehavior<SVGSVGElement, unknown> | null = null
const isMobile = computed(() => width.value < 600)
const nodeRadius = computed(() => isMobile.value ? 10 : 14)
function colorFor(family: string): string {
return props.familyColors[family] || '#9ca3af'
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'
}
function getRadius(d: SimNode): number {
return d.radius ?? nodeRadius.value
}
function strokeFor(d: SimNode): string {
return d.statut === 'edite' ? '#0f172a' : '#94a3b8'
}
function strokeWidthFor(d: SimNode): number {
return d.statut === 'edite' ? 2.5 : 1
}
function labelWeightFor(d: SimNode): string {
return d.statut === 'edite' ? 'bold' : 'normal'
}
function labelColorFor(d: SimNode): string {
return d.statut === 'edite' ? '#0f172a' : '#6b7280'
}
function truncate(str: string, max: number): string {
@@ -136,7 +165,6 @@ function render() {
const simNodes = buildSimNodes()
const simLinks = buildSimLinks(simNodes)
const r = nodeRadius.value
const fontSize = isMobile.value ? 9 : 11
// Liens
@@ -155,49 +183,55 @@ function render() {
.join('g')
.attr('class', 'node')
.style('cursor', 'pointer')
.on('click', (_event, d) => emit('node-click', d))
.on('mouseover', function () {
.on('click', (_event, d) => emit('node-click', { node: d as CarteNode, x: (d as SimNode).x || 0, y: (d as SimNode).y || 0 }))
.on('mouseover', function (_event, d) {
d3.select(this).select('circle')
.transition().duration(120)
.attr('stroke-width', 2.5)
.attr('stroke-width', strokeWidthFor(d) + 1.5)
})
.on('mouseout', function () {
.on('mouseout', function (_event, d) {
d3.select(this).select('circle')
.transition().duration(120)
.attr('stroke-width', 1.5)
.attr('stroke-width', strokeWidthFor(d))
})
// Cercle (couleur famille)
// Cercle
nodeGroups.append('circle')
.attr('r', r)
.attr('fill', d => colorFor(d.family))
.attr('stroke', '#ffffff')
.attr('stroke-width', 1.5)
.attr('r', d => getRadius(d))
.attr('fill', d => colorFor(d))
.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', r + 4)
.attr('dx', d => getRadius(d) + 4)
.attr('font-size', fontSize)
.attr('font-family', 'system-ui, sans-serif')
.attr('fill', '#1f2937')
.attr('font-weight', d => labelWeightFor(d))
.attr('fill', d => labelColorFor(d))
.attr('pointer-events', 'none')
.text(d => truncate(d.label, isMobile.value ? 18 : 30))
// Tooltip <title>
nodeGroups.append('title')
.text(d => `${d.label}\n[${d.family}]\n${truncate(d.intention || '', 200)}`)
.text(d => `${d.label}\n[${d.family}]\n${truncate(d.resume || d.intention || '', 200)}`)
// Simulation force
// Simulation force avec charges differenciees par niveau
simulation = d3.forceSimulation<SimNode, SimLink>(simNodes)
.force('link', d3.forceLink<SimNode, SimLink>(simLinks)
.id(d => d.id)
.distance(80)
.strength(0.35))
.force('charge', d3.forceManyBody<SimNode>().strength(-220))
.force('charge', d3.forceManyBody<SimNode>().strength(d => {
if (d.niveau === 0) return -800
if (d.niveau === 1) return -400
if (d.niveau === 2) return -150
return -220
}))
.force('center', d3.forceCenter(width.value / 2, height.value / 2))
.force('collide', d3.forceCollide<SimNode>().radius(r + 6))
.force('collide', d3.forceCollide<SimNode>().radius(d => getRadius(d) + 6))
.alphaDecay(0.03)
.on('tick', tick)