feat: add multiple weather data source scrapers and integrate dashboard temperature chart logic components
This commit is contained in:
@@ -3,6 +3,11 @@
|
||||
import { validNumber, type EvidenceSeries } from "@/components/dashboard/scan-terminal/temperature-chart-logic";
|
||||
|
||||
type TooltipSeries = Pick<EvidenceSeries, "key" | "label" | "color">;
|
||||
type TooltipRow = TooltipSeries & { value: number };
|
||||
|
||||
function isRunwayTooltipSeries(seriesKey: string) {
|
||||
return seriesKey.startsWith("runway_");
|
||||
}
|
||||
|
||||
function nearestSeriesValue(
|
||||
data: Array<Record<string, any>>,
|
||||
@@ -42,14 +47,7 @@ export function TemperatureTooltipContent({
|
||||
}) {
|
||||
if (!active || !payload?.length || !series.length) return null;
|
||||
const activePoint = payload[0]?.payload || {};
|
||||
const activeIndex = data.findIndex((point) => point.ts === activePoint.ts);
|
||||
const rows = series
|
||||
.map((item) => {
|
||||
const directValue = validNumber(activePoint[item.key]);
|
||||
const value = directValue ?? nearestSeriesValue(data, item.key, activeIndex);
|
||||
return value === null ? null : { ...item, value };
|
||||
})
|
||||
.filter((item): item is TooltipSeries & { value: number } => item !== null);
|
||||
const rows = buildTooltipRows(activePoint, data, series);
|
||||
if (!rows.length) return null;
|
||||
|
||||
return (
|
||||
@@ -69,3 +67,22 @@ export function TemperatureTooltipContent({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buildTooltipRows(
|
||||
activePoint: Record<string, any>,
|
||||
data: Array<Record<string, any>>,
|
||||
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);
|
||||
}
|
||||
|
||||
export const __buildTemperatureTooltipRowsForTest = buildTooltipRows;
|
||||
|
||||
+32
@@ -601,6 +601,38 @@ export function runTests() {
|
||||
"AMOS runway_obs patch should use the runway temperature, not ignore the SR/SL point",
|
||||
);
|
||||
|
||||
const busanSnapshotWithLocalAndUtc = __buildTemperatureChartDataForTest(
|
||||
{
|
||||
city: "busan",
|
||||
local_date: "2026-05-27",
|
||||
local_time: "09:58",
|
||||
tz_offset_seconds: 9 * 60 * 60,
|
||||
temp_symbol: "°C",
|
||||
} as any,
|
||||
{
|
||||
localTime: "09:58",
|
||||
times: ["00:00", "12:00", "18:00", "23:00"],
|
||||
temps: [19.6, 21.1, 20.0, 19.0],
|
||||
amos: {
|
||||
source: "amos",
|
||||
observation_time: "2026-05-27T00:58:00Z",
|
||||
observation_time_local: "2026-05-27 09:58:00",
|
||||
runway_obs: {
|
||||
runway_pairs: [["S R", "S L"]],
|
||||
temperatures: [[21.5, 12.4]],
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
"1D",
|
||||
);
|
||||
const busanSnapshotLabels = busanSnapshotWithLocalAndUtc.data
|
||||
.filter((point: any) => point[runwayKey("SR/SL")] === 21.5)
|
||||
.map((point: any) => point.label);
|
||||
assert(
|
||||
busanSnapshotLabels.includes("09:58:00"),
|
||||
"AMOS snapshot fallback should prefer UTC observation_time over naive observation_time_local for chart positioning",
|
||||
);
|
||||
|
||||
const busanCurrentOnly = __buildTemperatureChartDataForTest(
|
||||
{
|
||||
city: "busan",
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { __buildTemperatureTooltipRowsForTest } from "@/components/dashboard/scan-terminal/TemperatureTooltipContent";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
export function runTests() {
|
||||
const data = [
|
||||
{
|
||||
ts: Date.UTC(2026, 4, 27, 9, 10),
|
||||
label: "09:10:00",
|
||||
runway_35R_17L: 26,
|
||||
gfs: 24.2,
|
||||
deb: null,
|
||||
},
|
||||
{
|
||||
ts: Date.UTC(2026, 4, 27, 14, 0),
|
||||
label: "14:00:00",
|
||||
runway_35R_17L: null,
|
||||
gfs: null,
|
||||
deb: 26.7,
|
||||
},
|
||||
{
|
||||
ts: Date.UTC(2026, 4, 27, 19, 0),
|
||||
label: "19:00:00",
|
||||
runway_35R_17L: null,
|
||||
gfs: 24.8,
|
||||
deb: 23.5,
|
||||
},
|
||||
];
|
||||
const series = [
|
||||
{ key: "runway_35R_17L", label: "35R/17L 结算跑道", color: "#009688" },
|
||||
{ key: "gfs", label: "GFS", color: "#10b981" },
|
||||
{ key: "deb", label: "DEB Forecast", color: "#f97316" },
|
||||
];
|
||||
|
||||
const rows = __buildTemperatureTooltipRowsForTest(data[1], data, series);
|
||||
|
||||
assert(
|
||||
!rows.some((row) => row.key === "runway_35R_17L"),
|
||||
"runway tooltip rows should not use nearest-value fallback when the active x slot has no runway value",
|
||||
);
|
||||
assert(
|
||||
rows.some((row) => row.key === "deb" && row.value === 26.7),
|
||||
"tooltip should still show direct values at the active x slot",
|
||||
);
|
||||
assert(
|
||||
rows.some((row) => row.key === "gfs" && row.value === 24.2),
|
||||
"non-runway sparse series should keep nearest-value fallback",
|
||||
);
|
||||
}
|
||||
@@ -1069,7 +1069,7 @@ function buildRunwayHistorySeries(
|
||||
const pointTemps = runwayObs?.point_temperatures || [];
|
||||
const isAmosTempDewTuple = String(amos?.source || "").toLowerCase() === "amos";
|
||||
const anchor =
|
||||
getCityLocalUtcTimestamp(amos?.observation_time_local || amos?.observation_time || hourly?.localTime || row?.local_time, tzOffset, localDateStr) ??
|
||||
getCityLocalUtcTimestamp(amos?.observation_time || amos?.observation_time_local || hourly?.localTime || row?.local_time, tzOffset, localDateStr) ??
|
||||
getCityLocalUtcTimestamp(row?.local_time, tzOffset, localDateStr);
|
||||
|
||||
if (!anchor || !Array.isArray(runwayTemps)) return [];
|
||||
|
||||
Reference in New Issue
Block a user