NeonClary
Add settings menu for diagram layout and panel visibility
ebbd1a5
Raw
History Blame Contribute Delete
2.95 kB
import React, { useEffect, useRef, useMemo } from 'react';
import { Network } from 'vis-network/standalone';
import { LAYOUT_MODES } from '../../utils/settings';
/** Known node positions for explicit-coordinate mode (vis-network canvas coords). */
const EXPLICIT_POSITIONS = {
temperature: { x: 0, y: -140 },
D1: { x: -120, y: 20 },
D2: { x: 120, y: 20 },
'actuator-1': { x: 0, y: 140 },
D3: { x: 220, y: 20 },
};
function fallbackPosition(nodeId, index) {
const angle = (index * 2 * Math.PI) / 8;
return { x: Math.cos(angle) * 180, y: Math.sin(angle) * 120 };
}
function applyLayout(data, layoutMode) {
const base = data || { nodes: [], edges: [] };
const nodes = (base.nodes || []).map((node, index) => {
if (layoutMode !== LAYOUT_MODES.EXPLICIT) return { ...node };
const pos = EXPLICIT_POSITIONS[node.id] ?? fallbackPosition(node.id, index);
return { ...node, x: pos.x, y: pos.y, fixed: true };
});
return { nodes, edges: base.edges || [] };
}
function buildOptions(layoutMode) {
const options = {
autoResize: true,
nodes: {
borderWidth: 2,
font: { color: '#ffffff', size: 14 },
},
edges: {
arrows: 'to',
color: { color: '#9aa3b2', highlight: '#6366F1' },
font: { size: 10, color: '#9aa3b2', strokeWidth: 0, align: 'middle' },
smooth: { type: 'cubicBezier', roundness: 0.4 },
},
groups: {
sensor: { shape: 'box', color: { background: '#3b82f6', border: '#1d4ed8' } },
decidron: { shape: 'hexagon', color: { background: '#8b5cf6', border: '#6d28d9' } },
actuator: { shape: 'triangle', color: { background: '#10b981', border: '#047857' } },
},
interaction: { hover: true, tooltipDelay: 120 },
};
if (layoutMode === LAYOUT_MODES.EXPLICIT) {
options.physics = { enabled: false };
} else {
options.physics = {
stabilization: { iterations: 150 },
barnesHut: { springLength: 150, gravitationalConstant: -6000 },
};
if (layoutMode === LAYOUT_MODES.FIXED_SEED) {
options.layout = { randomSeed: 42 };
}
// RANDOM: no randomSeed — vis-network picks a new seed each build (default).
}
return options;
}
export default function NetworkDiagram({ data, layoutMode = LAYOUT_MODES.RANDOM, height = 380 }) {
const containerRef = useRef(null);
const networkRef = useRef(null);
const graphData = useMemo(
() => applyLayout(data, layoutMode),
[data, layoutMode],
);
useEffect(() => {
if (!containerRef.current) return undefined;
networkRef.current = new Network(
containerRef.current,
graphData,
buildOptions(layoutMode),
);
return () => {
if (networkRef.current) {
networkRef.current.destroy();
networkRef.current = null;
}
};
}, [graphData, layoutMode]);
return (
<div
ref={containerRef}
className="decidron-diagram"
style={{ height, width: '100%' }}
/>
);
}