Nora Petrova
Add project to new space
20e666e
raw
history blame
18.5 kB
// components/MetricsBreakdown.jsx
"use client";
import React, { useState, useEffect, useMemo } from "react";
import {
RadarChart,
PolarGrid,
PolarAngleAxis,
PolarRadiusAxis,
Radar,
Tooltip as RechartsTooltip, // Renamed to avoid conflict with local Tooltip
Legend,
ResponsiveContainer,
} from "recharts";
import { getScoreColor, getMetricTooltip } from "../lib/utils";
import { Tooltip } from "./Tooltip"; // Your custom Tooltip component for headers etc.
// Component receives processed metrics data, model metadata, and category radar data
const MetricsBreakdown = ({
metricsData,
modelsMeta,
radarData: categoryRadarDataProp, // Already processed radar data for categories
}) => {
const [subTab, setSubTab] = useState("categories"); // 'categories' or 'metrics'
const [selectedModels, setSelectedModels] = useState([]);
// console.log("Metrics Data in Breakdown:", metricsData); // For debugging
// console.log("Models Meta in Breakdown:", modelsMeta);
// console.log("Category Radar Data Prop:", categoryRadarDataProp);
// Extract data from props with defaults
const { highLevelCategories, lowLevelMetrics } = metricsData || {
highLevelCategories: {},
lowLevelMetrics: {},
};
// Use modelsMeta directly for clarity, aliasing if preferred
const models = modelsMeta || [];
// Get sorted lists of category and metric names
const sortedCategoryNames = useMemo(
() =>
Object.keys(highLevelCategories || {}).sort((a, b) => a.localeCompare(b)),
[highLevelCategories]
);
const sortedMetricNames = useMemo(
() => Object.keys(lowLevelMetrics || {}).sort((a, b) => a.localeCompare(b)),
[lowLevelMetrics]
);
// Initialize selections
useEffect(() => {
if (selectedModels.length === 0 && models.length > 0) {
setSelectedModels(models.map((m) => m.model));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [models]); // Only depends on models changing/loading
// --- Memoized data generation functions ---
// Radar data for LL Metrics (used when subTab === 'metrics') - CORRECTED ACCESSORS
const metricRadarData = useMemo(() => {
if (
!lowLevelMetrics ||
models.length === 0 ||
sortedMetricNames.length === 0
)
return [];
return sortedMetricNames.map((metricName) => {
const entry = { category: metricName }; // Use metric name as the axis category
const metricData = lowLevelMetrics[metricName];
if (metricData) {
models
.filter((m) => selectedModels.includes(m.model))
.forEach((model) => {
// Use correct camelCase keys
entry[model.model] =
Number(metricData.modelScores?.[model.model]?.nationalScore) || 0;
// Standard deviation per metric is NOT available, so we don't add it here
});
}
return entry;
});
}, [lowLevelMetrics, models, selectedModels, sortedMetricNames]);
// Custom tooltip (common for both radar charts) - CORRECTED (removed std dev logic)
const CustomRadarTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="bg-white p-3 border rounded shadow-lg max-w-xs opacity-95">
<p className="font-medium mb-1 text-gray-800">{label}</p>
{/* Get tooltip description for the category/metric itself */}
<p className="text-xs mb-3 text-gray-600 border-b pb-2">
{getMetricTooltip(label)}
</p>
<div className="space-y-1">
{payload
// Sort models by score within tooltip
.sort((a, b) => (b.value || 0) - (a.value || 0))
.map((entry) => (
<div
key={entry.dataKey} // dataKey is the model name here
className="flex items-center text-sm"
>
<div
className="w-2.5 h-2.5 rounded-full mr-2 flex-shrink-0"
style={{ backgroundColor: entry.color || "#8884d8" }}
></div>
<span className="mr-1 truncate flex-grow text-gray-700">
{entry.name}: {/* name is also the model name */}
</span>
<span className="font-medium flex-shrink-0 text-gray-900">
{/* Ensure value exists and format */}
{entry.value !== null && entry.value !== undefined
? Number(entry.value).toFixed(1)
: "N/A"}
{/* Removed standard deviation display */}
</span>
</div>
))}
</div>
</div>
);
}
return null;
};
// Use the radar data passed via prop for categories view, filtered by selected models - CORRECTED (removed std dev logic)
const filteredCategoryRadarData = useMemo(() => {
if (!categoryRadarDataProp || models.length === 0) return [];
// Filter based on selected models, removing std dev keys
return categoryRadarDataProp.map((item) => {
const newItem = { category: item.category };
models
.filter((m) => selectedModels.includes(m.model))
.forEach((model) => {
// We only need the model score itself for the radar data
newItem[model.model] = item[model.model] ?? 0; // Use nullish coalescing for default
});
return newItem;
});
}, [categoryRadarDataProp, models, selectedModels]);
return (
<>
{/* Top Controls: Model Selector & Sub-Tab Pills (No changes needed) */}
<div className="mb-6 flex flex-col md:flex-row justify-between items-center gap-4">
{/* Sub-Tab Pills */}
<div className="flex space-x-1 p-1 bg-gray-200 rounded-lg">
{" "}
<button
aria-pressed={subTab === "categories"}
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-colors duration-150 ${
subTab === "categories"
? "bg-white shadow text-blue-600"
: "text-gray-600 hover:text-gray-800"
}`}
onClick={() => setSubTab("categories")}
>
{" "}
High-Level Categories{" "}
</button>{" "}
<button
aria-pressed={subTab === "metrics"}
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-colors duration-150 ${
subTab === "metrics"
? "bg-white shadow text-blue-600"
: "text-gray-600 hover:text-gray-800"
}`}
onClick={() => setSubTab("metrics")}
>
{" "}
Low-Level Metrics{" "}
</button>{" "}
</div>
{/* Model Selector */}
<div className="flex items-center flex-wrap gap-1">
{" "}
<span className="text-sm text-gray-500 mr-2">Models:</span>{" "}
{models?.map((model) => (
<button
key={model.model}
className={`px-2 py-0.5 text-xs rounded border ${
selectedModels.includes(model.model)
? "bg-sky-100 text-sky-800 border-sky-300 font-medium"
: "bg-gray-100 text-gray-600 border-gray-300 hover:bg-gray-200"
}`}
onClick={() => {
if (selectedModels.includes(model.model)) {
if (selectedModels.length > 1) {
setSelectedModels(
selectedModels.filter((m) => m !== model.model)
);
}
} else {
setSelectedModels([...selectedModels, model.model]);
}
}}
>
{" "}
{model.model}{" "}
</button>
))}{" "}
</div>
</div>
{/* Conditional content based on sub-tab */}
{subTab === "categories" && (
<div className="space-y-6">
{/* CATEGORIES VIEW */}
{/* Summary Table: Models as Rows, Categories as Columns - CORRECTED ACCESSORS */}
<div className="border rounded-lg overflow-hidden shadow-sm">
<div className="px-4 py-3 bg-gray-50 border-b">
<h3 className="font-semibold text-gray-800">
Category Performance Summary
</h3>
</div>
<div className="p-4 overflow-x-auto">
{sortedCategoryNames.length > 0 ? (
<table className="min-w-full divide-y divide-gray-200 border border-gray-200">
<thead>
<tr className="bg-gray-100">
<th
scope="col"
className="sticky left-0 bg-gray-100 px-3 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider z-10"
>
Model
</th>
{sortedCategoryNames.map((catName) => (
<th
key={catName}
scope="col"
className="px-3 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider whitespace-nowrap"
>
{catName}
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{models
?.filter((m) => selectedModels.includes(m.model))
.map((model, idx) => (
<tr
key={model.model}
className={
idx % 2 === 0
? "bg-white hover:bg-gray-50"
: "bg-gray-50 hover:bg-gray-100"
}
>
<td className="sticky left-0 bg-inherit px-3 py-2 whitespace-nowrap z-10 text-left">
{" "}
{/* Keep sticky styles */}
<div className="flex items-center">
<div
className="w-3 h-3 rounded-full mr-2 shrink-0"
style={{ backgroundColor: model.color }}
></div>
<span className="text-sm font-medium">
{model.model}
</span>
</div>
</td>
{sortedCategoryNames.map((catName) => {
// Use correct camelCase keys
const scoreData =
highLevelCategories[catName]?.modelScores?.[
model.model
];
const score = scoreData?.nationalScore; // Access camelCase key
const displayScore =
score !== null && score !== undefined
? Number(score).toFixed(1)
: "N/A";
return (
<td
key={catName}
className="px-3 py-2 whitespace-nowrap text-center"
>
<div
className={`text-sm ${
displayScore === "N/A"
? "text-gray-400"
: getScoreColor(score)
}`}
>
{displayScore}
</div>
</td>
);
})}
</tr>
))}
</tbody>
</table>
) : (
<p className="text-center text-gray-500 py-4">
No category data available.
</p>
)}
</div>
</div>
{/* Radar Chart for Categories (Uses filteredCategoryRadarData) */}
<div className="border rounded-lg overflow-hidden shadow-sm">
<div className="px-4 py-3 bg-gray-50 border-b flex justify-between items-center">
<h3 className="font-semibold text-gray-800">
Performance Across Categories
</h3>
<div className="text-xs text-gray-500">
National Average Scores
</div>
</div>
<div className="p-4">
{filteredCategoryRadarData &&
filteredCategoryRadarData.length > 0 ? (
<div className="h-96 md:h-[450px]">
<ResponsiveContainer width="100%" height="100%">
<RadarChart
outerRadius="80%"
data={filteredCategoryRadarData}
>
<PolarGrid gridType="polygon" stroke="#e5e7eb" />
<PolarAngleAxis
dataKey="category"
tick={{ fill: "#4b5563", fontSize: 12 }}
/>
<PolarRadiusAxis
angle={90}
domain={[0, 100]}
axisLine={false}
tick={{ fill: "#6b7280", fontSize: 10 }}
/>
{models
?.filter((m) => selectedModels.includes(m.model))
.map((model) => (
<Radar
key={model.model}
name={model.model}
dataKey={model.model}
stroke={model.color}
fill={model.color}
fillOpacity={0.1}
strokeWidth={2}
/>
))}
{/* Use the corrected CustomRadarTooltip */}
<RechartsTooltip content={<CustomRadarTooltip />} />
<Legend
iconSize={10}
wrapperStyle={{ fontSize: "12px", paddingTop: "20px" }}
/>
</RadarChart>
</ResponsiveContainer>
</div>
) : (
<p className="text-center text-gray-500 py-4">
Radar data not available.
</p>
)}
<p className="text-xs text-gray-500 mt-4">
This radar chart visualizes how each model performs across
different high-level evaluation categories. The further out on
each axis, the better the performance on that category.
</p>
</div>
</div>
</div>
)}
{subTab === "metrics" && (
<div className="space-y-6">
{/* METRICS VIEW */}
{/* Radar Chart for Metrics (Uses metricRadarData) */}
<div className="border rounded-lg overflow-hidden shadow-sm">
<div className="px-4 py-3 bg-gray-50 border-b flex justify-between items-center">
<h3 className="font-semibold text-gray-800">
Performance Across All Metrics
</h3>
<div className="text-xs text-gray-500">
National Average Scores
</div>
</div>
<div className="p-4">
{metricRadarData.length > 0 ? (
<div className="h-96 md:h-[600px]">
{" "}
{/* Increased height */}
<ResponsiveContainer width="100%" height="100%">
<RadarChart outerRadius="80%" data={metricRadarData}>
{" "}
{/* Use metricRadarData */}
<PolarGrid gridType="polygon" stroke="#e5e7eb" />
<PolarAngleAxis
dataKey="category"
tick={{ fill: "#4b5563", fontSize: 10 }}
/>{" "}
{/* Adjusted font size */}
<PolarRadiusAxis
angle={90}
domain={[0, 100]}
axisLine={false}
tick={{ fill: "#6b7280", fontSize: 10 }}
/>
{models
?.filter((m) => selectedModels.includes(m.model))
.map((model) => (
<Radar
key={model.model}
name={model.model}
dataKey={model.model}
stroke={model.color}
fill={model.color}
fillOpacity={0.1}
strokeWidth={2}
/>
))}
{/* Use the corrected CustomRadarTooltip */}
<RechartsTooltip content={<CustomRadarTooltip />} />
<Legend
iconSize={10}
wrapperStyle={{ fontSize: "12px", paddingTop: "20px" }}
/>
</RadarChart>
</ResponsiveContainer>
</div>
) : (
<p className="text-center text-gray-500 py-4">
Metric data not available for radar chart.
</p>
)}
<p className="text-xs text-gray-500 mt-4">
This radar chart visualizes how each model performs across
different low-level metrics. The further out on each axis, the
better the performance on that metric.
</p>
</div>
</div>
{/* Optional: Add a table summary for low-level metrics similar to the categories one if desired */}
</div>
)}
</>
);
};
export default MetricsBreakdown;