remove web dependencies

This commit is contained in:
2026-02-06 16:13:54 +11:00
parent 730811b76e
commit c68c063ff1
11 changed files with 265 additions and 42 deletions

View File

@@ -363,6 +363,10 @@ function sum(values) {
return seen ? total : null;
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function updateText(id, text) {
const el = document.getElementById(id);
if (el) el.textContent = text;
@@ -430,48 +434,123 @@ function lastNonNull(points, key) {
return null;
}
function computeRainProbability(latest, pressureTrend1h) {
if (!latest) {
function classifyRainProbability(prob) {
if (prob >= 0.6) return "High";
if (prob >= 0.35) return "Medium";
return "Low";
}
function computeDewPointC(tempC, rh) {
if (tempC === null || tempC === undefined || rh === null || rh === undefined) {
return null;
}
const safeRh = clamp(rh, 1, 100);
const a = 17.625;
const b = 243.04;
const gamma = Math.log(safeRh / 100) + (a * tempC) / (b + tempC);
return (b * gamma) / (a - gamma);
}
function computeRainProbabilityFromInputs(tempC, rh, pressureHpa) {
if (tempC === null || tempC === undefined || rh === null || rh === undefined || pressureHpa === null || pressureHpa === undefined) {
return null;
}
let prob = 0.1;
if (pressureTrend1h !== null && pressureTrend1h !== undefined) {
if (pressureTrend1h <= -3.0) {
prob += 0.5;
} else if (pressureTrend1h <= -2.0) {
prob += 0.35;
} else if (pressureTrend1h <= -1.0) {
prob += 0.2;
} else if (pressureTrend1h <= -0.5) {
prob += 0.1;
}
const dewPointC = computeDewPointC(tempC, rh);
if (dewPointC === null) {
return null;
}
if (latest.rh !== null && latest.rh !== undefined) {
if (latest.rh >= 95) {
prob += 0.2;
} else if (latest.rh >= 90) {
prob += 0.15;
} else if (latest.rh >= 85) {
prob += 0.1;
}
const saturationSpread = Math.max(0, tempC - dewPointC);
const humidityFactor = clamp((rh - 55) / 45, 0, 1);
const pressureFactor = clamp((1016 - pressureHpa) / 18, 0, 1);
const saturationFactor = clamp((6 - saturationSpread) / 6, 0, 1);
const score = 0.45 * humidityFactor + 0.35 * pressureFactor + 0.2 * saturationFactor;
const prob = clamp(0.02 + 0.93 * score, 0.02, 0.98);
return { prob, label: classifyRainProbability(prob) };
}
function computeRainProbability(latest) {
if (!latest) {
return null;
}
return computeRainProbabilityFromInputs(latest.temp_c, latest.rh, latest.pressure_hpa);
}
function buildRainProbabilitySeries(points) {
const out = [];
for (const p of points) {
const t = new Date(p.ts).getTime();
if (Number.isNaN(t)) continue;
const rp = computeRainProbabilityFromInputs(p.temp_c, p.rh, p.pressure_hpa);
out.push({
x: t,
y: rp ? Math.round(rp.prob * 1000) / 10 : null,
});
}
return out;
}
function updateWeatherIcons(latest, rainProb) {
const sunEl = document.getElementById("live-icon-sun");
const cloudEl = document.getElementById("live-icon-cloud");
const rainEl = document.getElementById("live-icon-rain");
const textEl = document.getElementById("live-weather-text");
[sunEl, cloudEl, rainEl].forEach((el) => {
if (el) el.classList.remove("active");
});
if (!latest) {
if (textEl) textEl.textContent = "--";
return;
}
if (latest.wind_m_s !== null && latest.wind_m_s !== undefined && latest.wind_m_s >= 6) {
prob += 0.05;
const prob = rainProb ? rainProb.prob : null;
const rh = latest.rh;
const pressure = latest.pressure_hpa;
const uvi = latest.uvi;
let sunActive = false;
let cloudActive = false;
let rainActive = false;
let label = "Partly cloudy";
if (prob !== null && prob >= 0.6) {
rainActive = true;
cloudActive = true;
label = "Rain likely";
} else if (
(prob !== null && prob >= 0.35) ||
(rh !== null && rh !== undefined && rh >= 80) ||
(pressure !== null && pressure !== undefined && pressure <= 1008)
) {
cloudActive = true;
label = "Cloudy";
} else if (
uvi !== null &&
uvi !== undefined &&
uvi >= 4 &&
rh !== null &&
rh !== undefined &&
rh < 75 &&
pressure !== null &&
pressure !== undefined &&
pressure >= 1012
) {
sunActive = true;
label = "Sunny";
} else {
sunActive = true;
cloudActive = true;
}
prob = Math.max(0.05, Math.min(0.95, prob));
let label = "Low";
if (prob >= 0.6) {
label = "High";
} else if (prob >= 0.35) {
label = "Medium";
}
return { prob, label };
if (sunEl) sunEl.classList.toggle("active", sunActive);
if (cloudEl) cloudEl.classList.toggle("active", cloudActive);
if (rainEl) rainEl.classList.toggle("active", rainActive);
if (textEl) textEl.textContent = label;
}
function extendForecastTo(points, endTime) {
@@ -607,12 +686,13 @@ function renderDashboard(data) {
const forecast = filterRange(forecastAll, rangeStart, rangeEnd);
const forecastLine = extendForecastTo(forecast, rangeEnd);
const lastPressureTrend = lastNonNull(obsFiltered, "pressure_trend_1h");
const rainProb = computeRainProbability(latest, lastPressureTrend);
const rainProb = computeRainProbability(latest);
if (rainProb) {
updateText("live-rain-prob", `${Math.round(rainProb.prob * 100)}% (${rainProb.label})`);
} else {
updateText("live-rain-prob", "--");
}
updateWeatherIcons(latest, rainProb);
updateText("baro-outlook", describeBarometer(latest ? latest.pressure_hpa : null, lastPressureTrend));
const obsTemps = obsFiltered.map((p) => p.temp_c);
@@ -757,9 +837,12 @@ function renderDashboard(data) {
upsertChart("chart-power", powerChart);
const rainOptions = baseOptions(range);
rainOptions.scales.y.ticks.color = colors.rain;
rainOptions.scales.y.title = { display: true, text: "Observed Rain (mm)", color: colors.rain };
rainOptions.scales.y1 = {
position: "right",
ticks: { color: "#a4c4c4" },
ticks: { color: colors.forecast },
title: { display: true, text: "Forecast Rain (mm)", color: colors.forecast },
grid: { drawOnChartArea: false },
};
@@ -788,7 +871,7 @@ function renderDashboard(data) {
label: "forecast precip (mm)",
data: series(forecastRain, "precip_mm"),
backgroundColor: colors.forecast,
yAxisID: "y",
yAxisID: "y1",
},
],
},
@@ -796,6 +879,31 @@ function renderDashboard(data) {
};
upsertChart("chart-rain", rainChart);
const rainProbOptions = baseOptions(range);
rainProbOptions.scales.y.min = 0;
rainProbOptions.scales.y.max = 100;
rainProbOptions.scales.y.ticks.color = colors.rain;
rainProbOptions.scales.y.ticks.callback = (value) => `${value}%`;
rainProbOptions.scales.y.title = { display: true, text: "Probability (%)", color: colors.rain };
const rainProbChart = {
type: "line",
data: {
datasets: [
{
label: "predicted rain probability (%)",
data: buildRainProbabilitySeries(obsFiltered),
borderColor: colors.rain,
backgroundColor: "rgba(78, 168, 222, 0.18)",
fill: true,
yAxisID: "y",
},
],
},
options: rainProbOptions,
};
upsertChart("chart-rain-prob", rainProbChart);
updateSingleChartMode();
}