Spaces:
Running
Running
| 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%' }} | |
| /> | |
| ); | |
| } | |