trust-module-frontend/src/Components/TreeChart/TreeChart.jsx

189 lines
7.6 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 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,
}));
// Размещаем узлы иерархически
const center = { x: 0, y: 0 }; // Центральная точка
const baseRadius = 150; // Базовый радиус для 1-го уровня
const branchOffset = 80; // Смещение узлов вдоль ветки
const angleOffset = Math.PI / 4; // Угол смещения для дочерних ветвей
const spreadFactor = 1.5; // Коэффициент растяжения для последних узлов
nodes.forEach((node) => {
const prev = nodePositions.current.get(node.data.id);
if (prev) {
node.x = prev.x;
node.y = prev.y;
} else {
if (node.depth === 0) {
// Центральный узел
node.x = center.x;
node.y = center.y;
} else if (node.depth === 1) {
// Первый уровень - равномерно по окружности
const parent = node.parent;
const index = parent.children.indexOf(node);
const totalSiblings = parent.children.length;
const radius = baseRadius * node.depth;
const sectorAngle = (Math.PI * 2) / totalSiblings;
const angle = index * sectorAngle;
node.x = parent.x + radius * Math.cos(angle);
node.y = parent.y + radius * Math.sin(angle);
node.angle = angle; // Запоминаем угол для веток
} else {
// Второй уровень и дальше - ветка растет в направлении родителя
const parent = node.parent;
const siblings = parent.children || [];
const index = siblings.indexOf(node);
const totalSiblings = siblings.length;
const direction = parent.angle || 0;
const offsetAngle = ((index - (totalSiblings - 1) / 2) * angleOffset) / totalSiblings;
let distance = branchOffset;
if (!node.children || node.children.length === 0) {
// Если это последний узел, увеличиваем расстояние
distance *= spreadFactor + node.depth * 0.2; // Чем глубже, тем больше разброс
}
node.x = parent.x + distance * Math.cos(direction + offsetAngle);
node.y = parent.y + distance * Math.sin(direction + offsetAngle);
node.angle = direction + offsetAngle;
}
}
nodePositions.current.set(node.data.id, { x: node.x, y: node.y });
});
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, 1500, 1500])
.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");
// Очищаем предыдущие элементы
svg.selectAll(".links line").remove();
svg.selectAll(".nodes circle").remove();
svg.selectAll(".labels text").remove();
// Рисуем связи
const linkGroup = svg.select(".links");
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)
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
// Рисуем узлы
const nodeGroup = svg.select(".nodes");
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)
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y)
.call(drag());
node.on("click", (event, d) => {
if (onNodeClick) {
onNodeClick(d.data.id, d.data.title);
}
});
// Рисуем текстовые метки
const labelGroup = svg.select(".labels");
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") // Запрет взаимодействия с текстом
.style("fill", "var(--TreeChart-text-color)") // Используем переменную для цвета текста
.attr("x", (d) => d.x + 12)
.attr("y", (d) => d.y + 4);
}, [root, links, nodes, onNodeClick]);
const drag = () => {
function dragstarted(event, d) {
d3.select(this).raise().attr("stroke", "#000");
}
function dragged(event, d) {
d.x = event.x;
d.y = event.y;
d3.select(this).attr("cx", d.x).attr("cy", d.y);
// Обновляем текстовую метку
d3.select(this.parentNode)
.select("text")
.attr("x", d.x + 12)
.attr("y", d.y + 4);
// Обновляем связи
d3.select(chartRef.current)
.selectAll(".links line")
.filter((link) => link.source === d || link.target === d)
.attr("x1", (link) => link.source.x)
.attr("y1", (link) => link.source.y)
.attr("x2", (link) => link.target.x)
.attr("y2", (link) => link.target.y);
}
function dragended(event, d) {
d3.select(this).attr("stroke", "#fff");
nodePositions.current.set(d.data.id, { x: d.x, y: d.y });
}
return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended);
};
return <svg ref={chartRef} />;
};
export default TreeChart;