turash/bugulma/frontend/components/admin/EconomicGraph.tsx
2025-12-15 10:06:41 +01:00

99 lines
3.3 KiB
TypeScript
Raw Permalink 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.

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<string | null>(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<string>();
const connected = new Set<string>([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 (
<div className="w-full h-96">
<svg viewBox="0 0 400 400" className="w-full h-full">
{/* 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 (
<line
key={`${link.source}-${link.target}`}
x1={sourceNode.x}
y1={sourceNode.y}
x2={targetNode.x}
y2={targetNode.y}
className="stroke-muted-foreground transition-opacity duration-200"
strokeWidth={Math.max(0.5, link.value / 2)}
style={{ opacity }}
/>
);
})}
{/* Nodes */}
{nodes.map((node) => {
const isNodeHovered = hoveredNodeId ? connectedNodeIds.has(node.id) : true;
const opacity = isNodeHovered ? 1 : 0.2;
return (
<g
key={node.id}
transform={`translate(${node.x}, ${node.y})`}
onMouseEnter={() => setHoveredNodeId(node.id)}
onMouseLeave={() => setHoveredNodeId(null)}
className="transition-opacity duration-200 cursor-pointer"
style={{ opacity }}
>
<title>{`Сектор: ${node.label}\nОрганизаций: ${node.orgCount}`}</title>
<circle
r={Math.max(10, Number(node.size) || 10)}
className={`fill-sector-${node.color}`}
/>
<text
textAnchor="middle"
dy={(node.size || 10) + 12}
className="text-xs font-medium pointer-events-none fill-foreground"
>
{node.label}
</text>
</g>
);
})}
</svg>
</div>
);
};
export default React.memo(EconomicGraph);