SteadyStateChart.tsx 8,96 ko
Newer Older
/**
 * Graphique montrant l'évolution du nombre de clients pour identifier le régime permanent
 */
import { useMemo } from 'react';
import {
  Box,
  Paper,
  Typography,
  Alert,
  AlertTitle,
} from '@mui/material';
import { TrendingUp as TrendingUpIcon, MenuBook as MenuBookIcon } from '@mui/icons-material';
import { Line } from 'react-chartjs-2';
import { useSimulationStore } from '../../store/simulationStore';
import { formatTime } from '../../utils/timeFormat';

interface SteadyStateChartProps {
  warmupTime?: number;
}

export default function SteadyStateChart({ warmupTime }: SteadyStateChartProps) {
  const { simulationResults, config, timeUnit } = useSimulationStore();

  const { chartData, stats } = useMemo(() => {
    if (!simulationResults?.time_series) {
      return { chartData: null, stats: null };
    }

    const { timestamps, customers_in_system } = simulationResults.time_series;

    if (!timestamps || timestamps.length === 0) {
      return { chartData: null, stats: null };
    }

    const warmup = warmupTime || config?.warmup_time || 0;

    // Séparer période de chauffe et régime permanent
    const warmupData = timestamps
      .map((time, i) => ({ time, clients: customers_in_system[i] }))
      .filter(d => d.time < warmup);

    const steadyStateData = timestamps
      .map((time, i) => ({ time, clients: customers_in_system[i] }))
      .filter(d => d.time >= warmup);

    // Calculer statistiques du régime permanent
    let steadyStats = null;
    if (steadyStateData.length > 0) {
      const steadyStateClients = steadyStateData.map(d => d.clients);
      const mean = steadyStateClients.reduce((a, b) => a + b, 0) / steadyStateClients.length;
      const variance = steadyStateClients.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / steadyStateClients.length;
      const stdDev = Math.sqrt(variance);
      const min = Math.min(...steadyStateClients);
      const max = Math.max(...steadyStateClients);
      const cv = (stdDev / mean) * 100;

      steadyStats = {
        warmup,
        mean,
        stdDev,
        min,
        max,
        cv,
        warmupPoints: warmupData.length,
        steadyStatePoints: steadyStateData.length,
      };
    }

    // Préparer les données pour Chart.js
    const data = {
      labels: timestamps.map(t => formatTime(t, timeUnit)),
      datasets: [
        {
          label: 'Nombre de clients',
          data: customers_in_system,
          borderColor: 'rgb(25, 118, 210)',
          backgroundColor: 'rgba(25, 118, 210, 0.1)',
          borderWidth: 2,
          fill: true,
          tension: 0.1,
          pointRadius: 0,
          pointHoverRadius: 4,
          segment: {
            borderColor: (ctx: any) => {
              const idx = ctx.p0DataIndex;
              const time = timestamps[idx];
              return time < warmup ? 'rgba(237, 108, 2, 0.6)' : 'rgb(25, 118, 210)';
            },
            backgroundColor: (ctx: any) => {
              const idx = ctx.p0DataIndex;
              const time = timestamps[idx];
              return time < warmup ? 'rgba(237, 108, 2, 0.1)' : 'rgba(25, 118, 210, 0.1)';
            },
          },
        },
      ],
    };

    return { chartData: data, stats: steadyStats };
  }, [simulationResults, timeUnit, warmupTime, config]);
  if (!simulationResults) {
    return null;
  }

  if (!chartData || !stats) {
    return (
      <Paper sx={{ p: 3 }}>
        <Alert severity="info">
          <AlertTitle>Séries temporelles non disponibles</AlertTitle>
          Les données de séries temporelles n'ont pas été collectées pour cette simulation.
        </Alert>
      </Paper>
    );
  }

  const options = {
    responsive: true,
    maintainAspectRatio: false,
    interaction: {
      mode: 'index' as const,
      intersect: false,
    },
    plugins: {
      legend: {
        position: 'top' as const,
      },
      tooltip: {
        callbacks: {
          title: (items: any[]) => {
            if (items.length > 0) {
              return `Temps: ${items[0].label}`;
            }
            return '';
          },
          label: (context: any) => {
            return `Clients: ${context.parsed.y}`;
          },
        },
      },
    },
    scales: {
      x: {
        title: {
          display: true,
          text: `Temps (${timeUnit})`,
        },
        ticks: {
          maxTicksLimit: 10,
        },
      },
      y: {
        title: {
          display: true,
          text: 'Nombre de clients',
        },
        beginAtZero: true,
      },
    },
  };

  return (
    <Paper sx={{ p: 3 }}>
      <Typography variant="h6" gutterBottom sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
        <TrendingUpIcon /> Évolution du Système - Détection du Régime Permanent
      </Typography>

      {/* Informations sur le régime permanent */}
      <Box sx={{ mb: 3, display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 2 }}>
        <Paper variant="outlined" sx={{ p: 2, bgcolor: 'warning.50' }}>
          <Typography variant="caption" color="text.secondary">
            Période de chauffe
          </Typography>
          <Typography variant="h6" fontWeight={600}>
            {formatTime(stats.warmup, timeUnit)}
          </Typography>
          <Typography variant="caption" color="text.secondary">
            {stats.warmupPoints} points échantillonnés
          </Typography>
        </Paper>

        <Paper variant="outlined" sx={{ p: 2, bgcolor: 'success.50' }}>
          <Typography variant="caption" color="text.secondary">
            Régime permanent
          </Typography>
          <Typography variant="h6" fontWeight={600}>
            {stats.steadyStatePoints} points
          </Typography>
          <Typography variant="caption" color="text.secondary">
            L moyen = {stats.mean.toFixed(3)} clients
          </Typography>
        </Paper>

        <Paper variant="outlined" sx={{ p: 2, bgcolor: 'info.50' }}>
          <Typography variant="caption" color="text.secondary">
            Coefficient de variation
          </Typography>
          <Typography variant="h6" fontWeight={600}>
            {stats.cv.toFixed(1)}%
          </Typography>
          <Typography variant="caption" color="text.secondary">
            {stats.cv < 10 ? 'Très stable' : stats.cv < 30 ? 'Stable' : 'Variable'}
          </Typography>
        </Paper>
      </Box>

      {/* Statistiques du régime permanent */}
      <Box sx={{ mb: 3, p: 2, bgcolor: 'grey.50', borderRadius: 1 }}>
        <Typography variant="subtitle2" fontWeight={600} gutterBottom>
          Statistiques du régime permanent:
        </Typography>
        <Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 2 }}>
          <Box>
            <Typography variant="caption" color="text.secondary">Moyenne</Typography>
            <Typography variant="body2" fontWeight={600}>{stats.mean.toFixed(3)}</Typography>
          </Box>
          <Box>
            <Typography variant="caption" color="text.secondary">Écart-type</Typography>
            <Typography variant="body2" fontWeight={600}>{stats.stdDev.toFixed(3)}</Typography>
          </Box>
          <Box>
            <Typography variant="caption" color="text.secondary">Minimum</Typography>
            <Typography variant="body2" fontWeight={600}>{stats.min}</Typography>
          </Box>
          <Box>
            <Typography variant="caption" color="text.secondary">Maximum</Typography>
            <Typography variant="body2" fontWeight={600}>{stats.max}</Typography>
          </Box>
        </Box>
      </Box>

      {/* Graphique */}
      <Box sx={{ height: 400 }}>
        <Line data={chartData} options={options} />
      </Box>

      {/* Explications */}
      <Box sx={{ mt: 3, p: 2, bgcolor: 'info.50', borderRadius: 1 }}>
        <Typography variant="caption" fontWeight={600} gutterBottom display="block" sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
          <MenuBookIcon fontSize="small" /> Interprétation:
        </Typography>
        <Typography variant="caption" component="div" sx={{ mb: 1 }}>
<strong>Courbe bleue:</strong> Nombre de clients dans le système au fil du temps
        </Typography>
        <Typography variant="caption" component="div" sx={{ mb: 1 }}>
<strong>Zone orange (gauche):</strong> Période de chauffe ({stats.warmupPoints} points) où le système démarre
        </Typography>
        <Typography variant="caption" component="div" sx={{ mb: 1 }}>
<strong>Zone bleue (droite):</strong> Régime permanent ({stats.steadyStatePoints} points) après {formatTime(stats.warmup, timeUnit)}
        </Typography>
        <Typography variant="caption" component="div" sx={{ mb: 1 }}>
<strong>Moyenne en régime permanent:</strong> L = {stats.mean.toFixed(3)} clients
        </Typography>
        <Typography variant="caption" component="div">
<strong>Coefficient de variation:</strong> {stats.cv.toFixed(1)}% - Plus il est faible, plus le système est stable
        </Typography>
      </Box>
    </Paper>
  );
}