From 63038cfaea58f354029d1c9c5ec7822da99a3dc2 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Thu, 11 Jun 2026 20:31:07 +0800 Subject: [PATCH] fix multi-model chart time axis --- .../__tests__/temperatureChartData.test.ts | 30 ++++++++++++++++ .../scan-terminal/temperature-chart-logic.ts | 34 ++++++++++++++++--- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/frontend/components/dashboard/scan-terminal/__tests__/temperatureChartData.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/temperatureChartData.test.ts index 5ddc7e8d..fd99d2f6 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/temperatureChartData.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/temperatureChartData.test.ts @@ -84,6 +84,36 @@ export function runTests() { assert(correctedDebSeries, "corrected hourly DEB path should still render as DEB Forecast"); assert(correctedDebValues.includes(28.0), `chart should use backend deb.hourly_path before rebuilding a shifted curve; got ${correctedDebValues.join(",")}`); + const independentModelTimelineChart = buildFullDayChartData( + { + city: "madrid", + local_date: "2026-06-11", + local_time: "20:00", + temp_symbol: "°C", + tz_offset_seconds: 2 * 3600, + } as any, + { + forecastTodayHigh: 31, + debPrediction: 31, + localDate: "2026-06-11", + localTime: "20:00", + times: ["08:00", "09:00", "10:00"], + temps: [20, 25, 22], + modelTimes: ["15:00", "16:00", "17:00"], + modelCurves: { + ECMWF: [24, 32, 25], + }, + } as any, + true, + ); + const independentModelPeak = independentModelTimelineChart.data.find( + (point) => point.model_curve_ECMWF === 32, + ); + assert( + independentModelPeak?.label === "16:00:00", + `multi-model curves must use models_hourly.times instead of hourly.times; got ${independentModelPeak?.label}`, + ); + const correctedDetailChart = getTemperatureChartData( { name: "shanghai", diff --git a/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts b/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts index 7f8acb16..913a87b9 100644 --- a/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts +++ b/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts @@ -1199,6 +1199,7 @@ type HourlyForecast = { localTime?: string | null; times: string[]; temps: Array; + modelTimes?: string[]; modelCurves?: Record>; runwayPlateHistory?: Record>>; runwayBandHistory?: Array<{ time: string; high_temp: number; low_temp: number; avg_temp: number }>; @@ -1253,6 +1254,7 @@ function seedHourlyForecastFromRow(row: ScanOpportunityRow | null): HourlyForeca localTime: row.local_time || null, times: [], temps: [], + modelTimes: undefined, modelCurves: undefined, runwayPlateHistory: seededRunwayPlateHistory, runwayBandHistory: undefined, @@ -1368,6 +1370,7 @@ function parseHourlyForecastFromCityDetail(json: CityDetail | null): HourlyForec localTime: json.local_time || null, times: hourlySource.times || [], temps: hourlySource.temps || [], + modelTimes: (json.models_hourly ?? (json as any)?.timeseries?.models_hourly)?.times || undefined, modelCurves: (json.models_hourly ?? (json as any)?.timeseries?.models_hourly)?.curves || undefined, runwayPlateHistory: (json as any)?.runway_plate_history || (json.amos as any)?.runway_plate_history || undefined, runwayBandHistory: (json as any)?.runway_band_history || undefined, @@ -2161,6 +2164,14 @@ function addHourlyTimesToTimeline( }); } +function resolveModelCurveTimes( + hourly: HourlyForecast, + modelTemps: Array, +) { + if (hourly?.modelTimes?.length) return hourly.modelTimes; + return hourly?.times?.length === modelTemps.length ? hourly.times : []; +} + function probabilityBucketValue(bucket: ProbabilityBucket) { return validNumber(bucket.value ?? (bucket as any).temp ?? (bucket as any).temperature); } @@ -2354,9 +2365,16 @@ function buildFullDayChartData( if (debTimes.length && debTemps.length) { addHourlyTimesToTimeline(timelineSet, debTimes, debTemps, tzOffset, localDateStr, localDayBounds); } - if (hourly?.times?.length && hourly?.modelCurves) { + if (hourly?.modelCurves) { Object.values(hourly.modelCurves).forEach((modelTemps) => { - addHourlyTimesToTimeline(timelineSet, hourly.times, modelTemps, tzOffset, localDateStr, localDayBounds); + addHourlyTimesToTimeline( + timelineSet, + resolveModelCurveTimes(hourly, modelTemps), + modelTemps, + tzOffset, + localDateStr, + localDayBounds, + ); }); } @@ -2460,12 +2478,20 @@ function buildFullDayChartData( } // Per-model curves - if (hourly?.times?.length && hourly.modelCurves) { + if (hourly?.modelCurves) { const modelColors = ["#2563eb", "#7c3aed", "#059669", "#d97706", "#dc2626", "#0891b2"]; Object.keys(hourly.modelCurves).forEach((model, idx) => { const modelTemps = hourly.modelCurves![model]; if (!modelTemps?.length) return; - const vals = valuesForHourlyTimes(n, indexByTs, hourly.times, modelTemps, tzOffset, localDateStr, localDayBounds); + const vals = valuesForHourlyTimes( + n, + indexByTs, + resolveModelCurveTimes(hourly, modelTemps), + modelTemps, + tzOffset, + localDateStr, + localDayBounds, + ); if (vals.some((v) => v !== null)) { series.push({ key: `model_curve_${model}`,