"use client"; import { validNumber, type EvidenceSeries, type ProbabilityOverlay, } from "@/components/dashboard/scan-terminal/temperature-chart-logic"; type TooltipSeries = Pick; type TooltipRow = TooltipSeries & { value: number }; type TooltipProbabilityRow = { key: string; label: string; value: string; color: string }; function isRunwayTooltipSeries(seriesKey: string) { return seriesKey.startsWith("runway_"); } function nearestSeriesValue( data: Array>, seriesKey: string, activeIndex: number, ) { if (!data.length || activeIndex < 0) return null; for (let offset = 0; offset < data.length; offset += 1) { const left = activeIndex - offset; if (left >= 0) { const value = validNumber(data[left]?.[seriesKey]); if (value !== null) return value; } const right = activeIndex + offset; if (right < data.length) { const value = validNumber(data[right]?.[seriesKey]); if (value !== null) return value; } } return null; } export function TemperatureTooltipContent({ active, label, payload, data, series, probabilityOverlay, tempSymbol = "°C", isEn = false, }: { active?: boolean; label?: string | number; payload?: ReadonlyArray<{ payload?: Record }>; data: Array>; series: TooltipSeries[]; probabilityOverlay?: ProbabilityOverlay | null; tempSymbol?: string; isEn?: boolean; }) { if (!active) return null; const activePoint = payload?.[0]?.payload || findTooltipPointByLabel(data, label) || {}; const rows = series.length ? buildTooltipRows(activePoint, data, series) : []; const probabilityRows = buildTooltipProbabilityRows(probabilityOverlay, tempSymbol, isEn); if (!rows.length && !probabilityRows.length) return null; return (
{label}
{rows.length > 0 && (
{rows.slice(0, 8).map((item) => (
{item.label} {item.value.toFixed(2)}{tempSymbol}
))}
)} {probabilityRows.length > 0 && (
0 ? "mt-1.5 grid max-h-[260px] gap-1 overflow-y-auto border-t border-slate-100 pt-1.5 pr-1" : "grid max-h-[260px] gap-1 overflow-y-auto pr-1"}> {probabilityRows.map((item) => (
{item.label} {item.value}
))}
)}
); } function findTooltipPointByLabel( data: Array>, label?: string | number, ) { if (label === undefined || label === null) return null; return data.find((point) => String(point.label) === String(label)) || null; } function buildTooltipRows( activePoint: Record, data: Array>, series: TooltipSeries[], ): TooltipRow[] { const activeIndex = data.findIndex((point) => point.ts === activePoint.ts); return series .map((item) => { const directValue = validNumber(activePoint[item.key]); const value = directValue ?? ( isRunwayTooltipSeries(item.key) ? null : nearestSeriesValue(data, item.key, activeIndex) ); return value === null ? null : { ...item, value }; }) .filter((item): item is TooltipRow => item !== null); } function formatProbabilityRangeValue(value: number) { return Number(value.toFixed(1)).toString(); } function formatProbabilityRangeLabel( lower: number, upper: number, tempSymbol: string, ) { return `${formatProbabilityRangeValue(lower)}-${formatProbabilityRangeValue(upper)}${tempSymbol}`; } function buildTooltipProbabilityRows( probabilityOverlay: ProbabilityOverlay | null | undefined, tempSymbol: string, isEn: boolean, ): TooltipProbabilityRow[] { if (!probabilityOverlay) return []; const rows: TooltipProbabilityRow[] = []; const mu = validNumber(probabilityOverlay.muLine?.value); if (mu !== null) { rows.push({ key: "legacy_probability_mu", label: isEn ? "Gaussian μ" : "高斯 μ", value: `${mu.toFixed(1)}${tempSymbol}`, color: "#8b5cf6", }); } rows.push( ...[...probabilityOverlay.bands] .filter((band) => validNumber(band.probability) !== null && band.probability > 0) .sort((a, b) => a.lower - b.lower) .map((band) => { const probabilityPct = Math.round(band.probability * 100); return { key: band.key, label: formatProbabilityRangeLabel(band.lower, band.upper, tempSymbol), value: `${probabilityPct}%`, color: "#a78bfa", }; }), ); return rows; } export const __buildTemperatureTooltipRowsForTest = buildTooltipRows; export const __buildTemperatureTooltipProbabilityRowsForTest = buildTooltipProbabilityRows;