气温走势图:多模型逐小时预测曲线 + API 新增 models_hourly 字段
city_payloads 新增 models_hourly 包含 per-model hourly_forecasts。LiveTemperatureThresholdChart 渲染多模型曲线替代点预测卡片。CityDetail 类型新增 models_hourly。
This commit is contained in:
@@ -101,6 +101,7 @@ type HourlyForecast = {
|
||||
localTime?: string | null;
|
||||
times: string[];
|
||||
temps: Array<number | null>;
|
||||
modelCurves?: Record<string, Array<number | null>>;
|
||||
} | null;
|
||||
|
||||
function buildModelCurves(row: ScanOpportunityRow | null, length: number, hourly: HourlyForecast) {
|
||||
@@ -133,6 +134,33 @@ function buildModelCurves(row: ScanOpportunityRow | null, length: number, hourly
|
||||
values,
|
||||
});
|
||||
}
|
||||
|
||||
// Per-model hourly curves from Open-Meteo multi-model API
|
||||
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 values = Array.from({ length }, (): number | null => null);
|
||||
hourly.times.forEach((t, i) => {
|
||||
const slot = parseTimeSlot(t);
|
||||
if (slot !== null && slot >= 0 && slot < length && i < modelTemps.length) {
|
||||
values[slot] = validNumber(modelTemps[i]);
|
||||
}
|
||||
});
|
||||
if (values.some((v) => v !== null)) {
|
||||
result.push({
|
||||
key: `model_curve_${model}`,
|
||||
label: model,
|
||||
source: "Multi-model hourly",
|
||||
color: modelColors[idx % modelColors.length],
|
||||
dashed: true,
|
||||
smooth: true,
|
||||
values,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -365,6 +393,7 @@ export function LiveTemperatureThresholdChart({
|
||||
localTime: json.local_time || null,
|
||||
times: json.hourly.times || [],
|
||||
temps: json.hourly.temps || [],
|
||||
modelCurves: json.models_hourly?.curves || undefined,
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -374,7 +403,13 @@ export function LiveTemperatureThresholdChart({
|
||||
const { data, series } = useMemo(() => buildEvidenceChart(row, hourly), [row, hourly]);
|
||||
const visibleData = useMemo(() => buildMovingWindowData(data, row, hourly), [data, row, hourly]);
|
||||
const threshold = validNumber(row?.target_threshold) ?? validNumber(row?.target_value);
|
||||
const modelSummaryCards = useMemo(() => buildModelSummaryCards(row), [row]);
|
||||
const modelSummaryCards = useMemo(() => {
|
||||
const cards = buildModelSummaryCards(row);
|
||||
// Exclude models that already show as hourly curves (from buildModelCurves)
|
||||
if (!hourly?.modelCurves) return cards;
|
||||
const curveKeys = new Set(Object.keys(hourly.modelCurves));
|
||||
return cards.filter((card) => !curveKeys.has(card.label));
|
||||
}, [row, hourly]);
|
||||
const tableRows = [...series, ...modelSummaryCards]
|
||||
.slice(0, 5)
|
||||
.map((item) => ({ ...item, ...seriesStats(item.values) }));
|
||||
|
||||
Reference in New Issue
Block a user