import { useTranslation } from '@/hooks/useI18n.tsx'; import { useOrganizations } from '@/hooks/useOrganizations.ts'; import { generateGraphData, GraphNode } from '@/lib/graphUtils.ts'; import React, { useCallback, useMemo, useState } from 'react'; const EconomicGraph = () => { const { t } = useTranslation(); const { organizations } = useOrganizations(); const { nodes, links } = useMemo(() => generateGraphData(organizations, t), [organizations, t]); const [hoveredNodeId, setHoveredNodeId] = useState(null); // Memoize node map for O(1) lookup instead of O(n) find const nodeMap = useMemo(() => { return new Map(nodes.map((n) => [n.id, n])); }, [nodes]); const findNode = useCallback( (id: string): GraphNode | undefined => { return nodeMap.get(id); }, [nodeMap] ); const connectedNodeIds = useMemo(() => { if (!hoveredNodeId) return new Set(); const connected = new Set([hoveredNodeId]); links.forEach((link) => { if (link.source === hoveredNodeId) { connected.add(link.target); } if (link.target === hoveredNodeId) { connected.add(link.source); } }); return connected; }, [hoveredNodeId, links]); return (
{/* Links */} {links.map((link) => { const sourceNode = findNode(link.source); const targetNode = findNode(link.target); if (!sourceNode || !targetNode) return null; const isLinkHovered = hoveredNodeId && (link.source === hoveredNodeId || link.target === hoveredNodeId); const opacity = hoveredNodeId ? (isLinkHovered ? 0.8 : 0.1) : 0.5; return ( ); })} {/* Nodes */} {nodes.map((node) => { const isNodeHovered = hoveredNodeId ? connectedNodeIds.has(node.id) : true; const opacity = isNodeHovered ? 1 : 0.2; return ( setHoveredNodeId(node.id)} onMouseLeave={() => setHoveredNodeId(null)} className="transition-opacity duration-200 cursor-pointer" style={{ opacity }} > {`Сектор: ${node.label}\nОрганизаций: ${node.orgCount}`} {node.label} ); })}
); }; export default React.memo(EconomicGraph);