Skip to content

Commit b3521f7

Browse files
author
m.buchhorn-roth
committed
feat(graph): improve readability and relation visibility
- increase corner text sizes (legend + stats) - prevent node and relation labels from being occluded - highlight start node (case_becker) with glow/ring + START tag - highlight selected node and connected links - render relationship labels directly on graph lines
1 parent 3bb37a5 commit b3521f7

1 file changed

Lines changed: 126 additions & 15 deletions

File tree

src/components/RELIEFKnowledgeGraph3D.tsx

Lines changed: 126 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ interface GraphData {
4343
links: GraphLink[]
4444
}
4545

46+
const START_NODE_ID = 'case_becker'
47+
4648
// ────────────────────────────────────────────
4749
// Color palette per node type
4850
// ────────────────────────────────────────────
@@ -349,6 +351,17 @@ export function RELIEFKnowledgeGraph3D() {
349351

350352
const graphData = useMemo(() => buildCaseData(), [])
351353

354+
const getNodeId = useCallback((ref: string | { id: string }) => {
355+
return typeof ref === 'string' ? ref : ref.id
356+
}, [])
357+
358+
const isLinkConnectedToSelected = useCallback((link: GraphLink) => {
359+
if (!selectedNode) return false
360+
const src = getNodeId(link.source)
361+
const tgt = getNodeId(link.target)
362+
return src === selectedNode.id || tgt === selectedNode.id
363+
}, [getNodeId, selectedNode])
364+
352365
useEffect(() => {
353366
const updateSize = () => {
354367
if (containerRef.current) {
@@ -429,6 +442,8 @@ export function RELIEFKnowledgeGraph3D() {
429442

430443
const nodeThreeObject = useCallback((node: GraphNode) => {
431444
const group = new THREE.Group()
445+
const isSelected = selectedNode?.id === node.id
446+
const isStartNode = node.id === START_NODE_ID
432447
const color = NODE_COLORS[node.type] || '#999'
433448
const size = node.type === 'case' ? 11
434449
: node.type === 'law' ? 10
@@ -449,6 +464,39 @@ export function RELIEFKnowledgeGraph3D() {
449464
const sphere = new THREE.Mesh(geometry, material)
450465
group.add(sphere)
451466

467+
if (isStartNode) {
468+
const startGlowGeo = new THREE.SphereGeometry(size * 1.75, 28, 28)
469+
const startGlowMat = new THREE.MeshPhongMaterial({
470+
color: new THREE.Color('#facc15'),
471+
transparent: true,
472+
opacity: 0.22,
473+
shininess: 90,
474+
})
475+
group.add(new THREE.Mesh(startGlowGeo, startGlowMat))
476+
477+
const startRingGeo = new THREE.TorusGeometry(size * 1.45, 0.9, 16, 64)
478+
const startRingMat = new THREE.MeshBasicMaterial({ color: new THREE.Color('#fde047') })
479+
const startRing = new THREE.Mesh(startRingGeo, startRingMat)
480+
startRing.rotation.x = Math.PI / 2
481+
group.add(startRing)
482+
}
483+
484+
if (isSelected) {
485+
const selectedRingGeo = new THREE.TorusGeometry(size * 1.65, 0.8, 16, 64)
486+
const selectedRingMat = new THREE.MeshBasicMaterial({ color: new THREE.Color('#ffffff') })
487+
const selectedRing = new THREE.Mesh(selectedRingGeo, selectedRingMat)
488+
selectedRing.rotation.x = Math.PI / 2
489+
group.add(selectedRing)
490+
491+
const selectedHaloGeo = new THREE.SphereGeometry(size * 1.95, 24, 24)
492+
const selectedHaloMat = new THREE.MeshPhongMaterial({
493+
color: new THREE.Color('#ffffff'),
494+
transparent: true,
495+
opacity: 0.12,
496+
})
497+
group.add(new THREE.Mesh(selectedHaloGeo, selectedHaloMat))
498+
}
499+
452500
// Glow effect for case, law, and ai nodes
453501
if (node.type === 'law' || node.type === 'case' || node.type === 'ai') {
454502
const glowGeo = new THREE.SphereGeometry(size * 1.4, 24, 24)
@@ -462,17 +510,41 @@ export function RELIEFKnowledgeGraph3D() {
462510

463511
const label = new SpriteText(node.label) as any
464512
label.color = '#e2e8f0'
465-
label.textHeight = node.type === 'case' ? 5.5 : node.type === 'law' ? 5 : node.type === 'ai' ? 4.5 : node.type === 'person' ? 4 : 3.5
466-
label.backgroundColor = 'rgba(15, 23, 42, 0.75)'
467-
label.padding = [2, 4]
513+
label.textHeight = node.type === 'case' ? 7 : node.type === 'law' ? 6.5 : node.type === 'ai' ? 6 : node.type === 'person' ? 5.5 : 5
514+
label.backgroundColor = isSelected ? 'rgba(15, 23, 42, 0.95)' : 'rgba(15, 23, 42, 0.88)'
515+
label.padding = [3.5, 6]
468516
label.borderRadius = 3
469-
label.position.y = -(size + 6)
517+
label.position.y = -(size + 9)
518+
if (label.material) {
519+
label.material.depthTest = false
520+
label.material.depthWrite = false
521+
}
522+
label.renderOrder = 1000
470523
group.add(label)
471524

525+
if (isStartNode) {
526+
const startTag = new SpriteText('START') as any
527+
startTag.color = '#0f172a'
528+
startTag.textHeight = 4.2
529+
startTag.backgroundColor = 'rgba(250, 204, 21, 0.98)'
530+
startTag.padding = [2.5, 5]
531+
startTag.borderRadius = 3
532+
startTag.position.y = size + 8
533+
if (startTag.material) {
534+
startTag.material.depthTest = false
535+
startTag.material.depthWrite = false
536+
}
537+
startTag.renderOrder = 1001
538+
group.add(startTag)
539+
}
540+
472541
return group
473-
}, [])
542+
}, [selectedNode])
474543

475544
const linkColor = useCallback((link: GraphLink) => {
545+
if (selectedNode && !isLinkConnectedToSelected(link)) {
546+
return 'rgba(100, 116, 139, 0.08)'
547+
}
476548
switch (link.type) {
477549
case 'SR_CONTAINS': return 'rgba(59, 130, 246, 0.5)'
478550
case 'SR_REALIZED_BY': return 'rgba(16, 185, 129, 0.5)'
@@ -496,14 +568,51 @@ export function RELIEFKnowledgeGraph3D() {
496568
case 'SR_APPLIES_TO': return 'rgba(6, 182, 212, 0.4)'
497569
default: return 'rgba(148, 163, 184, 0.3)'
498570
}
499-
}, [])
571+
}, [isLinkConnectedToSelected, selectedNode])
572+
573+
const linkWidth = useCallback((link: GraphLink) => {
574+
if (!selectedNode) return 1.8
575+
return isLinkConnectedToSelected(link) ? 3.8 : 0.5
576+
}, [isLinkConnectedToSelected, selectedNode])
500577

501578
const linkDirectionalParticles = useCallback((link: GraphLink) => {
579+
if (selectedNode && !isLinkConnectedToSelected(link)) return 0
502580
return link.type === 'SR_SEQUENCE' ? 3
503581
: link.type === 'SR_LOEST' ? 3
504582
: link.type === 'SR_FEHLT' ? 2
505583
: link.type === 'SR_HAT_PROBLEM' ? 2
506584
: 1
585+
}, [isLinkConnectedToSelected, selectedNode])
586+
587+
const linkThreeObject = useCallback((link: GraphLink) => {
588+
const showLabel = !selectedNode || isLinkConnectedToSelected(link)
589+
if (!showLabel) return null
590+
591+
const relationText = link.description && link.description.trim().length > 0
592+
? link.description
593+
: link.type.replace('SR_', '').replace(/_/g, ' ')
594+
595+
const label = new SpriteText(relationText) as any
596+
label.color = '#f8fafc'
597+
label.textHeight = selectedNode ? 3.2 : 2.4
598+
label.backgroundColor = selectedNode ? 'rgba(15, 23, 42, 0.96)' : 'rgba(15, 23, 42, 0.72)'
599+
label.padding = selectedNode ? [1.8, 3.6] : [1.2, 2.6]
600+
label.borderRadius = 2
601+
if (label.material) {
602+
label.material.depthTest = false
603+
label.material.depthWrite = false
604+
}
605+
label.renderOrder = 1000
606+
return label
607+
}, [isLinkConnectedToSelected, selectedNode])
608+
609+
const linkPositionUpdate = useCallback((sprite: THREE.Object3D, coords: { start: { x: number; y: number; z: number }; end: { x: number; y: number; z: number } }) => {
610+
const middlePos = {
611+
x: coords.start.x + (coords.end.x - coords.start.x) * 0.5,
612+
y: coords.start.y + (coords.end.y - coords.start.y) * 0.5,
613+
z: coords.start.z + (coords.end.z - coords.start.z) * 0.5,
614+
}
615+
Object.assign(sprite.position, middlePos)
507616
}, [])
508617

509618
// Build adjacency for detail panel
@@ -555,27 +664,27 @@ export function RELIEFKnowledgeGraph3D() {
555664
</div>
556665

557666
{/* Legend */}
558-
<div className="absolute bottom-3 left-3 z-20 bg-slate-900/90 backdrop-blur-sm rounded-lg p-3 border border-slate-700">
559-
<div className="text-xs text-slate-400 font-semibold mb-2">Knotentypen</div>
667+
<div className="absolute bottom-3 left-3 z-20 bg-slate-900/90 backdrop-blur-sm rounded-lg p-4 border border-slate-700">
668+
<div className="text-sm text-slate-200 font-semibold mb-2">Knotentypen</div>
560669
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
561670
{(Object.keys(NODE_LABELS) as NodeType[]).map(type => (
562671
<div key={type} className="flex items-center gap-1.5">
563672
<span
564-
className="w-2.5 h-2.5 rounded-full flex-shrink-0"
673+
className="w-3 h-3 rounded-full flex-shrink-0"
565674
style={{ backgroundColor: NODE_COLORS[type] }}
566675
/>
567-
<span className="text-[10px] text-slate-300 whitespace-nowrap">{NODE_LABELS[type]}</span>
676+
<span className="text-xs text-slate-200 whitespace-nowrap">{NODE_LABELS[type]}</span>
568677
</div>
569678
))}
570679
</div>
571680
</div>
572681

573682
{/* Stats */}
574-
<div className="absolute top-3 left-3 z-20 bg-slate-900/90 backdrop-blur-sm rounded-lg px-3 py-2 border border-slate-700">
575-
<div className="text-[10px] text-slate-400">
683+
<div className="absolute top-3 left-3 z-20 bg-slate-900/90 backdrop-blur-sm rounded-lg px-4 py-3 border border-slate-700">
684+
<div className="text-sm text-slate-200">
576685
{graphData.nodes.length} Knoten · {graphData.links.length} Beziehungen
577686
</div>
578-
<div className="text-[10px] text-blue-400 font-medium">RELIEF E-AKTE Knowledge Graph</div>
687+
<div className="text-sm text-blue-300 font-semibold">RELIEF E-AKTE Knowledge Graph</div>
579688
</div>
580689

581690
{/* Graph */}
@@ -587,9 +696,11 @@ export function RELIEFKnowledgeGraph3D() {
587696
backgroundColor="#0f172a"
588697
nodeThreeObject={nodeThreeObject}
589698
onNodeClick={handleNodeClick}
699+
nodeThreeObjectExtend={true}
590700
linkColor={linkColor}
591-
linkWidth={1.5}
592-
linkOpacity={0.7}
701+
linkWidth={linkWidth}
702+
linkThreeObject={linkThreeObject}
703+
linkPositionUpdate={linkPositionUpdate}
593704
linkDirectionalArrowLength={4}
594705
linkDirectionalArrowRelPos={0.85}
595706
linkDirectionalParticles={linkDirectionalParticles}

0 commit comments

Comments
 (0)