feat: implement chart utilities for temperature forecasting, calibration, and observation processing
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
import { getTemperatureChartData } from "@/lib/chart-utils";
|
||||||
|
import type { CityDetail } from "@/lib/dashboard-types";
|
||||||
|
|
||||||
|
function assert(condition: unknown, message: string) {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runTests() {
|
||||||
|
const chartData = getTemperatureChartData(
|
||||||
|
{
|
||||||
|
name: "test-city",
|
||||||
|
display_name: "Test City",
|
||||||
|
local_date: "2026-05-16",
|
||||||
|
local_time: "10:00",
|
||||||
|
temp_symbol: "°C",
|
||||||
|
hourly: {
|
||||||
|
times: [
|
||||||
|
"2026-05-16T08:00:00+08:00",
|
||||||
|
"2026-05-16T09:00:00+08:00",
|
||||||
|
"2026-05-16T10:00:00+08:00",
|
||||||
|
"2026-05-16T11:00:00+08:00",
|
||||||
|
],
|
||||||
|
temps: [20, 22, 24, 26],
|
||||||
|
},
|
||||||
|
forecast: { today_high: 28 },
|
||||||
|
deb: { prediction: 28 },
|
||||||
|
} as CityDetail,
|
||||||
|
"zh-CN",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(chartData, "temperature chart data should exist for ISO datetime hourly input");
|
||||||
|
assert(
|
||||||
|
chartData?.datasets.debSeries.some((point) => point.labelTime === "08:00" && point.y === 20),
|
||||||
|
"temperature chart should normalize ISO hourly times into HH:mm points",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
chartData?.datasets.debSeries.some((point) => point.labelTime === "10:00" && point.y === 24),
|
||||||
|
"temperature chart should keep normalized hourly temperatures on the curve",
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -372,7 +372,7 @@ export function getTemperatureChartData(
|
|||||||
const rawTemps = Array.isArray(hourly.temps) ? hourly.temps : [];
|
const rawTemps = Array.isArray(hourly.temps) ? hourly.temps : [];
|
||||||
const validEntries = rawTimes
|
const validEntries = rawTimes
|
||||||
.map((time, index) => ({
|
.map((time, index) => ({
|
||||||
tail: String(time || "").trim(),
|
tail: normalizeHm(String(time || "").trim()) || "",
|
||||||
value: Number(rawTemps[index]),
|
value: Number(rawTemps[index]),
|
||||||
}))
|
}))
|
||||||
.filter((entry) => entry.tail !== "");
|
.filter((entry) => entry.tail !== "");
|
||||||
@@ -598,7 +598,7 @@ export function getTemperatureChartData(
|
|||||||
const tafMarkersRaw = Array.isArray(detail.taf?.signal?.markers)
|
const tafMarkersRaw = Array.isArray(detail.taf?.signal?.markers)
|
||||||
? detail.taf?.signal?.markers || []
|
? detail.taf?.signal?.markers || []
|
||||||
: [];
|
: [];
|
||||||
const normalizeHm = (value: unknown): string | null => {
|
const normalizeTafHm = (value: unknown): string | null => {
|
||||||
const match = String(value || "").match(/(\d{1,2}):(\d{2})/);
|
const match = String(value || "").match(/(\d{1,2}):(\d{2})/);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
const hour = Number.parseInt(match[1], 10);
|
const hour = Number.parseInt(match[1], 10);
|
||||||
@@ -632,7 +632,7 @@ export function getTemperatureChartData(
|
|||||||
}
|
}
|
||||||
return hour * 60 + minute;
|
return hour * 60 + minute;
|
||||||
};
|
};
|
||||||
const currentMinutes = chartHmToMinutes(normalizeHm(detail.local_time));
|
const currentMinutes = chartHmToMinutes(normalizeTafHm(detail.local_time));
|
||||||
const peakFirstHour = Number(detail.peak?.first_h);
|
const peakFirstHour = Number(detail.peak?.first_h);
|
||||||
const peakLastHour = Number(detail.peak?.last_h);
|
const peakLastHour = Number(detail.peak?.last_h);
|
||||||
const peakWindowStartMinutes =
|
const peakWindowStartMinutes =
|
||||||
@@ -692,23 +692,23 @@ export function getTemperatureChartData(
|
|||||||
const currentTafMarker =
|
const currentTafMarker =
|
||||||
currentMinutes !== null
|
currentMinutes !== null
|
||||||
? tafMarkers.find((marker) => {
|
? tafMarkers.find((marker) => {
|
||||||
const start = chartHmToMinutes(normalizeHm(marker.startLocal));
|
const start = chartHmToMinutes(normalizeTafHm(marker.startLocal));
|
||||||
const end = chartHmToMinutes(normalizeHm(marker.endLocal));
|
const end = chartHmToMinutes(normalizeTafHm(marker.endLocal));
|
||||||
return start !== null && end !== null && currentMinutes >= start && currentMinutes <= end;
|
return start !== null && end !== null && currentMinutes >= start && currentMinutes <= end;
|
||||||
}) || null
|
}) || null
|
||||||
: null;
|
: null;
|
||||||
const nextTafMarker =
|
const nextTafMarker =
|
||||||
currentMinutes !== null && !currentTafMarker
|
currentMinutes !== null && !currentTafMarker
|
||||||
? tafMarkers.find((marker) => {
|
? tafMarkers.find((marker) => {
|
||||||
const start = chartHmToMinutes(normalizeHm(marker.startLocal));
|
const start = chartHmToMinutes(normalizeTafHm(marker.startLocal));
|
||||||
return start !== null && start > currentMinutes;
|
return start !== null && start > currentMinutes;
|
||||||
}) || null
|
}) || null
|
||||||
: null;
|
: null;
|
||||||
const peakWindowTafMarker =
|
const peakWindowTafMarker =
|
||||||
peakWindowStartMinutes !== null && peakWindowEndMinutes !== null
|
peakWindowStartMinutes !== null && peakWindowEndMinutes !== null
|
||||||
? tafMarkers.find((marker) => {
|
? tafMarkers.find((marker) => {
|
||||||
const start = chartHmToMinutes(normalizeHm(marker.startLocal));
|
const start = chartHmToMinutes(normalizeTafHm(marker.startLocal));
|
||||||
const end = chartHmToMinutes(normalizeHm(marker.endLocal));
|
const end = chartHmToMinutes(normalizeTafHm(marker.endLocal));
|
||||||
return (
|
return (
|
||||||
start !== null &&
|
start !== null &&
|
||||||
end !== null &&
|
end !== null &&
|
||||||
|
|||||||
Reference in New Issue
Block a user