NetworkDiagram.tsx 9,56 ko
Newer Older
/**
 * 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<SVGSVGElement>(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)}`);

  return (
    <div className="bg-white border border-gray-200 rounded-lg p-4">
      <h3 className="text-sm font-semibold text-gray-900 mb-3">Topologie du réseau</h3>
      <div className="flex justify-center">
        <svg ref={svgRef} className="max-w-full h-auto"></svg>
      </div>
      <div className="mt-4 flex items-center justify-center space-x-6 text-xs text-gray-600">
        <div className="flex items-center">
          <div className="w-4 h-4 bg-blue-600 rounded-full mr-2"></div>
          <span>Coordinateur</span>
        </div>
        <div className="flex items-center">
          <div className="w-4 h-4 bg-purple-600 rounded-full mr-2"></div>
          <span>Serveur</span>
        </div>
        <div className="flex items-center">
          <div className="w-4 h-4 bg-red-600 rounded-full mr-2"></div>
          <span>Sortie</span>
        </div>
      </div>
    </div>
  );
}