Files
vf_react/src/views/Concept/components/ForceGraphView.js
2025-12-05 17:47:06 +08:00

849 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ForceGraphView - 概念层级关系图(使用 reagraph
*
* 特性:
* 1. 清晰的层级布局Radial/Hierarchical
* 2. 节点大小根据股票数量动态调整
* 3. 涨红跌绿颜色映射
* 4. 悬停显示涨跌幅详情
* 5. 点击节点可聚焦或跳转
*/
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import {
GraphCanvas,
useSelection,
lightTheme,
darkTheme,
} from 'reagraph';
import {
Box,
VStack,
HStack,
Text,
Spinner,
Center,
Icon,
Flex,
Button,
IconButton,
Tooltip,
Badge,
useBreakpointValue,
Select,
FormControl,
FormLabel,
Collapse,
useDisclosure,
Divider,
} from '@chakra-ui/react';
import {
FaLayerGroup,
FaSync,
FaExpand,
FaCompress,
FaCog,
FaHome,
FaChevronUp,
FaArrowUp,
FaArrowDown,
FaProjectDiagram,
FaCircle,
} from 'react-icons/fa';
import { logger } from '../../../utils/logger';
// 一级分类颜色映射
const LV1_COLORS = {
'人工智能': '#8B5CF6',
'半导体': '#3B82F6',
'机器人': '#10B981',
'消费电子': '#F59E0B',
'智能驾驶与汽车': '#EF4444',
'新能源与电力': '#06B6D4',
'空天经济': '#6366F1',
'国防军工': '#EC4899',
'政策与主题': '#14B8A6',
'周期与材料': '#F97316',
'大消费': '#A855F7',
'数字经济与金融科技': '#22D3EE',
'全球宏观与贸易': '#84CC16',
'医药健康': '#E879F9',
'前沿科技': '#38BDF8',
};
// 根据涨跌幅获取颜色(涨红跌绿)
const getChangeColor = (value, baseColor = '#64748B') => {
if (value === null || value === undefined) return baseColor;
// 涨 - 红色系(更鲜艳)
if (value > 7) return '#DC2626';
if (value > 5) return '#EF4444';
if (value > 3) return '#F87171';
if (value > 1) return '#FCA5A5';
if (value > 0) return '#FECACA';
// 跌 - 绿色系
if (value < -7) return '#15803D';
if (value < -5) return '#16A34A';
if (value < -3) return '#22C55E';
if (value < -1) return '#4ADE80';
if (value < 0) return '#86EFAC';
return baseColor; // 平盘
};
// 根据涨跌幅获取发光颜色
const getGlowColor = (value) => {
if (value === null || value === undefined) return 'rgba(100, 116, 139, 0.5)';
if (value > 3) return 'rgba(239, 68, 68, 0.6)';
if (value > 0) return 'rgba(252, 165, 165, 0.5)';
if (value < -3) return 'rgba(34, 197, 94, 0.6)';
if (value < 0) return 'rgba(134, 239, 172, 0.5)';
return 'rgba(100, 116, 139, 0.5)';
};
// 从 API 返回的名称中提取纯名称
const extractPureName = (apiName) => {
if (!apiName) return '';
return apiName.replace(/^\[(一级|二级|三级)\]\s*/, '');
};
// 格式化涨跌幅
const formatChangePercent = (value) => {
if (value === null || value === undefined) return '--';
const formatted = Math.abs(value).toFixed(2);
return value > 0 ? `+${formatted}%` : value < 0 ? `-${formatted}%` : '0.00%';
};
/**
* 自定义深色主题
*/
const customDarkTheme = {
...darkTheme,
canvas: {
background: 'transparent',
},
node: {
...darkTheme.node,
fill: '#8B5CF6',
activeFill: '#A78BFA',
label: {
color: '#E2E8F0',
stroke: '#0F172A',
activeColor: '#FFFFFF',
},
},
edge: {
...darkTheme.edge,
fill: '#475569',
activeFill: '#8B5CF6',
},
ring: {
...darkTheme.ring,
fill: '#8B5CF6',
activeFill: '#A78BFA',
},
arrow: {
...darkTheme.arrow,
fill: '#475569',
activeFill: '#8B5CF6',
},
cluster: {
...darkTheme.cluster,
stroke: '#475569',
fill: 'rgba(139, 92, 246, 0.1)',
label: {
...darkTheme.cluster?.label,
color: '#94A3B8',
},
},
};
/**
* 主组件
*/
const ForceGraphView = ({
apiBaseUrl,
onSelectCategory,
selectedDate,
}) => {
const [hierarchy, setHierarchy] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [priceData, setPriceData] = useState({ lv1Map: {}, lv2Map: {}, lv3Map: {}, leafMap: {} });
const [priceLoading, setPriceLoading] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [layoutType, setLayoutType] = useState('radialOut2d'); // 'radialOut2d', 'treeTd2d', 'treeLr2d', 'forceDirected2d'
const [displayLevel, setDisplayLevel] = useState('lv2'); // 'lv1', 'lv2', 'lv3', 'all'
const [selectedNode, setSelectedNode] = useState(null);
const graphRef = useRef();
const containerRef = useRef();
const { isOpen: isSettingsOpen, onToggle: onSettingsToggle } = useDisclosure({ defaultIsOpen: false });
const isMobile = useBreakpointValue({ base: true, md: false });
// 获取层级结构数据
const fetchHierarchy = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`${apiBaseUrl}/hierarchy`);
if (!response.ok) throw new Error('获取层级结构失败');
const data = await response.json();
setHierarchy(data.hierarchy || []);
logger.info('ForceGraphView', '层级结构加载完成', {
totalLv1: data.hierarchy?.length,
totalConcepts: data.total_concepts
});
} catch (err) {
logger.error('ForceGraphView', 'fetchHierarchy', err);
setError(err.message);
} finally {
setLoading(false);
}
}, [apiBaseUrl]);
// 获取层级涨跌幅数据
const fetchHierarchyPrice = useCallback(async () => {
setPriceLoading(true);
try {
let url = `${apiBaseUrl}/hierarchy/price`;
if (selectedDate) {
const dateStr = selectedDate.toISOString().split('T')[0];
url += `?trade_date=${dateStr}`;
}
const response = await fetch(url);
if (!response.ok) {
logger.warn('ForceGraphView', '获取层级涨跌幅失败', { status: response.status });
return;
}
const data = await response.json();
const lv1Map = {};
const lv2Map = {};
const lv3Map = {};
const leafMap = {};
(data.lv1_concepts || []).forEach(item => {
const pureName = extractPureName(item.concept_name);
lv1Map[pureName] = item;
});
(data.lv2_concepts || []).forEach(item => {
const pureName = extractPureName(item.concept_name);
lv2Map[pureName] = item;
});
(data.lv3_concepts || []).forEach(item => {
const pureName = extractPureName(item.concept_name);
lv3Map[pureName] = item;
});
(data.leaf_concepts || []).forEach(item => {
leafMap[item.concept_name] = item;
});
setPriceData({ lv1Map, lv2Map, lv3Map, leafMap });
logger.info('ForceGraphView', '层级涨跌幅加载完成', {
lv1Count: Object.keys(lv1Map).length,
lv2Count: Object.keys(lv2Map).length,
});
} catch (err) {
logger.warn('ForceGraphView', '获取层级涨跌幅失败', { error: err.message });
} finally {
setPriceLoading(false);
}
}, [apiBaseUrl, selectedDate]);
useEffect(() => {
fetchHierarchy();
}, [fetchHierarchy]);
useEffect(() => {
if (hierarchy.length > 0) {
fetchHierarchyPrice();
}
}, [hierarchy, fetchHierarchyPrice]);
// 构建图数据
const graphData = useMemo(() => {
const nodes = [];
const edges = [];
const { lv1Map, lv2Map, lv3Map, leafMap } = priceData;
// 根节点
nodes.push({
id: 'root',
label: '概念中心',
data: { level: 'root', changePct: null },
fill: '#8B5CF6',
size: 60,
});
hierarchy.forEach((lv1) => {
const lv1Id = `lv1_${lv1.name}`;
const lv1Price = lv1Map[lv1.name] || {};
const lv1BaseColor = LV1_COLORS[lv1.name] || '#8B5CF6';
const lv1Color = getChangeColor(lv1Price.avg_change_pct, lv1BaseColor);
// 一级节点
nodes.push({
id: lv1Id,
label: lv1.name,
data: {
level: 'lv1',
changePct: lv1Price.avg_change_pct,
stockCount: lv1Price.stock_count,
conceptCount: lv1.concept_count,
baseColor: lv1BaseColor,
},
fill: lv1Color,
size: Math.max(35, Math.min(55, 35 + (lv1Price.stock_count || 0) / 50)),
});
edges.push({
id: `root-${lv1Id}`,
source: 'root',
target: lv1Id,
size: 2,
});
// 二级节点
if (lv1.children && (displayLevel === 'lv2' || displayLevel === 'lv3' || displayLevel === 'all')) {
lv1.children.forEach((lv2) => {
const lv2Id = `lv2_${lv1.name}_${lv2.name}`;
const lv2Price = lv2Map[lv2.name] || {};
const lv2Color = getChangeColor(lv2Price.avg_change_pct, lv1BaseColor);
nodes.push({
id: lv2Id,
label: lv2.name,
data: {
level: 'lv2',
parentLv1: lv1.name,
changePct: lv2Price.avg_change_pct,
stockCount: lv2Price.stock_count,
conceptCount: lv2.concept_count,
baseColor: lv1BaseColor,
},
fill: lv2Color,
size: Math.max(25, Math.min(40, 25 + (lv2Price.stock_count || 0) / 80)),
});
edges.push({
id: `${lv1Id}-${lv2Id}`,
source: lv1Id,
target: lv2Id,
size: 1.5,
});
// 三级节点
if (lv2.children && (displayLevel === 'lv3' || displayLevel === 'all')) {
lv2.children.forEach((lv3) => {
const lv3Id = `lv3_${lv1.name}_${lv2.name}_${lv3.name}`;
const lv3Price = lv3Map[lv3.name] || {};
const lv3Color = getChangeColor(lv3Price.avg_change_pct, lv1BaseColor);
nodes.push({
id: lv3Id,
label: lv3.name,
data: {
level: 'lv3',
parentLv1: lv1.name,
parentLv2: lv2.name,
changePct: lv3Price.avg_change_pct,
stockCount: lv3Price.stock_count,
baseColor: lv1BaseColor,
},
fill: lv3Color,
size: Math.max(18, Math.min(30, 18 + (lv3Price.stock_count || 0) / 100)),
});
edges.push({
id: `${lv2Id}-${lv3Id}`,
source: lv2Id,
target: lv3Id,
size: 1,
});
// 叶子概念
if (displayLevel === 'all' && lv3.concepts) {
lv3.concepts.slice(0, 5).forEach((conceptName) => { // 限制每个lv3只显示5个概念
const conceptId = `concept_${conceptName}`;
const conceptPrice = leafMap[conceptName] || {};
const conceptColor = getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor);
// 避免重复添加节点
if (!nodes.find(n => n.id === conceptId)) {
nodes.push({
id: conceptId,
label: conceptName,
data: {
level: 'concept',
parentLv1: lv1.name,
parentLv2: lv2.name,
parentLv3: lv3.name,
changePct: conceptPrice.avg_change_pct,
stockCount: conceptPrice.stock_count,
baseColor: lv1BaseColor,
},
fill: conceptColor,
size: 12,
});
}
edges.push({
id: `${lv3Id}-${conceptId}`,
source: lv3Id,
target: conceptId,
size: 0.5,
});
});
}
});
}
// lv2 直接包含的概念
if (displayLevel === 'all' && lv2.concepts) {
lv2.concepts.slice(0, 5).forEach((conceptName) => {
const conceptId = `concept_${conceptName}`;
const conceptPrice = leafMap[conceptName] || {};
const conceptColor = getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor);
if (!nodes.find(n => n.id === conceptId)) {
nodes.push({
id: conceptId,
label: conceptName,
data: {
level: 'concept',
parentLv1: lv1.name,
parentLv2: lv2.name,
changePct: conceptPrice.avg_change_pct,
stockCount: conceptPrice.stock_count,
baseColor: lv1BaseColor,
},
fill: conceptColor,
size: 12,
});
}
edges.push({
id: `${lv2Id}-${conceptId}`,
source: lv2Id,
target: conceptId,
size: 0.5,
});
});
}
});
}
});
return { nodes, edges };
}, [hierarchy, priceData, displayLevel]);
// 节点点击处理
const handleNodeClick = useCallback((node) => {
setSelectedNode(node);
if (node.data?.level === 'concept') {
const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(node.label)}.html`;
window.open(htmlPath, '_blank');
}
logger.info('ForceGraphView', '节点点击', { level: node.data?.level, name: node.label });
}, []);
// 刷新数据
const handleRefresh = useCallback(() => {
fetchHierarchyPrice();
}, [fetchHierarchyPrice]);
// 全屏切换
const toggleFullscreen = useCallback(() => {
setIsFullscreen(prev => !prev);
}, []);
// 获取容器高度
const containerHeight = useMemo(() => {
if (isFullscreen) return '100vh';
return isMobile ? '500px' : '700px';
}, [isFullscreen, isMobile]);
if (loading) {
return (
<Center h="500px" bg="rgba(15, 23, 42, 0.8)" borderRadius="2xl">
<VStack spacing={4}>
<Spinner size="xl" color="purple.400" thickness="4px" />
<Text color="gray.400">正在构建概念关系图...</Text>
</VStack>
</Center>
);
}
if (error) {
return (
<Center h="500px" bg="rgba(15, 23, 42, 0.8)" borderRadius="2xl">
<VStack spacing={4}>
<Icon as={FaLayerGroup} boxSize={16} color="gray.600" />
<Text color="gray.400">加载失败{error}</Text>
<Button colorScheme="purple" size="sm" onClick={fetchHierarchy}>
重试
</Button>
</VStack>
</Center>
);
}
return (
<Box
ref={containerRef}
position={isFullscreen ? 'fixed' : 'relative'}
top={isFullscreen ? 0 : 'auto'}
left={isFullscreen ? 0 : 'auto'}
right={isFullscreen ? 0 : 'auto'}
bottom={isFullscreen ? 0 : 'auto'}
zIndex={isFullscreen ? 1000 : 'auto'}
bg="linear-gradient(135deg, #0F172A 0%, #1E1B4B 50%, #0F172A 100%)"
borderRadius={isFullscreen ? '0' : '2xl'}
overflow="hidden"
border={isFullscreen ? 'none' : '1px solid'}
borderColor="whiteAlpha.200"
h={containerHeight}
>
{/* 背景装饰 */}
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
opacity={0.1}
bgImage="radial-gradient(circle at 20% 30%, rgba(139, 92, 246, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(59, 130, 246, 0.3) 0%, transparent 50%)"
pointerEvents="none"
/>
{/* 顶部工具栏 */}
<Flex
position="absolute"
top={4}
left={4}
right={4}
justify="space-between"
align="flex-start"
zIndex={10}
pointerEvents="none"
>
{/* 左侧标题和信息 */}
<VStack align="start" spacing={3} pointerEvents="auto">
<HStack
bg="rgba(0, 0, 0, 0.7)"
backdropFilter="blur(12px)"
px={4}
py={2}
borderRadius="full"
border="1px solid"
borderColor="whiteAlpha.200"
boxShadow="0 4px 20px rgba(0, 0, 0, 0.3)"
>
<Icon as={FaProjectDiagram} color="purple.300" />
<Text color="white" fontWeight="bold" fontSize="sm">
概念关系图
</Text>
<Badge
bg="purple.500"
color="white"
borderRadius="full"
px={2}
fontSize="xs"
>
{graphData.nodes.length} 节点
</Badge>
</HStack>
{/* 选中节点信息卡片 */}
{selectedNode && selectedNode.data?.level !== 'root' && (
<Box
bg="rgba(0, 0, 0, 0.85)"
backdropFilter="blur(12px)"
px={4}
py={3}
borderRadius="xl"
border="1px solid"
borderColor={selectedNode.fill || 'purple.500'}
boxShadow={`0 4px 20px ${getGlowColor(selectedNode.data?.changePct)}`}
maxW="280px"
>
<VStack align="start" spacing={2}>
<HStack spacing={2}>
<Badge
bg={selectedNode.data?.baseColor || 'purple.500'}
color="white"
borderRadius="full"
px={2}
fontSize="xs"
>
{selectedNode.data?.level === 'lv1' ? '一级分类' :
selectedNode.data?.level === 'lv2' ? '二级分类' :
selectedNode.data?.level === 'lv3' ? '三级分类' : '概念'}
</Badge>
</HStack>
<Text color="white" fontWeight="bold" fontSize="md">
{selectedNode.label}
</Text>
{/* 涨跌幅显示 */}
{selectedNode.data?.changePct !== undefined && selectedNode.data?.changePct !== null && (
<HStack
bg={selectedNode.data.changePct > 0 ? 'rgba(239, 68, 68, 0.2)' : selectedNode.data.changePct < 0 ? 'rgba(34, 197, 94, 0.2)' : 'rgba(100, 116, 139, 0.2)'}
px={3}
py={1}
borderRadius="full"
border="1px solid"
borderColor={selectedNode.data.changePct > 0 ? 'red.400' : selectedNode.data.changePct < 0 ? 'green.400' : 'gray.500'}
>
<Icon
as={selectedNode.data.changePct > 0 ? FaArrowUp : selectedNode.data.changePct < 0 ? FaArrowDown : FaCircle}
color={selectedNode.data.changePct > 0 ? 'red.400' : selectedNode.data.changePct < 0 ? 'green.400' : 'gray.400'}
boxSize={3}
/>
<Text
color={selectedNode.data.changePct > 0 ? 'red.300' : selectedNode.data.changePct < 0 ? 'green.300' : 'gray.300'}
fontWeight="bold"
fontSize="lg"
fontFamily="mono"
>
{formatChangePercent(selectedNode.data.changePct)}
</Text>
</HStack>
)}
<HStack spacing={4} color="whiteAlpha.700" fontSize="xs">
{selectedNode.data?.stockCount && (
<Text>{selectedNode.data.stockCount} 只股票</Text>
)}
{selectedNode.data?.conceptCount && (
<Text>{selectedNode.data.conceptCount} 个概念</Text>
)}
</HStack>
{selectedNode.data?.level === 'concept' && (
<Text color="purple.300" fontSize="xs">
点击查看详情
</Text>
)}
</VStack>
</Box>
)}
</VStack>
{/* 右侧控制按钮 */}
<VStack spacing={2} pointerEvents="auto">
<HStack spacing={2}>
{priceLoading && <Spinner size="sm" color="purple.300" />}
<Tooltip label="刷新数据" placement="left">
<IconButton
size="sm"
icon={<FaSync />}
onClick={handleRefresh}
isLoading={priceLoading}
bg="rgba(0, 0, 0, 0.7)"
color="white"
border="1px solid"
borderColor="whiteAlpha.200"
_hover={{ bg: 'whiteAlpha.200', borderColor: 'purple.400' }}
aria-label="刷新"
/>
</Tooltip>
<Tooltip label="设置" placement="left">
<IconButton
size="sm"
icon={isSettingsOpen ? <FaChevronUp /> : <FaCog />}
onClick={onSettingsToggle}
bg={isSettingsOpen ? 'purple.500' : 'rgba(0, 0, 0, 0.7)'}
color="white"
border="1px solid"
borderColor={isSettingsOpen ? 'purple.400' : 'whiteAlpha.200'}
_hover={{ bg: isSettingsOpen ? 'purple.400' : 'whiteAlpha.200' }}
aria-label="设置"
/>
</Tooltip>
<Tooltip label={isFullscreen ? '退出全屏' : '全屏'} placement="left">
<IconButton
size="sm"
icon={isFullscreen ? <FaCompress /> : <FaExpand />}
onClick={toggleFullscreen}
bg="rgba(0, 0, 0, 0.7)"
color="white"
border="1px solid"
borderColor="whiteAlpha.200"
_hover={{ bg: 'whiteAlpha.200', borderColor: 'purple.400' }}
aria-label={isFullscreen ? '退出全屏' : '全屏'}
/>
</Tooltip>
</HStack>
{/* 设置面板 */}
<Collapse in={isSettingsOpen}>
<Box
bg="rgba(0, 0, 0, 0.85)"
backdropFilter="blur(12px)"
p={4}
borderRadius="xl"
border="1px solid"
borderColor="whiteAlpha.200"
w="220px"
boxShadow="0 4px 20px rgba(0, 0, 0, 0.4)"
>
<VStack spacing={4} align="stretch">
<FormControl>
<FormLabel color="whiteAlpha.800" fontSize="xs" mb={1}>
布局方式
</FormLabel>
<Select
value={layoutType}
onChange={(e) => setLayoutType(e.target.value)}
size="sm"
bg="whiteAlpha.100"
color="white"
borderColor="whiteAlpha.300"
_hover={{ borderColor: 'purple.400' }}
sx={{ option: { bg: '#1a1a2e', color: 'white' } }}
>
<option value="radialOut2d">放射状</option>
<option value="treeTd2d">树形(上下)</option>
<option value="treeLr2d">树形(左右)</option>
<option value="forceDirected2d">力导向</option>
</Select>
</FormControl>
<FormControl>
<FormLabel color="whiteAlpha.800" fontSize="xs" mb={1}>
显示层级
</FormLabel>
<Select
value={displayLevel}
onChange={(e) => setDisplayLevel(e.target.value)}
size="sm"
bg="whiteAlpha.100"
color="white"
borderColor="whiteAlpha.300"
_hover={{ borderColor: 'purple.400' }}
sx={{ option: { bg: '#1a1a2e', color: 'white' } }}
>
<option value="lv1">一级分类</option>
<option value="lv2">到二级</option>
<option value="lv3">到三级</option>
<option value="all">全部(含概念)</option>
</Select>
</FormControl>
</VStack>
</Box>
</Collapse>
</VStack>
</Flex>
{/* 底部图例 */}
<Flex
position="absolute"
bottom={4}
left={4}
zIndex={10}
gap={2}
flexWrap="wrap"
pointerEvents="none"
>
<HStack
bg="rgba(0, 0, 0, 0.7)"
backdropFilter="blur(10px)"
px={3}
py={2}
borderRadius="full"
border="1px solid"
borderColor="whiteAlpha.200"
spacing={2}
>
<Box w={3} h={3} borderRadius="full" bg="#EF4444" boxShadow="0 0 8px rgba(239, 68, 68, 0.5)" />
<Text color="whiteAlpha.800" fontSize="xs"></Text>
</HStack>
<HStack
bg="rgba(0, 0, 0, 0.7)"
backdropFilter="blur(10px)"
px={3}
py={2}
borderRadius="full"
border="1px solid"
borderColor="whiteAlpha.200"
spacing={2}
>
<Box w={3} h={3} borderRadius="full" bg="#22C55E" boxShadow="0 0 8px rgba(34, 197, 94, 0.5)" />
<Text color="whiteAlpha.800" fontSize="xs"></Text>
</HStack>
<HStack
bg="rgba(0, 0, 0, 0.7)"
backdropFilter="blur(10px)"
px={3}
py={2}
borderRadius="full"
border="1px solid"
borderColor="whiteAlpha.200"
spacing={2}
>
<Box w={3} h={3} borderRadius="full" bg="#64748B" />
<Text color="whiteAlpha.800" fontSize="xs">/无数据</Text>
</HStack>
</Flex>
{/* 操作提示 */}
<Box
position="absolute"
bottom={4}
right={4}
zIndex={10}
pointerEvents="none"
>
<HStack
bg="rgba(0, 0, 0, 0.7)"
backdropFilter="blur(10px)"
px={3}
py={2}
borderRadius="full"
border="1px solid"
borderColor="whiteAlpha.200"
>
<Text color="whiteAlpha.600" fontSize="xs">
滚轮缩放 · 拖拽平移 · 点击节点查看详情
</Text>
</HStack>
</Box>
{/* 图表 */}
<GraphCanvas
ref={graphRef}
nodes={graphData.nodes}
edges={graphData.edges}
theme={customDarkTheme}
layoutType={layoutType}
labelType="all"
edgeArrowPosition="none"
draggable
onNodeClick={handleNodeClick}
contextMenu={null}
cameraMode="pan"
sizingType="centrality"
/>
</Box>
);
};
export default ForceGraphView;