feat: add initial app.js for client-side application logic.
This commit is contained in:
+102
-67
@@ -462,17 +462,27 @@ function renderChart(data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find current hour index
|
// Current hour index
|
||||||
const curHour = data.local_time
|
const curHour = data.local_time
|
||||||
? data.local_time.split(":")[0] + ":00"
|
? data.local_time.split(":")[0] + ":00"
|
||||||
: null;
|
: null;
|
||||||
const curIdx = curHour ? times.indexOf(curHour) : -1;
|
const curIdx = curHour ? times.indexOf(curHour) : -1;
|
||||||
|
|
||||||
// Open-Meteo model: past = solid, future = dashed
|
// === DEB-adjusted curve ===
|
||||||
const pastTemps = temps.map((t, i) =>
|
// Shift the OM hourly shape so its peak matches DEB prediction
|
||||||
|
const omMax = data.forecast?.today_high;
|
||||||
|
const debMax = data.deb?.prediction;
|
||||||
|
const offset = debMax != null && omMax != null ? debMax - omMax : 0;
|
||||||
|
|
||||||
|
const debTemps = temps.map((t) =>
|
||||||
|
t != null ? +(t + offset).toFixed(1) : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Split DEB curve: past = solid, future = dashed
|
||||||
|
const debPast = debTemps.map((t, i) =>
|
||||||
curIdx >= 0 && i <= curIdx ? t : null,
|
curIdx >= 0 && i <= curIdx ? t : null,
|
||||||
);
|
);
|
||||||
const futureTemps = temps.map((t, i) =>
|
const debFuture = debTemps.map((t, i) =>
|
||||||
curIdx < 0 || i >= curIdx ? t : null,
|
curIdx < 0 || i >= curIdx ? t : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -483,18 +493,14 @@ function renderChart(data) {
|
|||||||
: data.trend?.recent || [];
|
: data.trend?.recent || [];
|
||||||
if (metarSrc.length > 0) {
|
if (metarSrc.length > 0) {
|
||||||
metarSrc.forEach((r) => {
|
metarSrc.forEach((r) => {
|
||||||
// METAR time may be "14:20" but chart labels are whole hours "14:00"
|
|
||||||
const parts = r.time.split(":");
|
const parts = r.time.split(":");
|
||||||
let h = parseInt(parts[0], 10);
|
let h = parseInt(parts[0], 10);
|
||||||
const m = parseInt(parts[1] || "0", 10);
|
const m = parseInt(parts[1] || "0", 10);
|
||||||
if (m >= 30) h = (h + 1) % 24; // round to nearest hour
|
if (m >= 30) h = (h + 1) % 24;
|
||||||
const hourKey = h.toString().padStart(2, "0") + ":00";
|
const hourKey = h.toString().padStart(2, "0") + ":00";
|
||||||
const idx = times.indexOf(hourKey);
|
const idx = times.indexOf(hourKey);
|
||||||
if (idx >= 0) {
|
if (idx >= 0 && metarPoints[idx] === null) {
|
||||||
// If multiple METARs map to the same hour, keep the latest (first in array)
|
metarPoints[idx] = r.temp;
|
||||||
if (metarPoints[idx] === null) {
|
|
||||||
metarPoints[idx] = r.temp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -502,55 +508,73 @@ function renderChart(data) {
|
|||||||
const ctx = document.getElementById("tempChart").getContext("2d");
|
const ctx = document.getElementById("tempChart").getContext("2d");
|
||||||
if (tempChart) tempChart.destroy();
|
if (tempChart) tempChart.destroy();
|
||||||
|
|
||||||
const validTemps = temps.filter((t) => t != null);
|
const validDebTemps = debTemps.filter((t) => t != null);
|
||||||
const validMetar = metarPoints.filter((t) => t != null);
|
const validMetar = metarPoints.filter((t) => t != null);
|
||||||
const allVals = [...validTemps, ...validMetar];
|
const allVals = [...validDebTemps, ...validMetar];
|
||||||
|
if (allVals.length === 0) {
|
||||||
|
document.getElementById("chartLegend").textContent = "暂无数据";
|
||||||
|
return;
|
||||||
|
}
|
||||||
const minTemp = Math.floor(Math.min(...allVals)) - 1;
|
const minTemp = Math.floor(Math.min(...allVals)) - 1;
|
||||||
const maxTemp = Math.ceil(Math.max(...allVals)) + 1;
|
const maxTemp = Math.ceil(Math.max(...allVals)) + 1;
|
||||||
|
|
||||||
|
// Build datasets
|
||||||
|
const datasets = [
|
||||||
|
{
|
||||||
|
label: "DEB预期",
|
||||||
|
data: debPast,
|
||||||
|
borderColor: "rgba(52, 211, 153, 0.6)",
|
||||||
|
backgroundColor: "rgba(52, 211, 153, 0.05)",
|
||||||
|
borderWidth: 1.5,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 3,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.3,
|
||||||
|
spanGaps: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "DEB预报",
|
||||||
|
data: debFuture,
|
||||||
|
borderColor: "rgba(52, 211, 153, 0.35)",
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderDash: [5, 3],
|
||||||
|
pointRadius: 0,
|
||||||
|
fill: false,
|
||||||
|
tension: 0.3,
|
||||||
|
spanGaps: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "METAR实测",
|
||||||
|
data: metarPoints,
|
||||||
|
borderColor: "#22d3ee",
|
||||||
|
backgroundColor: "#22d3ee",
|
||||||
|
borderWidth: 0,
|
||||||
|
pointRadius: 5,
|
||||||
|
pointHoverRadius: 7,
|
||||||
|
pointStyle: "circle",
|
||||||
|
fill: false,
|
||||||
|
showLine: false,
|
||||||
|
order: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add subtle OM reference line if DEB offset is significant
|
||||||
|
if (Math.abs(offset) > 0.3) {
|
||||||
|
datasets.push({
|
||||||
|
label: "OM原始",
|
||||||
|
data: temps,
|
||||||
|
borderColor: "rgba(99, 102, 241, 0.2)",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderDash: [2, 4],
|
||||||
|
pointRadius: 0,
|
||||||
|
fill: false,
|
||||||
|
tension: 0.3,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
tempChart = new Chart(ctx, {
|
tempChart = new Chart(ctx, {
|
||||||
type: "line",
|
type: "line",
|
||||||
data: {
|
data: { labels: times, datasets },
|
||||||
labels: times,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "OM模型",
|
|
||||||
data: pastTemps,
|
|
||||||
borderColor: "rgba(99, 102, 241, 0.5)",
|
|
||||||
backgroundColor: "rgba(99, 102, 241, 0.05)",
|
|
||||||
borderWidth: 1.5,
|
|
||||||
pointRadius: 0,
|
|
||||||
pointHoverRadius: 3,
|
|
||||||
fill: true,
|
|
||||||
tension: 0.3,
|
|
||||||
spanGaps: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "OM预报",
|
|
||||||
data: futureTemps,
|
|
||||||
borderColor: "rgba(99, 102, 241, 0.4)",
|
|
||||||
borderWidth: 1.5,
|
|
||||||
borderDash: [5, 3],
|
|
||||||
pointRadius: 0,
|
|
||||||
fill: false,
|
|
||||||
tension: 0.3,
|
|
||||||
spanGaps: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "METAR实测",
|
|
||||||
data: metarPoints,
|
|
||||||
borderColor: "#22d3ee",
|
|
||||||
backgroundColor: "#22d3ee",
|
|
||||||
borderWidth: 0,
|
|
||||||
pointRadius: 5,
|
|
||||||
pointHoverRadius: 7,
|
|
||||||
pointStyle: "circle",
|
|
||||||
fill: false,
|
|
||||||
showLine: false,
|
|
||||||
order: 0, // Draw on top
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@@ -559,7 +583,7 @@ function renderChart(data) {
|
|||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
backgroundColor: "rgba(15, 23, 42, 0.9)",
|
backgroundColor: "rgba(15, 23, 42, 0.9)",
|
||||||
borderColor: "rgba(99, 102, 241, 0.3)",
|
borderColor: "rgba(52, 211, 153, 0.3)",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
titleFont: { family: "Inter", size: 12 },
|
titleFont: { family: "Inter", size: 12 },
|
||||||
bodyFont: { family: "Inter", size: 12 },
|
bodyFont: { family: "Inter", size: 12 },
|
||||||
@@ -594,36 +618,47 @@ function renderChart(data) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// DEB prediction line annotation
|
// DEB max line annotation (horizontal)
|
||||||
if (data.deb?.prediction != null) {
|
if (debMax != null) {
|
||||||
const debY = data.deb.prediction;
|
|
||||||
tempChart.options.plugins.annotation = {
|
tempChart.options.plugins.annotation = {
|
||||||
annotations: {
|
annotations: {
|
||||||
debLine: {
|
debLine: {
|
||||||
type: "line",
|
type: "line",
|
||||||
yMin: debY,
|
yMin: debMax,
|
||||||
yMax: debY,
|
yMax: debMax,
|
||||||
borderColor: "#34d399",
|
borderColor: "#34d399",
|
||||||
borderWidth: 1,
|
borderWidth: 1.5,
|
||||||
borderDash: [4, 4],
|
borderDash: [6, 3],
|
||||||
|
label: {
|
||||||
|
display: true,
|
||||||
|
content: `DEB ${debMax}${data.temp_symbol}`,
|
||||||
|
position: "end",
|
||||||
|
backgroundColor: "rgba(52, 211, 153, 0.15)",
|
||||||
|
color: "#34d399",
|
||||||
|
font: { size: 10, family: "Inter" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
tempChart.update();
|
tempChart.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
// METAR recent points overlay
|
// Chart legend text
|
||||||
const legend = document.getElementById("chartLegend");
|
const legend = document.getElementById("chartLegend");
|
||||||
|
const legendParts = [];
|
||||||
|
if (debMax != null && omMax != null && Math.abs(offset) > 0.3) {
|
||||||
|
const sign = offset > 0 ? "+" : "";
|
||||||
|
legendParts.push(`DEB偏移 ${sign}${offset.toFixed(1)}° vs OM`);
|
||||||
|
}
|
||||||
if (data.trend?.recent?.length) {
|
if (data.trend?.recent?.length) {
|
||||||
const recentStr = [...data.trend.recent]
|
const recentStr = [...data.trend.recent]
|
||||||
.slice(0, 4)
|
.slice(0, 4)
|
||||||
.reverse() // Fix chronologically left to right
|
.reverse()
|
||||||
.map((r) => `${r.temp}${data.temp_symbol}@${r.time}`)
|
.map((r) => `${r.temp}${data.temp_symbol}@${r.time}`)
|
||||||
.join(" → ");
|
.join(" → ");
|
||||||
legend.textContent = `METAR 趋势:${recentStr}`;
|
legendParts.push(`METAR: ${recentStr}`);
|
||||||
} else {
|
|
||||||
legend.textContent = "";
|
|
||||||
}
|
}
|
||||||
|
legend.textContent = legendParts.join(" ┃ ") || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProbabilities(data) {
|
function renderProbabilities(data) {
|
||||||
|
|||||||
Reference in New Issue
Block a user