3.2.3.1
Fix average to top temps
This commit is contained in:
+3
-3
@@ -1649,8 +1649,8 @@ export function UTCIForecast() {
|
|||||||
const hasFeltAnchor = glanceSummary.some(i => i.label === 'Peak felt temp');
|
const hasFeltAnchor = glanceSummary.some(i => i.label === 'Peak felt temp');
|
||||||
const comfortItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Comfortable window') : null;
|
const comfortItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Comfortable window') : null;
|
||||||
const drivingItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Good driving time') : null;
|
const drivingItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Good driving time') : null;
|
||||||
const climateItem = hasFeltAnchor ? glanceSummary.find(i => i.label === '1991-2020 Average') : null;
|
const climateRows = hasFeltAnchor ? glanceSummary.filter(i => i.label === '1991-2020 High' || i.label === '1991-2020 Average') : [];
|
||||||
const restSummary = glanceSummary.filter(i => !(hasFeltAnchor && (i.label === 'Comfortable window' || i.label === 'Good driving time' || i.label === '1991-2020 Average')));
|
const restSummary = glanceSummary.filter(i => !(hasFeltAnchor && (i.label === 'Comfortable window' || i.label === 'Good driving time' || i.label === '1991-2020 High' || i.label === '1991-2020 Average')));
|
||||||
return [
|
return [
|
||||||
...restSummary.flatMap(item => [
|
...restSummary.flatMap(item => [
|
||||||
renderRow(item),
|
renderRow(item),
|
||||||
@@ -1659,7 +1659,7 @@ export function UTCIForecast() {
|
|||||||
? [
|
? [
|
||||||
...heatEvents.map(e => renderRow(e, 'ev-')),
|
...heatEvents.map(e => renderRow(e, 'ev-')),
|
||||||
...(drivingItem ? [renderRow(drivingItem)] : []),
|
...(drivingItem ? [renderRow(drivingItem)] : []),
|
||||||
...(climateItem ? [renderRow(climateItem)] : []),
|
...climateRows.map(item => renderRow(item)),
|
||||||
...(comfortItem ? [renderRow(comfortItem)] : []),
|
...(comfortItem ? [renderRow(comfortItem)] : []),
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|||||||
+23
-15
@@ -491,31 +491,39 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
|||||||
alert: maxRainProb >= 60,
|
alert: maxRainProb >= 60,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Vs seasonal average (climate anomaly) ──────────────────────────────
|
// ── Vs seasonal norms (climate anomalies) ──────────────────────────────
|
||||||
// Shown across every profile: how far this day's mean air temp sits above or
|
// Shown across every profile: two rows comparing this day against the
|
||||||
// below the 1991-2020 normal for the date. A small, honest climate-change cue.
|
// 1991-2020 norm for the date — the day's HIGH vs the normal high, and the
|
||||||
|
// day's 24h AVERAGE vs the normal mean. High tracks the daytime peak people
|
||||||
|
// notice; the average is the standard climate anomaly. An honest climate cue.
|
||||||
const climateItem = (() => {
|
const climateItem = (() => {
|
||||||
if (!normals) return [];
|
if (!normals) return [];
|
||||||
const key = dayKey || todayRows[0]?.iso?.slice(0, 10);
|
const key = dayKey || todayRows[0]?.iso?.slice(0, 10);
|
||||||
if (!key) return [];
|
if (!key) return [];
|
||||||
const LEAP_CUM = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335];
|
const LEAP_CUM = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335];
|
||||||
const doy = LEAP_CUM[parseInt(key.slice(5, 7), 10) - 1] + (parseInt(key.slice(8, 10), 10) - 1);
|
const doy = LEAP_CUM[parseInt(key.slice(5, 7), 10) - 1] + (parseInt(key.slice(8, 10), 10) - 1);
|
||||||
const normal = normals[doy];
|
|
||||||
if (normal == null) return [];
|
|
||||||
const taVals = todayRows.map(r => r.Ta).filter(v => v != null);
|
const taVals = todayRows.map(r => r.Ta).filter(v => v != null);
|
||||||
if (taVals.length === 0) return [];
|
if (taVals.length === 0) return [];
|
||||||
|
const dayHigh = Math.max(...taVals);
|
||||||
const dayMean = taVals.reduce((a, b) => a + b, 0) / taVals.length;
|
const dayMean = taVals.reduce((a, b) => a + b, 0) / taVals.length;
|
||||||
const anomaly = dayMean - normal;
|
const fmt = (anom) => {
|
||||||
const mag = Math.abs(anomaly);
|
const mag = Math.abs(anom);
|
||||||
const value = mag < 0.5
|
return mag < 0.5
|
||||||
? 'About average'
|
? 'About average'
|
||||||
: `${anomaly >= 0 ? '+' : '−'}${mag.toFixed(1)}° ${anomaly >= 0 ? 'warmer' : 'cooler'}`;
|
: `${anom >= 0 ? '+' : '−'}${mag.toFixed(1)}° ${anom >= 0 ? 'warmer' : 'cooler'}`;
|
||||||
return [{
|
};
|
||||||
icon: '🌡',
|
const out = [];
|
||||||
label: '1991-2020 Average',
|
const nHigh = normals.high?.[doy];
|
||||||
value,
|
if (nHigh != null) {
|
||||||
alert: anomaly >= 5,
|
const a = dayHigh - nHigh;
|
||||||
}];
|
out.push({ icon: '🌡', label: '1991-2020 High', value: fmt(a), alert: a >= 5 });
|
||||||
|
}
|
||||||
|
const nMean = normals.mean?.[doy];
|
||||||
|
if (nMean != null) {
|
||||||
|
const a = dayMean - nMean;
|
||||||
|
out.push({ icon: '🌡', label: '1991-2020 Average', value: fmt(a), alert: a >= 5 });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ── Good Driving Time ──────────────────────────────────────────────────
|
// ── Good Driving Time ──────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -87,22 +87,24 @@ function soilCacheKey(loc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── CLIMATE NORMALS (ERA5 archive) ───────────────────────────────────────
|
// ─── CLIMATE NORMALS (ERA5 archive) ───────────────────────────────────────
|
||||||
// Open-Meteo's Historical Archive gives daily mean temps back to 1940. We pull
|
// Open-Meteo's Historical Archive gives daily temps back to 1940. We pull the
|
||||||
// the WMO reference period (1991-2020) once per location and reduce it into a
|
// WMO reference period (1991-2020) once per location and reduce it into two
|
||||||
// 366-entry day-of-year table of "normal" daily-mean temperature, so the app
|
// 366-entry day-of-year tables of "normal" daily HIGH and daily MEAN temperature,
|
||||||
// can show how far the forecast for a day sits above/below its seasonal norm.
|
// so the app can show how far a day's forecast high AND daily average each sit
|
||||||
// Normals don't change, so this caches for a long time.
|
// above/below their seasonal norm. Normals don't change, so this caches long.
|
||||||
|
|
||||||
function buildClimateUrl(loc) {
|
function buildClimateUrl(loc) {
|
||||||
return `https://archive-api.open-meteo.com/v1/archive` +
|
return `https://archive-api.open-meteo.com/v1/archive` +
|
||||||
`?latitude=${loc.lat}&longitude=${loc.lon}` +
|
`?latitude=${loc.lat}&longitude=${loc.lon}` +
|
||||||
`&start_date=1991-01-01&end_date=2020-12-31` +
|
`&start_date=1991-01-01&end_date=2020-12-31` +
|
||||||
`&daily=temperature_2m_mean&timezone=auto`;
|
`&daily=temperature_2m_max,temperature_2m_mean&timezone=auto`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coarser key (~2 dp ≈ 1 km) so nearby lookups share one cached climatology.
|
// Coarser key (~2 dp ≈ 1 km) so nearby lookups share one cached climatology.
|
||||||
|
// The `_hm` marks the { high, mean } shape so it won't collide with any
|
||||||
|
// earlier single-array cache from a previous build.
|
||||||
function climateCacheKey(loc) {
|
function climateCacheKey(loc) {
|
||||||
return `sunscope_normals_${loc.lat.toFixed(2)}_${loc.lon.toFixed(2)}`;
|
return `sunscope_normals_hm_${loc.lat.toFixed(2)}_${loc.lon.toFixed(2)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cumulative days before each month on a leap-year calendar, so every date
|
// Cumulative days before each month on a leap-year calendar, so every date
|
||||||
@@ -114,15 +116,13 @@ function doyFromKey(key) {
|
|||||||
return LEAP_CUM[m - 1] + (d - 1); // 0..365
|
return LEAP_CUM[m - 1] + (d - 1); // 0..365
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reduce raw daily means into a smoothed 366-entry day-of-year normals table.
|
// Reduce one raw daily series into a smoothed 366-entry day-of-year table.
|
||||||
function reduceNormals(daily) {
|
function reduceSeries(times, values) {
|
||||||
const times = daily?.time;
|
if (!times || !values) return null;
|
||||||
const means = daily?.temperature_2m_mean;
|
|
||||||
if (!times || !means) return null;
|
|
||||||
const sums = new Array(366).fill(0);
|
const sums = new Array(366).fill(0);
|
||||||
const counts = new Array(366).fill(0);
|
const counts = new Array(366).fill(0);
|
||||||
for (let i = 0; i < times.length; i++) {
|
for (let i = 0; i < times.length; i++) {
|
||||||
const v = means[i];
|
const v = values[i];
|
||||||
if (v == null) continue;
|
if (v == null) continue;
|
||||||
const doy = doyFromKey(times[i]);
|
const doy = doyFromKey(times[i]);
|
||||||
sums[doy] += v;
|
sums[doy] += v;
|
||||||
@@ -143,8 +143,18 @@ function reduceNormals(daily) {
|
|||||||
return smooth;
|
return smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches (or reads cached) climate normals for a location. Returns a
|
// Build both the normal-high and normal-mean day-of-year tables from the
|
||||||
// 366-entry array of daily-mean °C, or null on any failure (feature hides).
|
// archive response. Returns { high, mean } or null.
|
||||||
|
function reduceNormals(daily) {
|
||||||
|
const times = daily?.time;
|
||||||
|
const high = reduceSeries(times, daily?.temperature_2m_max);
|
||||||
|
const mean = reduceSeries(times, daily?.temperature_2m_mean);
|
||||||
|
if (!high && !mean) return null;
|
||||||
|
return { high, mean };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches (or reads cached) climate normals for a location. Returns
|
||||||
|
// { high, mean } day-of-year tables (°C), or null on any failure (feature hides).
|
||||||
async function fetchClimateNormals(loc) {
|
async function fetchClimateNormals(loc) {
|
||||||
const key = climateCacheKey(loc);
|
const key = climateCacheKey(loc);
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user