feat(lightrag_webui): optimize GraphControl performance with caching and memoization
- Cache selection state and neighbor sets in refs to prevent expensive reducer recreation on every hover/selection change - Memoize theme-derived values (labelColor, edgeColor, etc) to avoid recomputation in reducer functions - Improve node neighbor lookup from O(n) array.includes() to O(1) Set lookup - Refactor nodeReducer and edgeReducer with stable dependencies on themeColors - Remove unnecessary error handling in reducers (defensive checks) - Clean up comments and consolidate logic for improved readability - Fix typo: "Simgma" → "Sigma"
This commit is contained in:
parent
e106c8e16b
commit
99f950671e
1 changed files with 115 additions and 139 deletions
|
|
@ -1,22 +1,18 @@
|
||||||
import { useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
import { useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
||||||
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
|
||||||
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
|
||||||
import type { AbstractGraph } from 'graphology-types'
|
import type { AbstractGraph } from 'graphology-types'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
|
|
||||||
import type { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
import type { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
||||||
import useTheme from '@/hooks/useTheme'
|
import useTheme from '@/hooks/useTheme'
|
||||||
import * as Constants from '@/lib/constants'
|
import * as Constants from '@/lib/constants'
|
||||||
|
|
||||||
import { useGraphStore } from '@/stores/graph'
|
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { useGraphStore } from '@/stores/graph'
|
||||||
|
|
||||||
const isButtonPressed = (ev: MouseEvent | TouchEvent) => {
|
const isButtonPressed = (ev: MouseEvent | TouchEvent) => {
|
||||||
if (ev.type.startsWith('mouse')) {
|
if (ev.type.startsWith('mouse')) {
|
||||||
if ((ev as MouseEvent).buttons !== 0) {
|
return (ev as MouseEvent).buttons !== 0
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -45,8 +41,8 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
||||||
|
|
||||||
// Track system theme changes when theme is set to 'system'
|
// Track system theme changes when theme is set to 'system'
|
||||||
const [systemThemeIsDark, setSystemThemeIsDark] = useState(
|
const [systemThemeIsDark, setSystemThemeIsDark] = useState(() =>
|
||||||
() => window.matchMedia('(prefers-color-scheme: dark)').matches
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -58,20 +54,86 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
}
|
}
|
||||||
}, [theme])
|
}, [theme])
|
||||||
|
|
||||||
|
// ==================== PERFORMANCE OPTIMIZATION ====================
|
||||||
|
// Cache selection state in refs to avoid recreating reducers on every hover
|
||||||
|
const selectionRef = useRef({
|
||||||
|
selectedNode: null as string | null,
|
||||||
|
focusedNode: null as string | null,
|
||||||
|
selectedEdge: null as string | null,
|
||||||
|
focusedEdge: null as string | null,
|
||||||
|
hideUnselectedEdges,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cache computed neighbors for the focused node (Set for O(1) lookup)
|
||||||
|
const neighborsCache = useRef<{ nodeId: string | null; neighbors: Set<string> }>({
|
||||||
|
nodeId: null,
|
||||||
|
neighbors: new Set(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Memoize theme-derived values to avoid recomputing in reducers
|
||||||
|
const themeColors = useMemo(() => {
|
||||||
|
const isDarkTheme =
|
||||||
|
theme === 'dark' ||
|
||||||
|
(theme === 'system' && window.document.documentElement.classList.contains('dark'))
|
||||||
|
return {
|
||||||
|
isDarkTheme,
|
||||||
|
labelColor: isDarkTheme ? Constants.labelColorDarkTheme : undefined,
|
||||||
|
edgeColor: isDarkTheme ? Constants.edgeColorDarkTheme : undefined,
|
||||||
|
edgeHighlightColor: isDarkTheme
|
||||||
|
? Constants.edgeColorHighlightedDarkTheme
|
||||||
|
: Constants.edgeColorHighlightedLightTheme,
|
||||||
|
}
|
||||||
|
}, [theme, systemThemeIsDark])
|
||||||
|
|
||||||
|
// Update refs when selection changes, then trigger sigma refresh (not reducer recreation)
|
||||||
|
useEffect(() => {
|
||||||
|
selectionRef.current = {
|
||||||
|
selectedNode,
|
||||||
|
focusedNode,
|
||||||
|
selectedEdge,
|
||||||
|
focusedEdge,
|
||||||
|
hideUnselectedEdges,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate and rebuild neighbor cache if focused node changed
|
||||||
|
const targetNode = focusedNode || selectedNode
|
||||||
|
if (neighborsCache.current.nodeId !== targetNode) {
|
||||||
|
if (targetNode && sigma) {
|
||||||
|
const graph = sigma.getGraph()
|
||||||
|
if (graph.hasNode(targetNode)) {
|
||||||
|
// Build Set once for O(1) lookups in reducer
|
||||||
|
neighborsCache.current = {
|
||||||
|
nodeId: targetNode,
|
||||||
|
neighbors: new Set(graph.neighbors(targetNode)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
neighborsCache.current = { nodeId: null, neighbors: new Set() }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
neighborsCache.current = { nodeId: null, neighbors: new Set() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger sigma refresh to re-run reducers with updated ref values
|
||||||
|
if (sigma) {
|
||||||
|
sigma.refresh()
|
||||||
|
}
|
||||||
|
}, [selectedNode, focusedNode, selectedEdge, focusedEdge, hideUnselectedEdges, sigma])
|
||||||
|
// ==================== END OPTIMIZATION ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When component mount or maxIterations changes
|
* When component mount or maxIterations changes
|
||||||
* => ensure graph reference and apply layout
|
* => ensure graph reference and apply layout
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sigmaGraph && sigma) {
|
if (sigmaGraph && sigma) {
|
||||||
// Ensure sigma binding to sigmaGraph
|
|
||||||
try {
|
try {
|
||||||
if (typeof sigma.setGraph === 'function') {
|
if (typeof sigma.setGraph === 'function') {
|
||||||
sigma.setGraph(sigmaGraph as unknown as AbstractGraph<NodeType, EdgeType>)
|
sigma.setGraph(sigmaGraph as unknown as AbstractGraph<NodeType, EdgeType>)
|
||||||
console.log('Binding graph to sigma instance')
|
console.log('Binding graph to sigma instance')
|
||||||
} else {
|
} else {
|
||||||
;(sigma as any).graph = sigmaGraph
|
;(sigma as any).graph = sigmaGraph
|
||||||
console.warn('Simgma missing setGraph function, set graph property directly')
|
console.warn('Sigma missing setGraph function, set graph property directly')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting graph on sigma instance:', error)
|
console.error('Error setting graph on sigma instance:', error)
|
||||||
|
|
@ -84,11 +146,9 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the sigma instance is set in the store
|
* Ensure the sigma instance is set in the store
|
||||||
* This provides a backup in case the instance wasn't set in GraphViewer
|
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sigma) {
|
if (sigma) {
|
||||||
// Double-check that the store has the sigma instance
|
|
||||||
const currentInstance = useGraphStore.getState().sigmaInstance
|
const currentInstance = useGraphStore.getState().sigmaInstance
|
||||||
if (!currentInstance) {
|
if (!currentInstance) {
|
||||||
console.log('Setting sigma instance from GraphControl')
|
console.log('Setting sigma instance from GraphControl')
|
||||||
|
|
@ -98,18 +158,15 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
}, [sigma])
|
}, [sigma])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When component mount
|
* Register events
|
||||||
* => register events
|
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } =
|
const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } =
|
||||||
useGraphStore.getState()
|
useGraphStore.getState()
|
||||||
|
|
||||||
// Define event types
|
|
||||||
type NodeEvent = { node: string; event: { original: MouseEvent | TouchEvent } }
|
type NodeEvent = { node: string; event: { original: MouseEvent | TouchEvent } }
|
||||||
type EdgeEvent = { edge: string; event: { original: MouseEvent | TouchEvent } }
|
type EdgeEvent = { edge: string; event: { original: MouseEvent | TouchEvent } }
|
||||||
|
|
||||||
// Register all events, but edge events will only be processed if enableEdgeEvents is true
|
|
||||||
const events: Record<string, any> = {
|
const events: Record<string, any> = {
|
||||||
enterNode: (event: NodeEvent) => {
|
enterNode: (event: NodeEvent) => {
|
||||||
if (!isButtonPressed(event.event.original)) {
|
if (!isButtonPressed(event.event.original)) {
|
||||||
|
|
@ -134,19 +191,16 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
clickStage: () => clearSelection(),
|
clickStage: () => clearSelection(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add edge event handlers if enableEdgeEvents is true
|
|
||||||
if (enableEdgeEvents) {
|
if (enableEdgeEvents) {
|
||||||
events.clickEdge = (event: EdgeEvent) => {
|
events.clickEdge = (event: EdgeEvent) => {
|
||||||
setSelectedEdge(event.edge)
|
setSelectedEdge(event.edge)
|
||||||
setSelectedNode(null)
|
setSelectedNode(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
events.enterEdge = (event: EdgeEvent) => {
|
events.enterEdge = (event: EdgeEvent) => {
|
||||||
if (!isButtonPressed(event.event.original)) {
|
if (!isButtonPressed(event.event.original)) {
|
||||||
setFocusedEdge(event.edge)
|
setFocusedEdge(event.edge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events.leaveEdge = (event: EdgeEvent) => {
|
events.leaveEdge = (event: EdgeEvent) => {
|
||||||
if (!isButtonPressed(event.event.original)) {
|
if (!isButtonPressed(event.event.original)) {
|
||||||
setFocusedEdge(null)
|
setFocusedEdge(null)
|
||||||
|
|
@ -154,10 +208,8 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the events
|
|
||||||
registerEvents(events)
|
registerEvents(events)
|
||||||
|
|
||||||
// Cleanup function - basic cleanup without relying on specific APIs
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
try {
|
||||||
console.log('Cleaning up graph event listeners')
|
console.log('Cleaning up graph event listeners')
|
||||||
|
|
@ -168,20 +220,16 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
}, [registerEvents, enableEdgeEvents, sigma])
|
}, [registerEvents, enableEdgeEvents, sigma])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When edge size settings change, recalculate edge sizes and refresh the sigma instance
|
* Recalculate edge sizes when settings change
|
||||||
* to ensure changes take effect immediately
|
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sigma && sigmaGraph) {
|
if (sigma && sigmaGraph) {
|
||||||
// Get the graph from sigma
|
|
||||||
const graph = sigma.getGraph()
|
const graph = sigma.getGraph()
|
||||||
|
|
||||||
// Find min and max weight values
|
|
||||||
let minWeight = Number.MAX_SAFE_INTEGER
|
let minWeight = Number.MAX_SAFE_INTEGER
|
||||||
let maxWeight = 0
|
let maxWeight = 0
|
||||||
|
|
||||||
graph.forEachEdge((edge) => {
|
graph.forEachEdge((edge) => {
|
||||||
// Get original weight (before scaling)
|
|
||||||
const weight = graph.getEdgeAttribute(edge, 'originalWeight') || 1
|
const weight = graph.getEdgeAttribute(edge, 'originalWeight') || 1
|
||||||
if (typeof weight === 'number') {
|
if (typeof weight === 'number') {
|
||||||
minWeight = Math.min(minWeight, weight)
|
minWeight = Math.min(minWeight, weight)
|
||||||
|
|
@ -189,7 +237,6 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Scale edge sizes based on weight range and current min/max edge size settings
|
|
||||||
const weightRange = maxWeight - minWeight
|
const weightRange = maxWeight - minWeight
|
||||||
if (weightRange > 0) {
|
if (weightRange > 0) {
|
||||||
const sizeScale = maxEdgeSize - minEdgeSize
|
const sizeScale = maxEdgeSize - minEdgeSize
|
||||||
|
|
@ -202,91 +249,35 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// If all weights are the same, use default size
|
|
||||||
graph.forEachEdge((edge) => {
|
graph.forEachEdge((edge) => {
|
||||||
graph.setEdgeAttribute(edge, 'size', minEdgeSize)
|
graph.setEdgeAttribute(edge, 'size', minEdgeSize)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the sigma instance to apply changes
|
|
||||||
sigma.refresh()
|
sigma.refresh()
|
||||||
}
|
}
|
||||||
}, [sigma, sigmaGraph, minEdgeSize, maxEdgeSize])
|
}, [sigma, sigmaGraph, minEdgeSize, maxEdgeSize])
|
||||||
|
|
||||||
// Cache selection state in refs so we don't trigger expensive reducer recreation on hover/selection changes
|
// ==================== STABLE REDUCERS (read from refs) ====================
|
||||||
const selectionRef = useRef({
|
// These reducers are stable and only recreated when sigma/theme changes
|
||||||
selectedNode: null as string | null,
|
// Selection state is read from refs, avoiding costly reducer recreation on hover
|
||||||
focusedNode: null as string | null,
|
|
||||||
selectedEdge: null as string | null,
|
|
||||||
focusedEdge: null as string | null,
|
|
||||||
hideUnselectedEdges,
|
|
||||||
})
|
|
||||||
|
|
||||||
const focusedNeighborsRef = useRef<{ key: string | null; neighbors: Set<string> | null }>({
|
|
||||||
key: null,
|
|
||||||
neighbors: null,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
selectionRef.current = {
|
|
||||||
selectedNode,
|
|
||||||
focusedNode,
|
|
||||||
selectedEdge,
|
|
||||||
focusedEdge,
|
|
||||||
hideUnselectedEdges,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate neighbor cache when focused node changes
|
|
||||||
if (focusedNeighborsRef.current.key !== (focusedNode || null)) {
|
|
||||||
focusedNeighborsRef.current = { key: focusedNode || null, neighbors: null }
|
|
||||||
}
|
|
||||||
}, [selectedNode, focusedNode, selectedEdge, focusedEdge, hideUnselectedEdges])
|
|
||||||
|
|
||||||
// Theme values used inside reducers; kept in refs to avoid re-creating reducer functions
|
|
||||||
const themeRef = useRef({
|
|
||||||
isDarkTheme: false,
|
|
||||||
labelColor: undefined as string | undefined,
|
|
||||||
edgeColor: undefined as string | undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isDarkTheme =
|
|
||||||
theme === 'dark' ||
|
|
||||||
(theme === 'system' && window.document.documentElement.classList.contains('dark'))
|
|
||||||
|
|
||||||
themeRef.current = {
|
|
||||||
isDarkTheme,
|
|
||||||
labelColor: isDarkTheme ? Constants.labelColorDarkTheme : undefined,
|
|
||||||
edgeColor: isDarkTheme ? Constants.edgeColorDarkTheme : undefined,
|
|
||||||
}
|
|
||||||
}, [theme, systemThemeIsDark])
|
|
||||||
|
|
||||||
// Helper to lazily compute focused node neighbors and reuse across reducer calls
|
|
||||||
const getFocusedNeighbors = useCallback((graph: AbstractGraph, nodeId: string): Set<string> => {
|
|
||||||
if (focusedNeighborsRef.current.key === nodeId && focusedNeighborsRef.current.neighbors) {
|
|
||||||
return focusedNeighborsRef.current.neighbors
|
|
||||||
}
|
|
||||||
const neighbors = new Set(graph.neighbors(nodeId))
|
|
||||||
focusedNeighborsRef.current = { key: nodeId, neighbors }
|
|
||||||
return neighbors
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const nodeReducer = useCallback(
|
const nodeReducer = useCallback(
|
||||||
(node: string, data: NodeType) => {
|
(node: string, data: NodeType) => {
|
||||||
const graph = sigma.getGraph()
|
const graph = sigma.getGraph()
|
||||||
|
const { labelColor, isDarkTheme } = themeColors
|
||||||
const { selectedNode, focusedNode, selectedEdge, focusedEdge } = selectionRef.current
|
const { selectedNode, focusedNode, selectedEdge, focusedEdge } = selectionRef.current
|
||||||
const { isDarkTheme, labelColor } = themeRef.current
|
|
||||||
|
|
||||||
if (!graph.hasNode(node)) {
|
if (!graph.hasNode(node)) {
|
||||||
return { ...data, highlighted: false, labelColor }
|
return { ...data, highlighted: false, labelColor }
|
||||||
}
|
}
|
||||||
|
|
||||||
const newData: NodeType & { labelColor?: string; borderColor?: string; borderSize?: number } =
|
// Always start with highlighted: false to prevent persistence
|
||||||
{
|
const newData: NodeType & {
|
||||||
...data,
|
labelColor?: string
|
||||||
highlighted: data.highlighted || false,
|
borderColor?: string
|
||||||
labelColor,
|
borderSize?: number
|
||||||
}
|
} = { ...data, highlighted: false, labelColor }
|
||||||
|
|
||||||
// Hidden connections indicator
|
// Hidden connections indicator
|
||||||
const dbDegree = graph.getNodeAttribute(node, 'db_degree') || 0
|
const dbDegree = graph.getNodeAttribute(node, 'db_degree') || 0
|
||||||
|
|
@ -300,34 +291,30 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
return newData
|
return newData
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetNode = focusedNode || selectedNode
|
const _focusedNode = focusedNode || selectedNode
|
||||||
const targetEdge = focusedEdge || selectedEdge
|
const _focusedEdge = focusedEdge || selectedEdge
|
||||||
|
|
||||||
if (targetNode && graph.hasNode(targetNode)) {
|
if (_focusedNode && graph.hasNode(_focusedNode)) {
|
||||||
try {
|
// O(1) lookup using cached Set instead of O(n) array.includes()
|
||||||
const neighbors = getFocusedNeighbors(graph, targetNode)
|
const isNeighbor = node === _focusedNode || neighborsCache.current.neighbors.has(node)
|
||||||
if (node === targetNode || neighbors.has(node)) {
|
|
||||||
newData.highlighted = true
|
if (isNeighbor) {
|
||||||
if (node === selectedNode) {
|
newData.highlighted = true
|
||||||
newData.borderColor = Constants.nodeBorderColorSelected
|
if (node === selectedNode) {
|
||||||
}
|
newData.borderColor = Constants.nodeBorderColorSelected
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in nodeReducer:', error)
|
|
||||||
return { ...data, highlighted: false, labelColor }
|
|
||||||
}
|
}
|
||||||
} else if (targetEdge && graph.hasEdge(targetEdge)) {
|
} else if (_focusedEdge && graph.hasEdge(_focusedEdge)) {
|
||||||
try {
|
if (graph.extremities(_focusedEdge).includes(node)) {
|
||||||
if (graph.extremities(targetEdge).includes(node)) {
|
newData.highlighted = true
|
||||||
newData.highlighted = true
|
newData.size = 3
|
||||||
newData.size = 3
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error accessing edge extremities in nodeReducer:', error)
|
|
||||||
return { ...data, highlighted: false, labelColor }
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// No focus - early return with original colors (don't apply disabled)
|
||||||
|
return newData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply highlight/disabled styling only when there's a focus target
|
||||||
if (newData.highlighted) {
|
if (newData.highlighted) {
|
||||||
if (isDarkTheme) {
|
if (isDarkTheme) {
|
||||||
newData.labelColor = Constants.LabelColorHighlightedDarkTheme
|
newData.labelColor = Constants.LabelColorHighlightedDarkTheme
|
||||||
|
|
@ -338,15 +325,15 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
|
|
||||||
return newData
|
return newData
|
||||||
},
|
},
|
||||||
[sigma, disableHoverEffect, getFocusedNeighbors]
|
[sigma, disableHoverEffect, themeColors]
|
||||||
)
|
)
|
||||||
|
|
||||||
const edgeReducer = useCallback(
|
const edgeReducer = useCallback(
|
||||||
(edge: string, data: EdgeType) => {
|
(edge: string, data: EdgeType) => {
|
||||||
const graph = sigma.getGraph()
|
const graph = sigma.getGraph()
|
||||||
|
const { labelColor, edgeColor, edgeHighlightColor } = themeColors
|
||||||
const { selectedNode, focusedNode, selectedEdge, focusedEdge, hideUnselectedEdges } =
|
const { selectedNode, focusedNode, selectedEdge, focusedEdge, hideUnselectedEdges } =
|
||||||
selectionRef.current
|
selectionRef.current
|
||||||
const { isDarkTheme, labelColor, edgeColor } = themeRef.current
|
|
||||||
|
|
||||||
if (!graph.hasEdge(edge)) {
|
if (!graph.hasEdge(edge)) {
|
||||||
return { ...data, hidden: false, labelColor, color: edgeColor }
|
return { ...data, hidden: false, labelColor, color: edgeColor }
|
||||||
|
|
@ -358,22 +345,14 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
return newData
|
return newData
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetNode = focusedNode || selectedNode
|
const _focusedNode = focusedNode || selectedNode
|
||||||
const edgeHighlightColor = isDarkTheme
|
|
||||||
? Constants.edgeColorHighlightedDarkTheme
|
|
||||||
: Constants.edgeColorHighlightedLightTheme
|
|
||||||
|
|
||||||
if (targetNode && graph.hasNode(targetNode)) {
|
if (_focusedNode && graph.hasNode(_focusedNode)) {
|
||||||
try {
|
const includesNode = graph.extremities(edge).includes(_focusedNode)
|
||||||
const includesNode = graph.extremities(edge).includes(targetNode)
|
if (hideUnselectedEdges && !includesNode) {
|
||||||
if (hideUnselectedEdges && !includesNode) {
|
newData.hidden = true
|
||||||
newData.hidden = true
|
} else if (includesNode) {
|
||||||
} else if (includesNode) {
|
newData.color = edgeHighlightColor
|
||||||
newData.color = edgeHighlightColor
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in edgeReducer:', error)
|
|
||||||
return { ...data, hidden: false, labelColor, color: edgeColor }
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const _selectedEdge = selectedEdge && graph.hasEdge(selectedEdge) ? selectedEdge : null
|
const _selectedEdge = selectedEdge && graph.hasEdge(selectedEdge) ? selectedEdge : null
|
||||||
|
|
@ -392,13 +371,10 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
|
|
||||||
return newData
|
return newData
|
||||||
},
|
},
|
||||||
[sigma, disableHoverEffect]
|
[sigma, disableHoverEffect, themeColors]
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
// Set reducers only when they actually change (not on every hover)
|
||||||
* Keep sigma reducers stable; selection/theme changes are read from refs to avoid
|
|
||||||
* re-registering reducers on every hover and maintain frame budget.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSettings({
|
setSettings({
|
||||||
enableEdgeEvents,
|
enableEdgeEvents,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue