import React, { useRef, useEffect, useMemo } from "react"; import * as d3 from "d3"; import "../../Style/TreeChart.css"; import { getStatusColor } from "./dataUtils"; const TreeChart = ({ data, onNodeClick }) => { const chartRef = useRef(); const simulationRef = useRef(null); const nodePositions = useRef(new Map()); const { root, nodes, links } = useMemo(() => { if (!data || !data.items) return { root: null, nodes: [], links: [] }; const root = d3.hierarchy(data, (d) => d.items); const maxDepth = d3.max(root.descendants(), (d) => d.depth); // Фильтруем узлы, исключая последний уровень const nodes = root.descendants().filter((d) => d.depth < maxDepth); // Фильтруем связи const links = nodes.filter((d) => d.parent).map((d) => ({ source: d.parent, target: d, })); // Применяем сохраненные позиции к узлам nodes.forEach((node) => { const prev = nodePositions.current.get(node.data.id); if (prev) { node.x = prev.x; node.y = prev.y; node.fx = prev.fx ?? null; node.fy = prev.fy ?? null; } else { const parent = node.parent; node.x = parent ? parent.x + Math.random() * 50 - 25 : Math.random() * 1000; node.y = parent ? parent.y + Math.random() * 50 - 25 : Math.random() * 1000; } nodePositions.current.set(node.data.id, { x: node.x, y: node.y, fx: node.fx, fy: node.fy }); }); return { root, nodes, links }; }, [data]); useEffect(() => { if (!chartRef.current) return; const svg = d3.select(chartRef.current) .attr("width", 2000) .attr("height", 2000) .attr("viewBox", [-500, -500, 1000, 1000]) .attr("style", "max-width: 100%; height: auto;"); svg.append("g").attr("class", "links"); svg.append("g").attr("class", "nodes"); svg.append("g").attr("class", "labels"); // Инициализация симуляции simulationRef.current = d3.forceSimulation() .force("link", d3.forceLink().id((d) => d.data.id).distance(80).strength(1)) .force("charge", d3.forceManyBody().strength(-200)) .force("center", d3.forceCenter(0, 0)) .force("collision", d3.forceCollide().radius(20)) .force("x", d3.forceX(0).strength(0.05)) .force("y", d3.forceY(0).strength(0.05)) .force("radial", d3.forceRadial(200, 0, 0).strength(0.02)) .alphaDecay(0.02) .alphaTarget(0.1); // Запускаем симуляцию на 15 секунд, затем отключаем setTimeout(() => { if (simulationRef.current) { simulationRef.current.stop(); // Останавливаем симуляцию nodes.forEach((node) => { node.fx = node.x; // Фиксируем текущие позиции узлов node.fy = node.y; }); } }, 15000); // 15 секунд }, []); useEffect(() => { if (!root || !chartRef.current || !simulationRef.current) return; // Проверяем, что симуляция инициализирована const svg = d3.select(chartRef.current); const linkGroup = svg.select(".links"); const nodeGroup = svg.select(".nodes"); const labelGroup = svg.select(".labels"); // Обновляем связи const link = linkGroup .selectAll("line") .data(links, (d) => `${d.source.data.id}-${d.target.data.id}`) .join("line") .attr("stroke", "#999") .attr("stroke-opacity", 0.6); // Обновляем узлы const node = nodeGroup .selectAll("circle") .data(nodes, (d) => d.data.id) .join("circle") .attr("fill", (d) => getStatusColor(d.data.status)) .attr("stroke", "#fff") .attr("r", 7) .call(drag()); node.on("click", (event, d) => { if (onNodeClick) { onNodeClick(d.data.id, d.data.title); } }); // Обновляем текстовые метки const text = labelGroup .selectAll("text") .data(nodes, (d) => d.data.id) .join("text") .text((d) => (nodes.length > 50 ? "" : d.data.title)) // Скрываем текст, если узлов много .attr("dx", 12) .attr("dy", 4) .style("user-select", "none") // Запрет выделения текста .style("pointer-events", "none"); // Запрет взаимодействия с текстом // Обновляем симуляцию simulationRef.current.nodes(nodes); simulationRef.current.force("link").links(links); simulationRef.current.alphaTarget(0.1).restart(); simulationRef.current.on("tick", () => { link .attr("x1", (d) => d.source.x) .attr("y1", (d) => d.source.y) .attr("x2", (d) => d.target.x) .attr("y2", (d) => d.target.y); node .attr("cx", (d) => d.x) .attr("cy", (d) => d.y); text .attr("x", (d) => d.x + 12) .attr("y", (d) => d.y + 4); }); }, [root, links, nodes, onNodeClick]); const drag = () => { function dragstarted(event, d) { if (!event.active && simulationRef.current) simulationRef.current.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event, d) { d.fx = event.x; d.fy = event.y; } function dragended(event, d) { if (!event.active && simulationRef.current) simulationRef.current.alphaTarget(0); nodePositions.current.set(d.data.id, { x: d.x, y: d.y, fx: d.fx, fy: d.fy }); } return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended); }; return ; }; export default TreeChart;