/** * Network topology diagram using D3.js - shows coordinator and servers. */ import { useEffect, useRef } from 'react'; import * as d3 from 'd3'; import type { SimulationConfig } from '../../types/simulation'; import { useSimulationStore } from '../../store/simulationStore'; import { formatRate } from '../../utils/timeFormat'; interface NetworkDiagramProps { config: SimulationConfig; } interface Node { id: string; type: 'coordinator' | 'server' | 'exit'; x: number; y: number; label: string; } interface Link { source: string; target: string; probability: number; label: string; } export default function NetworkDiagram({ config }: NetworkDiagramProps) { const svgRef = useRef(null); const { timeUnit } = useSimulationStore(); useEffect(() => { if (!svgRef.current) return; const width = 800; const height = 500; const margin = { top: 20, right: 20, bottom: 20, left: 20 }; // Clear previous SVG d3.select(svgRef.current).selectAll('*').remove(); const svg = d3 .select(svgRef.current) .attr('width', width) .attr('height', height) .attr('viewBox', `0 0 ${width} ${height}`) .attr('preserveAspectRatio', 'xMidYMid meet'); // Create nodes const nodes: Node[] = [ { id: 'coordinator', type: 'coordinator', x: width / 2, y: 100, label: 'Coordinateur', }, { id: 'exit', type: 'exit', x: width / 2, y: height - 80, label: 'Sortie', }, ]; // Add server nodes const numServers = config.servers.length; const serverSpacing = Math.min(150, (width - 2 * margin.left) / (numServers + 1)); const serverY = height / 2; config.servers.forEach((server, i) => { const x = margin.left + (i + 1) * serverSpacing + (width - numServers * serverSpacing) / 2; nodes.push({ id: server.id, type: 'server', x, y: serverY, label: `Serveur ${i + 1}`, }); }); // Create links const links: Link[] = []; // External arrival to coordinator links.push({ source: 'arrival', target: 'coordinator', probability: config.arrival_rate, label: `λ = ${formatRate(config.arrival_rate, timeUnit)}`, }); // Coordinator to exit links.push({ source: 'coordinator', target: 'exit', probability: config.coordinator_exit_probability, label: `p = ${config.coordinator_exit_probability.toFixed(2)}`, }); // Coordinator to servers config.servers.forEach((server) => { links.push({ source: 'coordinator', target: server.id, probability: server.routing_probability, label: `q = ${server.routing_probability.toFixed(2)}`, }); }); // Servers back to coordinator config.servers.forEach((server) => { links.push({ source: server.id, target: 'coordinator', probability: 1.0, label: '', }); }); // Draw links const linkGroup = svg.append('g').attr('class', 'links'); // Draw coordinator to servers/exit links .filter((l) => l.source === 'coordinator') .forEach((link) => { const sourceNode = nodes.find((n) => n.id === link.source); const targetNode = nodes.find((n) => n.id === link.target); if (!sourceNode || !targetNode) return; // Draw curved arrow linkGroup .append('path') .attr('d', () => { const dx = targetNode.x - sourceNode.x; const dy = targetNode.y - sourceNode.y; const dr = Math.sqrt(dx * dx + dy * dy) * 0.3; return `M ${sourceNode.x},${sourceNode.y} Q ${sourceNode.x + dx / 2 + dr},${ sourceNode.y + dy / 2 } ${targetNode.x},${targetNode.y - 30}`; }) .attr('fill', 'none') .attr('stroke', link.target === 'exit' ? '#ef4444' : '#3b82f6') .attr('stroke-width', 2) .attr('marker-end', 'url(#arrowhead)'); // Add label if (link.label) { const midX = sourceNode.x + (targetNode.x - sourceNode.x) / 2; const midY = sourceNode.y + (targetNode.y - sourceNode.y) / 2; linkGroup .append('text') .attr('x', midX + 20) .attr('y', midY - 10) .attr('font-size', '12px') .attr('fill', '#6b7280') .text(link.label); } }); // Draw servers to coordinator (feedback) links .filter((l) => l.source !== 'coordinator' && l.source !== 'arrival') .forEach((link) => { const sourceNode = nodes.find((n) => n.id === link.source); const targetNode = nodes.find((n) => n.id === link.target); if (!sourceNode || !targetNode) return; linkGroup .append('path') .attr('d', () => { const dx = targetNode.x - sourceNode.x; const dy = targetNode.y - sourceNode.y; const dr = Math.sqrt(dx * dx + dy * dy) * 0.5; return `M ${sourceNode.x},${sourceNode.y - 30} Q ${sourceNode.x + dx / 2 - dr},${ sourceNode.y + dy / 2 } ${targetNode.x},${targetNode.y + 30}`; }) .attr('fill', 'none') .attr('stroke', '#9ca3af') .attr('stroke-width', 1.5) .attr('stroke-dasharray', '5,5') .attr('marker-end', 'url(#arrowhead-gray)'); }); // Draw external arrival const arrivalX = width / 2 - 100; linkGroup .append('line') .attr('x1', arrivalX) .attr('y1', 50) .attr('x2', width / 2 - 40) .attr('y2', 80) .attr('stroke', '#10b981') .attr('stroke-width', 2) .attr('marker-end', 'url(#arrowhead-green)'); linkGroup .append('text') .attr('x', arrivalX - 10) .attr('y', 45) .attr('font-size', '12px') .attr('fill', '#059669') .text(`λ = ${formatRate(config.arrival_rate, timeUnit)}`); // Define arrowhead markers const defs = svg.append('defs'); defs .append('marker') .attr('id', 'arrowhead') .attr('viewBox', '0 0 10 10') .attr('refX', 9) .attr('refY', 5) .attr('markerWidth', 6) .attr('markerHeight', 6) .attr('orient', 'auto') .append('path') .attr('d', 'M 0 0 L 10 5 L 0 10 z') .attr('fill', '#3b82f6'); defs .append('marker') .attr('id', 'arrowhead-gray') .attr('viewBox', '0 0 10 10') .attr('refX', 9) .attr('refY', 5) .attr('markerWidth', 6) .attr('markerHeight', 6) .attr('orient', 'auto') .append('path') .attr('d', 'M 0 0 L 10 5 L 0 10 z') .attr('fill', '#9ca3af'); defs .append('marker') .attr('id', 'arrowhead-green') .attr('viewBox', '0 0 10 10') .attr('refX', 9) .attr('refY', 5) .attr('markerWidth', 6) .attr('markerHeight', 6) .attr('orient', 'auto') .append('path') .attr('d', 'M 0 0 L 10 5 L 0 10 z') .attr('fill', '#10b981'); // Draw nodes const nodeGroup = svg.append('g').attr('class', 'nodes'); nodes.forEach((node) => { const group = nodeGroup.append('g'); // Node circle group .append('circle') .attr('cx', node.x) .attr('cy', node.y) .attr('r', node.type === 'coordinator' ? 40 : node.type === 'exit' ? 30 : 35) .attr('fill', node.type === 'coordinator' ? '#3b82f6' : node.type === 'exit' ? '#ef4444' : '#8b5cf6') .attr('stroke', '#fff') .attr('stroke-width', 3); // Node label group .append('text') .attr('x', node.x) .attr('y', node.y + 5) .attr('text-anchor', 'middle') .attr('font-size', '12px') .attr('font-weight', 'bold') .attr('fill', '#fff') .text(node.label); // Service rate label for servers and coordinator if (node.type === 'coordinator') { group .append('text') .attr('x', node.x) .attr('y', node.y + 60) .attr('text-anchor', 'middle') .attr('font-size', '11px') .attr('fill', '#4b5563') .text(`μc = ${formatRate(config.coordinator_service_rate, timeUnit)}`); } else if (node.type === 'server') { const serverIndex = config.servers.findIndex((s) => s.id === node.id); if (serverIndex >= 0) { group .append('text') .attr('x', node.x) .attr('y', node.y + 55) .attr('text-anchor', 'middle') .attr('font-size', '11px') .attr('fill', '#4b5563') .text(`μ = ${formatRate(config.servers[serverIndex].service_rate, timeUnit)}`); } } }); }, [config, timeUnit]); return (

Topologie du réseau

Coordinateur
Serveur
Sortie
); }