More compact dagre layout

This commit is contained in:
dimitrievgs 2025-09-24 00:01:52 +03:00
parent 351605a052
commit 32f55ab086
2 changed files with 69 additions and 25 deletions

View File

@ -137,8 +137,8 @@ const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
// Определяем размеры узлов для корректного расположения.
// Может потребоваться корректировка в зависимости от реального размера узла в CSS.
const nodeWidth = 172;
const nodeHeight = 36;
const nodeWidth = 120;
const nodeHeight = 40;
/**
* Применяет алгоритм расположения Dagre к узлам и рёбрам графа.
@ -148,14 +148,27 @@ const nodeHeight = 36;
* @returns {{nodes: any[], edges: any[]}} Объект с обновлёнными узлами и рёбрами, содержащими позиции.
*/
const getLayoutedElements = (nodes: any[], edges: any[], direction: 'TB' | 'LR' = 'TB') => {
const isHorizontal = direction === 'LR'; // Проверяем, горизонтальное ли расположение
// ДОБАВЛЕНО: ranksep и nodesep для лучшего отступа между узлами
dagreGraph.setGraph({ rankdir: direction, ranksep: 100, nodesep: 50 });
const isHorizontal = direction === 'LR';
// Компактные настройки для расположения в 2 ряда
dagreGraph.setGraph({
rankdir: direction,
ranksep: 40, // Уменьшенное расстояние между рангами (рядами)
nodesep: 10, // Уменьшенное расстояние между узлами в одном ранге
edgesep: 20, // Расстояние между рёбрами
marginx: 20, // Отступы по X
marginy: 20 // Отступы по Y
});
// Добавляем узлы в граф Dagre с их размерами
// Добавляем узлы в граф Dagre с динамическими размерами
nodes.forEach((node) => {
// Убедимся, что узел имеет уникальный id и необходимые размеры для Dagre
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
// Пытаемся найти элемент узла в DOM для получения реальных размеров
const nodeElement = document.querySelector(`[data-id="${node.id}"]`) as HTMLElement;
const dimensions = getNodeDimensions(nodeElement);
dagreGraph.setNode(node.id, {
width: Math.min(dimensions.width + 10, 140), // Ограничиваем максимальную ширину
height: Math.min(dimensions.height + 10, 50) // Ограничиваем максимальную высоту
});
});
// Добавляем рёбра в граф Dagre
@ -169,22 +182,34 @@ const getLayoutedElements = (nodes: any[], edges: any[], direction: 'TB' | 'LR'
// Обновляем позиции узлов React Flow на основе расчётов Dagre
const newNodes = nodes.map((node) => {
const nodeWithPosition: DagreNode = dagreGraph.node(node.id); // Получаем данные узла из Dagre
// Смещаем позицию узла Dagre (якорь в центре) к верхнему левому углу,
// чтобы она соответствовала точке привязки узла React Flow (верхний левый угол).
const nodeWithPosition: DagreNode = dagreGraph.node(node.id);
const nodeDimensions = dagreGraph.node(node.id);
return {
...node,
// Обновляем targetPosition и sourcePosition в зависимости от направления для корректного отображения ручек
targetPosition: isHorizontal ? 'left' : 'top',
sourcePosition: isHorizontal ? 'right' : 'bottom',
position: {
x: nodeWithPosition.x - nodeWidth / 2, // Корректировка X
y: nodeWithPosition.y - nodeHeight / 2 // Корректировка Y
x: nodeWithPosition.x - nodeDimensions.width / 2,
y: nodeWithPosition.y - nodeDimensions.height / 2
}
};
});
return { nodes: newNodes, edges }; // Возвращаем обновлённые узлы и рёбра
return { nodes: newNodes, edges };
};
// Функция для получения реальных размеров узла из DOM
const getNodeDimensions = (nodeElement: HTMLElement | null): { width: number; height: number } => {
if (!nodeElement) {
return { width: 120, height: 40 }; // Fallback размеры
}
const rect = nodeElement.getBoundingClientRect();
return {
width: Math.max(Math.min(rect.width, 130), 100), // Ограничиваем ширину: мин 100px, макс 130px
height: Math.max(Math.min(rect.height, 45), 30) // Ограничиваем высоту: мин 30px, макс 45px
};
};
/**
@ -214,29 +239,33 @@ export const GraphPanelComponent: React.FC<GraphPanelProps> = ({ graphData, onNo
if (graphData.nodes.length > 0 || graphData.edges.length > 0) {
console.log('Applying Dagre layout with data:', graphData);
// Преобразуем входящие данные в формат, необходимый для Dagre и React Flow
// Убеждаемся, что каждый узел имеет данные.label для отображения
const initialNodesForLayout = graphData.nodes.map((node) => ({
id: node.id,
// ДОБАВЛЕНО: используем node.type, если он есть, иначе 'default'
type: node.type || 'default',
data: { label: node.data?.label || `Node ${node.id}` },
position: { x: 0, y: 0 } // Временная позиция, будет перезаписана Dagre
position: { x: 0, y: 0 }
}));
const initialEdgesForLayout = graphData.edges.map((edge) => ({
id: edge.id,
source: edge.source,
target: edge.target,
// Для рёбер можно добавить тип 'default' или ваш кастомный
type: edge.type || 'default'
}));
// Применяем Dagre layout
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(initialNodesForLayout, initialEdgesForLayout);
// Сначала устанавливаем узлы без layout
setNodes(initialNodesForLayout);
setEdges(initialEdgesForLayout);
// Обновляем состояния узлов и рёбер React Flow
setNodes(layoutedNodes);
setEdges(layoutedEdges);
// Применяем layout с задержкой, чтобы DOM успел отрендерить узлы
setTimeout(() => {
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
initialNodesForLayout,
initialEdgesForLayout
);
setNodes(layoutedNodes);
setEdges(layoutedEdges);
}, 100); // Задержка 100ms для рендеринга DOM
} else {
// Если данных нет, очищаем граф
setNodes([]);

View File

@ -178,6 +178,21 @@
background-color: var(--llm-assistant-background-color); /* Светло-серый фон */
}
/* Компактные стили для узлов React Flow */
.react-flow__node {
font-size: 12px !important;
padding: 4px 8px !important;
min-width: 100px !important;
max-width: 130px !important;
min-height: 30px !important;
border-radius: 6px !important;
}
.react-flow__node-default {
width: auto !important;
height: auto !important;
}
/* ----------------------------------------------- Chat Panel Styles -------------------------------------------------------- */
.llm-agent-chat-panel {