import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/src/components/ui/card";
import { Loader2 } from "lucide-react";
import { useScoreAnalytics } from "../ScoreAnalyticsProvider";
import { Heatmap } from "../charts/Heatmap";
import { HeatmapLegend } from "../charts/HeatmapLegend";
import { HeatmapSkeleton } from "../charts/HeatmapSkeleton";
import { getHeatmapCellColor } from "@/src/features/score-analytics/lib/color-scales";
import { type HeatmapCell } from "@/src/features/score-analytics/lib/heatmap-utils";
import { useCallback } from "react";
import { SamplingDetailsHoverCard } from "../SamplingDetailsHoverCard";
import { type ScoreDataTypeType } from "@langfuse/shared";
interface HeatmapTooltipContentProps {
cell: HeatmapCell;
dataType: ScoreDataTypeType;
score1: { name: string; source: string };
score2: { name: string; source: string } | undefined;
score1Color: string;
score2Color: string;
totalMatchedPairs: number;
}
/**
* HeatmapTooltipContent - Renders tooltip content for heatmap cells
*
* Displays different information based on data type:
* - Numeric: Shows bin ranges and value distributions
* - Categorical: Shows category pairs and agreement metrics
*/
function HeatmapTooltipContent({
cell,
dataType,
score1,
score2,
score1Color,
score2Color,
totalMatchedPairs,
}: HeatmapTooltipContentProps) {
const percentage = (cell.metadata?.percentage as number) ?? 0;
return (
{/* Header Section */}
{dataType === "NUMERIC"
? `Bin ${cell.row}×${cell.col}`
: `${cell.metadata?.rowCategory as string} → ${cell.metadata?.colCategory as string}`}
{/* Primary Metrics Section */}
{cell.value.toLocaleString()} observations
{percentage.toFixed(1)}% of {totalMatchedPairs.toLocaleString()}{" "}
matched pairs
{/* Secondary Info Section */}
{dataType === "NUMERIC" ? (
<>
{score1.name} ({score1.source})
{(cell.metadata?.yRange as [number, number])?.[0]?.toFixed(2)} -{" "}
{(cell.metadata?.yRange as [number, number])?.[1]?.toFixed(2)}
{score2?.name} ({score2?.source})
{(cell.metadata?.xRange as [number, number])?.[0]?.toFixed(2)} -{" "}
{(cell.metadata?.xRange as [number, number])?.[1]?.toFixed(2)}
>
) : (
<>
{score1.name} ({score1.source})
{score2?.name} ({score2?.source})
>
)}
);
}
/**
* HeatmapCard - Smart card component for displaying score comparison heatmaps
*
* Consumes ScoreAnalyticsProvider context and displays:
* - Numeric scores: 10x10 bin heatmap showing correlation patterns
* - Categorical/Boolean: Confusion matrix showing agreement
* - Placeholder in single-score mode
*
* Handles:
* - Loading states
* - Empty states
* - Single vs two-score modes (only shows in two-score mode)
* - Numeric vs categorical data types
*/
export function HeatmapCard() {
const { data, isLoading, params, getColorForScore } = useScoreAnalytics();
// Compute max value for color scaling (must be before early returns)
const maxValue =
data?.heatmap && data.heatmap.cells && data.heatmap.cells.length > 0
? "maxValue" in data.heatmap && typeof data.heatmap.maxValue === "number"
? data.heatmap.maxValue
: Math.max(...data.heatmap.cells.map((c: HeatmapCell) => c.value))
: 0;
// Create color function using score1's color (must be before early returns)
const getColor = useCallback(
(cell: HeatmapCell) => {
return getHeatmapCellColor(1, cell.value, 0, maxValue);
},
[maxValue],
);
// Loading state
if (isLoading) {
return (
Score Comparison
Loading heatmap...
);
}
// No data state
if (!data) {
return (
Score Comparison
No data available
Select a score to view comparison
);
}
const { heatmap, metadata, statistics } = data;
const { mode, dataType } = metadata;
const { score1, score2 } = params;
// Get total matched pairs for tooltip context
const totalMatchedPairs = statistics.comparison?.matchedCount ?? 0;
const title =
dataType === "NUMERIC" ? "Score Comparison Heatmap" : "Confusion Matrix";
const description =
mode === "single"
? dataType === "NUMERIC"
? "Distribution of matched score pairs showing correlation patterns"
: "Agreement matrix between categorical scores"
: dataType === "NUMERIC"
? `${totalMatchedPairs.toLocaleString()} matched pairs showing correlation patterns`
: `${totalMatchedPairs.toLocaleString()} matched pairs showing agreement`;
// Single score mode - show placeholder
if (mode === "single") {
return (
{title}
{description}
Select a second score to view comparison heatmap
);
}
// Two score mode - show heatmap or empty state
const hasData = heatmap && heatmap.cells.length > 0;
// Calculate dynamic cell height based on available space
// Magic number 230px represents approximate available height for grid
// (card height minus header, labels, legend, gaps)
const numRows =
dataType === "NUMERIC"
? 10
: heatmap && "rows" in heatmap
? heatmap.rows
: 10;
const calculatedCellHeight = Math.floor(200 / numRows);
return (
{title}
{data.samplingMetadata.isSampled && (
)}
{description}
{hasData && (
)}
{/* Placeholder to align with tabs in other cards */}
{hasData ? (
(
)}
/>
) : (
)}
);
}