Files
MemoryBear/web/src/components/Charts/GraphNetworkChart.tsx
2026-03-07 12:18:11 +08:00

201 lines
7.8 KiB
TypeScript

/*
* @Author: ZhaoYing
* @Date: 2026-02-10 14:06:09
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-10 14:06:09
*/
/**
* GraphNetworkChart Component
*
* A force-directed graph visualization component built with ECharts.
* Displays nodes and edges in an interactive network diagram with physics-based layout.
* Supports zooming, panning, dragging nodes, and click interactions.
*/
import { type FC, useEffect, useRef, type SetStateAction, type Dispatch } from 'react'
import ReactEcharts from 'echarts-for-react';
import PageEmpty from '@/components/Empty/PageEmpty'
// Default color palette for node categories
const Colors = ['#171719', '#155EEF', '#9C6FFF', '#FF8A4C']
/**
* Node interface representing a graph node/vertex
*/
export interface Node {
id: string; // Unique identifier for the node
label: string; // Display label for the node
category: number; // Category index for grouping and coloring
symbolSize: number; // Size of the node symbol in pixels
name: string; // Node name (used in ECharts)
itemStyle: {
color: string; // Custom color for this node
}
caption: string; // Additional description or caption
[key: string]: any; // Allow additional custom properties
}
/**
* Edge interface representing a connection between two nodes
*/
export interface Edge {
id: string; // Unique identifier for the edge
source: string; // Source node ID
target: string; // Target node ID
type: string; // Type/category of the relationship
caption: string; // Description of the relationship
value: number; // Numeric value associated with the edge
weight: number; // Weight/strength of the connection
}
/**
* Props for the GraphNetworkChart component
*/
interface GraphNetworkChartProps {
nodes: Node[]; // Array of nodes to display in the graph
links: Edge[]; // Array of edges connecting the nodes
categories: { name: string }[]; // Category definitions for node grouping
colors?: string[]; // Optional custom color palette (defaults to Colors)
onNodeClick: Dispatch<SetStateAction<Node | null>>; // Callback when a node is clicked
}
const GraphNetworkChart: FC<GraphNetworkChartProps> = ({
nodes,
links,
categories,
colors = Colors,
onNodeClick,
}) => {
// Reference to the ECharts instance for programmatic control
const chartRef = useRef<ReactEcharts>(null);
// Flag to prevent multiple simultaneous resize operations (debouncing)
const resizeScheduledRef = useRef(false)
/**
* Effect: Handle responsive chart resizing
*
* Uses ResizeObserver to detect container size changes and resize the chart accordingly.
* Implements requestAnimationFrame for smooth, debounced resize operations.
* Re-runs when nodes change to ensure proper sizing with new data.
*/
useEffect(() => {
const handleResize = () => {
if (chartRef.current && !resizeScheduledRef.current) {
resizeScheduledRef.current = true
// Use requestAnimationFrame for smooth, optimized resize
requestAnimationFrame(() => {
chartRef.current?.getEchartsInstance().resize();
resizeScheduledRef.current = false
});
}
}
// Observe the chart container for size changes
const resizeObserver = new ResizeObserver(handleResize)
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
if (chartElement) {
resizeObserver.observe(chartElement)
}
// Cleanup: disconnect observer when component unmounts
return () => {
resizeObserver.disconnect()
}
}, [nodes])
return (
<div className="rb:w-full rb:h-full">
{/* Render chart only if nodes exist, otherwise show empty state */}
{nodes && nodes.length > 0
? <ReactEcharts
ref={chartRef}
option={{
// Color palette for node categories
colors: colors,
// Disable default tooltip (custom interaction via onNodeClick)
tooltip: {
show: false
},
// Hide legend (categories not displayed in legend)
legend: {
show: false,
bottom: 12,
},
series: [
{
type: 'graph', // Graph/network chart type
layout: 'force', // Force-directed layout algorithm
data: nodes || [], // Node data
links: links || [], // Edge data
categories: categories, // Category definitions
roam: true, // Enable zoom and pan interactions
// Dynamic zoom level based on node count for better initial view
zoom: nodes.length < 50 ? 3 : nodes.length < 100 ? 2 : 1,
// Node label configuration
label: {
show: true, // Display labels
position: 'right', // Position label to the right of node
formatter: '{b}', // Use node name as label text
},
// Edge styling
lineStyle: {
color: '#5B6167', // Gray color for edges
curveness: 0.3 // Slight curve for better visibility
},
// Force-directed layout physics configuration
force: {
repulsion: 100, // Repulsion force between nodes
edgeLength: 80, // Ideal distance between connected nodes
gravity: 0.3, // Gravity pulling nodes to center
layoutAnimation: true, // Animate layout changes
preventOverlap: true, // Prevent nodes from overlapping
edgeSymbol: ['none', 'arrow'], // Arrow on target end of edge
edgeSymbolSize: [4, 10], // Size of edge symbols
initLayout: 'force' // Use force-directed for initial layout
},
selectedMode: 'single', // Allow selecting one node at a time
draggable: true, // Enable dragging nodes
animationDurationUpdate: 0, // Disable animation on data update for performance
// Styling for selected nodes
select: {
itemStyle: {
borderWidth: 2, // Thicker border when selected
borderColor: '#ffffff', // White border for contrast
shadowBlur: 10, // Glow effect on selection
}
}
}
]
}}
style={{ height: '100%', width: '100%' }}
notMerge={false} // Merge options instead of replacing (better performance)
lazyUpdate={true} // Batch updates for better performance
// Event handlers
onEvents={{
click: (params: { dataType: string; data: Node; name: string }) => {
// Only trigger callback for node clicks (not edges or background)
if (params.dataType === 'node') {
onNodeClick(params.data)
}
}
}}
/>
: <PageEmpty />
}
</div>
)
}
export default GraphNetworkChart