turash/bugulma/frontend/components/admin/EconomicGraph.tsx
Damir Mukimov 6347f42e20
Consolidate repositories: Remove nested frontend .git and merge into main repository
- 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.
2025-11-25 06:02:57 +01:00

96 lines
3.3 KiB
TypeScript
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.

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);