mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- Remove nested git repository from bugulma/frontend/.git - Add all frontend files to main repository tracking - Convert from separate frontend/backend repos to unified monorepo - Preserve all frontend code and development history as tracked files - Eliminate nested repository complexity for simpler development workflow This creates a proper monorepo structure with frontend and backend coexisting in the same repository for easier development and deployment.
96 lines
3.3 KiB
TypeScript
96 lines
3.3 KiB
TypeScript
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);
|