'use client'; import { useState } from 'react'; import Link from 'next/link'; import dynamic from 'next/dynamic'; import { DefinitionRankingWithHistory } from '@/src/services/modelService'; import { ChevronDown, ChevronRight } from 'lucide-react'; const Plot = dynamic(() => import('react-plotly.js'), { ssr: false }); interface ModelPerformanceChartsProps { definitionRankings: DefinitionRankingWithHistory[]; } export default function ModelPerformanceCharts({ definitionRankings }: ModelPerformanceChartsProps) { const [expandedDefinitions, setExpandedDefinitions] = useState>({}); const toggleDefinition = (uniqueKey: string) => { setExpandedDefinitions(prev => ({ ...prev, [uniqueKey]: !prev[uniqueKey] })); }; const formatFrequencyHorizon = (scopeId: string) => { // Remove everything after comma const cleanedScopeId = scopeId.split(',')[0].trim(); // Split by :: const [frequencyPart, horizonPart] = cleanedScopeId.split('::'); // Parse frequency (format: H:MM:SS) const [hours, minutes, seconds] = frequencyPart.split(':').map(v => parseInt(v, 10)); let frequencyDisplay = ''; if (hours > 0) { frequencyDisplay = `${hours}h`; } else if (minutes > 0) { frequencyDisplay = `${minutes}min`; } else if (seconds > 0) { frequencyDisplay = `${seconds}s`; } else { frequencyDisplay = 'instant'; } return { frequency: frequencyDisplay, horizon: horizonPart?.trim() || 'N/A' }; }; const getChartTitle = (definition: DefinitionRankingWithHistory) => { if (definition.scope_type === 'global') { return 'Overall Ranking'; } else if (definition.scope_type === 'definition') { return definition.definition_name; } else if (definition.scope_type === 'frequency_horizon') { const { frequency, horizon } = formatFrequencyHorizon(definition.scope_id); return (
Horizon: {horizon}, Frequency: {frequency} ({definition.scope_id})
); } return definition.scope_type; // fallback }; if (!definitionRankings || definitionRankings.length === 0) { return (
No ranking data available for this model.
); } // Sort rankings to show global first, then definitions (alphabetically), then frequency_horizon const sortedRankings = [...definitionRankings].sort((a, b) => { // Global always comes first if (a.scope_type === 'global') return -1; if (b.scope_type === 'global') return 1; // Frequency_horizon always comes last if (a.scope_type === 'frequency_horizon') return 1; if (b.scope_type === 'frequency_horizon') return -1; // Both are definitions, sort alphabetically by name if (a.scope_type === 'definition' && b.scope_type === 'definition') { return a.definition_name.localeCompare(b.definition_name); } return 0; }); // Group rankings by scope_type const groupedRankings = sortedRankings.reduce((acc, ranking) => { if (!acc[ranking.scope_type]) { acc[ranking.scope_type] = []; } acc[ranking.scope_type].push(ranking); return acc; }, {} as Record); const renderSectionHeader = (scopeType: string) => { if (scopeType === 'global') return null; if (scopeType === 'definition') { return (

Challenge Definitions

Rankings evaluated for each individual challenge definition.

); } if (scopeType === 'frequency_horizon') { return (

Frequency & Horizon Combinations

Rankings evaluated across different forecast frequency and horizon configurations.

); } return null; }; return (
{['global', 'definition', 'frequency_horizon'].map((scopeType) => { const rankings = groupedRankings[scopeType]; if (!rankings || rankings.length === 0) return null; return (
{renderSectionHeader(scopeType)} {rankings.map((definition) => { const uniqueKey = `${definition.scope_type}-${definition.scope_id}`; const isExpanded = expandedDefinitions[uniqueKey]; const dailyRankings = definition.daily_rankings || []; const chartTitle = getChartTitle(definition); // Sort rankings by date const sortedRankings = [...dailyRankings].sort((a, b) => new Date(a.calculation_date).getTime() - new Date(b.calculation_date).getTime() ); // Prepare data for Plotly const dates = sortedRankings.map(r => r.calculation_date); const eloScores = sortedRankings.map(r => r.elo_score); const eloUpper = sortedRankings.map(r => r.elo_ci_upper); const eloLower = sortedRankings.map(r => r.elo_ci_lower); const ranks = sortedRankings.map(r => r.rank_position); return (
{isExpanded && sortedRankings.length > 0 && (
Date: %{x}
' + 'ELO Score: %{y:.1f}
' + 'Rank: #%{customdata}
' + '', }, ] as any} layout={{ title: 'ELO Score Over Time', xaxis: { title: 'Date', gridcolor: '#e5e7eb', showgrid: true, }, yaxis: { title: 'ELO Score', gridcolor: '#e5e7eb', showgrid: true, range: [0, Math.max(...eloUpper, ...eloScores, ...eloLower) * 1.05], }, hovermode: 'closest', showlegend: true, legend: { x: 0, y: 1, bgcolor: 'rgba(255, 255, 255, 0.8)', }, margin: { l: 60, r: 40, t: 50, b: 60 }, paper_bgcolor: 'white', plot_bgcolor: 'white', } as any} config={{ responsive: true, displayModeBar: true, displaylogo: false, modeBarButtonsToRemove: ['lasso2d', 'select2d'], }} style={{ width: '100%', height: '400px' }} /> {/* Current stats */}
Current Rank
#{sortedRankings[sortedRankings.length - 1].rank_position}
Current ELO
{sortedRankings[sortedRankings.length - 1].elo_score.toFixed(1)}
95% CI
[{sortedRankings[sortedRankings.length - 1].elo_ci_lower.toFixed(0)}, {sortedRankings[sortedRankings.length - 1].elo_ci_upper.toFixed(0)}]
)} {isExpanded && sortedRankings.length === 0 && (
No ranking history available for this challenge.
)}
); })}
); })}
); }