fix multi-model chart time axis
This commit is contained in:
@@ -84,6 +84,36 @@ export function runTests() {
|
|||||||
assert(correctedDebSeries, "corrected hourly DEB path should still render as DEB Forecast");
|
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(",")}`);
|
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(
|
const correctedDetailChart = getTemperatureChartData(
|
||||||
{
|
{
|
||||||
name: "shanghai",
|
name: "shanghai",
|
||||||
|
|||||||
@@ -1199,6 +1199,7 @@ type HourlyForecast = {
|
|||||||
localTime?: string | null;
|
localTime?: string | null;
|
||||||
times: string[];
|
times: string[];
|
||||||
temps: Array<number | null>;
|
temps: Array<number | null>;
|
||||||
|
modelTimes?: string[];
|
||||||
modelCurves?: Record<string, Array<number | null>>;
|
modelCurves?: Record<string, Array<number | null>>;
|
||||||
runwayPlateHistory?: Record<string, Array<Record<string, unknown>>>;
|
runwayPlateHistory?: Record<string, Array<Record<string, unknown>>>;
|
||||||
runwayBandHistory?: Array<{ time: string; high_temp: number; low_temp: number; avg_temp: number }>;
|
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,
|
localTime: row.local_time || null,
|
||||||
times: [],
|
times: [],
|
||||||
temps: [],
|
temps: [],
|
||||||
|
modelTimes: undefined,
|
||||||
modelCurves: undefined,
|
modelCurves: undefined,
|
||||||
runwayPlateHistory: seededRunwayPlateHistory,
|
runwayPlateHistory: seededRunwayPlateHistory,
|
||||||
runwayBandHistory: undefined,
|
runwayBandHistory: undefined,
|
||||||
@@ -1368,6 +1370,7 @@ function parseHourlyForecastFromCityDetail(json: CityDetail | null): HourlyForec
|
|||||||
localTime: json.local_time || null,
|
localTime: json.local_time || null,
|
||||||
times: hourlySource.times || [],
|
times: hourlySource.times || [],
|
||||||
temps: hourlySource.temps || [],
|
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,
|
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,
|
runwayPlateHistory: (json as any)?.runway_plate_history || (json.amos as any)?.runway_plate_history || undefined,
|
||||||
runwayBandHistory: (json as any)?.runway_band_history || undefined,
|
runwayBandHistory: (json as any)?.runway_band_history || undefined,
|
||||||
@@ -2161,6 +2164,14 @@ function addHourlyTimesToTimeline(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveModelCurveTimes(
|
||||||
|
hourly: HourlyForecast,
|
||||||
|
modelTemps: Array<number | null>,
|
||||||
|
) {
|
||||||
|
if (hourly?.modelTimes?.length) return hourly.modelTimes;
|
||||||
|
return hourly?.times?.length === modelTemps.length ? hourly.times : [];
|
||||||
|
}
|
||||||
|
|
||||||
function probabilityBucketValue(bucket: ProbabilityBucket) {
|
function probabilityBucketValue(bucket: ProbabilityBucket) {
|
||||||
return validNumber(bucket.value ?? (bucket as any).temp ?? (bucket as any).temperature);
|
return validNumber(bucket.value ?? (bucket as any).temp ?? (bucket as any).temperature);
|
||||||
}
|
}
|
||||||
@@ -2354,9 +2365,16 @@ function buildFullDayChartData(
|
|||||||
if (debTimes.length && debTemps.length) {
|
if (debTimes.length && debTemps.length) {
|
||||||
addHourlyTimesToTimeline(timelineSet, debTimes, debTemps, tzOffset, localDateStr, localDayBounds);
|
addHourlyTimesToTimeline(timelineSet, debTimes, debTemps, tzOffset, localDateStr, localDayBounds);
|
||||||
}
|
}
|
||||||
if (hourly?.times?.length && hourly?.modelCurves) {
|
if (hourly?.modelCurves) {
|
||||||
Object.values(hourly.modelCurves).forEach((modelTemps) => {
|
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
|
// Per-model curves
|
||||||
if (hourly?.times?.length && hourly.modelCurves) {
|
if (hourly?.modelCurves) {
|
||||||
const modelColors = ["#2563eb", "#7c3aed", "#059669", "#d97706", "#dc2626", "#0891b2"];
|
const modelColors = ["#2563eb", "#7c3aed", "#059669", "#d97706", "#dc2626", "#0891b2"];
|
||||||
Object.keys(hourly.modelCurves).forEach((model, idx) => {
|
Object.keys(hourly.modelCurves).forEach((model, idx) => {
|
||||||
const modelTemps = hourly.modelCurves![model];
|
const modelTemps = hourly.modelCurves![model];
|
||||||
if (!modelTemps?.length) return;
|
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)) {
|
if (vals.some((v) => v !== null)) {
|
||||||
series.push({
|
series.push({
|
||||||
key: `model_curve_${model}`,
|
key: `model_curve_${model}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user