Politur: Gedaechnis-Graph um Quadranten-Clustering und forceCollide-Kollisionstuning erweitert (vollstaendige Entzerrung und farbige, gut sichtbare Kanten)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import ForceGraph2D from "react-force-graph-2d"
|
||||
import { forceCollide, forceX, forceY } from "d3-force"
|
||||
import { Trash2, Sparkles, Share2, RefreshCw } from "lucide-react"
|
||||
import { type MemoryGraph } from "@/lib/api"
|
||||
|
||||
@@ -10,9 +11,16 @@ const CAT_LABEL: Record<string, string> = {
|
||||
identity: "Identität", knowledge: "Wissen", rules: "Regeln", events: "Ereignisse",
|
||||
}
|
||||
const AUTO = new Set(["auto", "agent", "hermes"])
|
||||
const DIM_EDGE = "rgba(15, 23, 42, 0.01)"
|
||||
const EDGE = "rgba(255, 255, 255, 0.05)"
|
||||
const EDGE_HI = "rgba(99, 102, 241, 0.85)"
|
||||
const DIM_EDGE = "rgba(255, 255, 255, 0.02)"
|
||||
|
||||
// Hilfsfunktion zur Konvertierung von Hex in RGBA für farbkodierte Verbindungen
|
||||
function hexToRgba(hex: string, alpha: number): string {
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
||||
}
|
||||
|
||||
export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id: string) => void }) {
|
||||
const [selected, setSelected] = useState<string | null>(null)
|
||||
@@ -60,17 +68,51 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
|
||||
return d
|
||||
}, [data])
|
||||
|
||||
// D3 Force Engine tunen (Luftige Struktur, kein Überlappen)
|
||||
// Quadranten-Fokuskoordinaten für sauberes Clustering
|
||||
const getClusterX = (category: string) => {
|
||||
return {
|
||||
identity: -160,
|
||||
knowledge: 160,
|
||||
rules: -160,
|
||||
events: 160,
|
||||
}[category] || 0
|
||||
}
|
||||
|
||||
const getClusterY = (category: string) => {
|
||||
return {
|
||||
identity: -160,
|
||||
knowledge: -160,
|
||||
rules: 160,
|
||||
events: 160,
|
||||
}[category] || 0
|
||||
}
|
||||
|
||||
// D3 Force Engine tunen (Luftige Struktur, kein Überlappen, Clustering)
|
||||
useEffect(() => {
|
||||
const fg = fgRef.current
|
||||
if (!fg) return
|
||||
|
||||
// Sicherstellen, dass D3-Kräfte existieren, bevor wir aufgerufen werden
|
||||
const charge = fg.d3Force("charge")
|
||||
if (charge) charge.strength(-140)
|
||||
// 1. Clustering-Kräfte (Knoten zu ihren jeweiligen Sektions-Foci ziehen)
|
||||
fg.d3Force("x", forceX().x((node: any) => getClusterX(node.category)).strength(0.14))
|
||||
fg.d3Force("y", forceY().y((node: any) => getClusterY(node.category)).strength(0.14))
|
||||
|
||||
// 2. Kollisionskraft (Verhindert, dass Knoten und Textlabels ineinandergeschoben werden)
|
||||
fg.d3Force("collide", forceCollide().radius((node: any) => {
|
||||
const nodeDegree = degree[node.id] || 0
|
||||
// Radius = Knotengröße + großzügiger Textpuffer
|
||||
return 28 + Math.min(nodeDegree, 12) * 1.5
|
||||
}).iterations(2))
|
||||
|
||||
// 3. Abstoßung (Charge) moderat, da collide und foci die Hauptarbeit leisten
|
||||
const charge = fg.d3Force("charge")
|
||||
if (charge) charge.strength(-60)
|
||||
|
||||
// 4. Federn (Links)
|
||||
const link = fg.d3Force("link")
|
||||
if (link) link.distance(60)
|
||||
if (link) link.distance(50)
|
||||
|
||||
// Simulation neu anschubsen bei neuen Daten
|
||||
fg.alpha(0.8).restart()
|
||||
}, [graphData])
|
||||
|
||||
const activeNode = hovered || selected
|
||||
@@ -126,7 +168,7 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
|
||||
graphData={graphData}
|
||||
backgroundColor="rgba(0,0,0,0)"
|
||||
nodeRelSize={4}
|
||||
cooldownTicks={100}
|
||||
cooldownTicks={120}
|
||||
|
||||
// Hover- und Klick-Logik
|
||||
onNodeClick={(node: any) => setSelected(node.id)}
|
||||
@@ -142,13 +184,22 @@ export function GraphView({ data, onDelete }: { data: MemoryGraph; onDelete: (id
|
||||
linkColor={(link: any) => {
|
||||
const sId = typeof link.source === "object" ? link.source.id : link.source
|
||||
const tId = typeof link.target === "object" ? link.target.id : link.target
|
||||
|
||||
// Highlight-Kanten leuchten in Indigo auf
|
||||
if (activeNode === sId || activeNode === tId) return EDGE_HI
|
||||
return activeNode ? DIM_EDGE : EDGE
|
||||
|
||||
// Wenn ein anderer Knoten aktiv ist, blenden wir diese Kante fast komplett aus
|
||||
if (activeNode) return DIM_EDGE
|
||||
|
||||
// Standard-Zustand: Kante leuchtet dezent in der Farbe der Kategorie des Quell-Knotens
|
||||
const sourceNode = graphData.nodes.find(n => n.id === sId)
|
||||
const catColor = sourceNode ? CAT_COLOR[sourceNode.category] : "#64748b"
|
||||
return hexToRgba(catColor, 0.16)
|
||||
}}
|
||||
linkWidth={(link: any) => {
|
||||
const sId = typeof link.source === "object" ? link.source.id : link.source
|
||||
const tId = typeof link.target === "object" ? link.target.id : link.target
|
||||
return (activeNode === sId || activeNode === tId) ? 2.2 : 0.8
|
||||
return (activeNode === sId || activeNode === tId) ? 2.5 : 1.2
|
||||
}}
|
||||
|
||||
// Daten-Partikel fließen auf den aktiven Verbindungen
|
||||
|
||||
Reference in New Issue
Block a user