Fix historic highs
This commit is contained in:
fraxle
2026-08-09 12:36:35 +01:00
parent bf882f441e
commit 9ce32f59e5
6 changed files with 64 additions and 25 deletions
+16
View File
@@ -1787,6 +1787,17 @@
white-space: pre-line; /* honour \n so multi-range windows stack one per line */
}
/* Secondary figure inside a value — currently the day's peak alongside the
climate anomaly. Inline where there is width; on its own line in the 240px
desktop rail, so the two numbers break deliberately rather than wherever
the text happens to run out of room. Nested inside .insight-value rather
than given its own grid area so rows without a sub keep their layout. */
.insight-sub {
font-weight: 500;
opacity: 0.72;
}
.insight-sub::before { content: ' '; }
/* Alert state - draws attention without being aggressive */
.insight-row--alert .insight-value {
color: #b84020;
@@ -1853,6 +1864,11 @@
grid-area: value;
text-align: left; /* value sits left-aligned under its label */
}
/* Rail is only 240px — the secondary figure takes its own line. */
.insight-panel--glance .insight-sub {
display: block;
}
.insight-panel--glance .insight-sub::before { content: none; }
}
/* ── Narrow screens - drop the rail below the table ─────────────────── */
+4 -4
View File
@@ -1579,19 +1579,19 @@ export function UTCIForecast() {
${(() => {
const heatEvents = eventGlanceItems.filter(e => /heat caution|heat warning|extreme heat/i.test(e.label)).map(e => ({ ...e, alert: true }));
const otherEvents = eventGlanceItems.filter(e => !/heat caution|heat warning|extreme heat/i.test(e.label));
const renderRow = ({ icon, label, value, alert, grp }, keyPrefix = '') => html`
const renderRow = ({ icon, label, value, sub, alert, grp }, keyPrefix = '') => html`
<div class=${`insight-row${alert ? ' insight-row--alert' : ''}${grp ? ` insight-row--${grp}` : ''}`} key=${`${keyPrefix}${label}`}>
<span class="insight-icon">${icon}</span>
<span class="insight-label">${titleCaseText(label)}</span>
<span class="insight-value">${titleCaseText(value)}</span>
<span class="insight-value">${titleCaseText(value)}${sub ? html`<span class="insight-sub">${titleCaseText(sub)}</span>` : ''}</span>
</div>`;
// Only the outdoors profile reorders items around the heat block
// (it has a 'Peak felt temp' anchor). Other profiles render in order.
const hasFeltAnchor = glanceSummary.some(i => i.label === 'Peak felt temp');
const comfortItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Comfortable window') : null;
const drivingItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Good driving time') : null;
const climateRows = glanceSummary.filter(i => i.label === '1991-2020 High' || i.label === '1991-2020 Average');
const restSummary = glanceSummary.filter(i => i.label !== '1991-2020 High' && i.label !== '1991-2020 Average' && !(hasFeltAnchor && (i.label === 'Comfortable window' || i.label === 'Good driving time')));
const climateRows = glanceSummary.filter(i => i.label === '1991-2020 Average');
const restSummary = glanceSummary.filter(i => i.label !== '1991-2020 Average' && !(hasFeltAnchor && (i.label === 'Comfortable window' || i.label === 'Good driving time')));
return [
...restSummary.flatMap(item => [
renderRow(item),
+33 -17
View File
@@ -548,10 +548,12 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
};
// ── Vs seasonal norms (climate anomalies) ──────────────────────────────
// Shown across every profile: two rows comparing this day against the
// 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.
// One row comparing this day against the 1991-2020 norm for the date. The
// headline is the standard climate anomaly — the day's 24h AVERAGE vs the
// normal mean — with the day's HIGH vs the normal high carried alongside as
// the peak people actually notice. Kept in a single row because two rows
// sharing the icon and "1991-2020 … warmer" phrasing read as duplicates
// giving contradictory answers. An honest climate cue.
const climateItem = (() => {
if (!normals) return [];
const key = dayKey || todayRows[0]?.iso?.slice(0, 10);
@@ -562,22 +564,36 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
if (taVals.length === 0) return [];
const dayHigh = Math.max(...taVals);
const dayMean = taVals.reduce((a, b) => a + b, 0) / taVals.length;
const out = [];
// Either day-of-year table can have null slots — reduceNormals keeps the
// { high, mean } shape as long as one series survived — so treat each as
// independently optional.
const nHigh = normals.high?.[doy];
if (nHigh != null) {
const a = dayHigh - nHigh;
if (Math.abs(a) >= 3) {
out.push({ icon: '🌡', label: '1991-2020 High', value: `${a >= 0 ? '+' : ''}${Math.abs(a).toFixed(1)}° ${a >= 0 ? 'warmer' : 'cooler'}`, alert: a >= 5, grp: 'ambient' });
}
}
const nMean = normals.mean?.[doy];
if (nMean != null) {
const a = dayMean - nMean;
if (Math.abs(a) >= 3) {
out.push({ icon: '🌡', label: '1991-2020 Average', value: `${a >= 0 ? '+' : ''}${Math.abs(a).toFixed(1)}° ${a >= 0 ? 'warmer' : 'cooler'}`, alert: a >= 5, grp: 'ambient' });
}
const aHigh = nHigh != null ? dayHigh - nHigh : null;
const aMean = nMean != null ? dayMean - nMean : null;
// Show the row when EITHER anomaly is notable. The mean anomaly is usually
// the smaller of the two (a clear night pulls it back towards normal), so
// gating on it alone would hide days with a dramatic peak.
const notable = v => v != null && Math.abs(v) >= 3;
if (!notable(aHigh) && !notable(aMean)) return [];
const fmt = a => `${a >= 0 ? '+' : ''}${Math.abs(a).toFixed(1)}°`;
const word = a => (a >= 0 ? 'warmer' : 'cooler');
// Each half keeps its own sign and word, so a day whose peak and mean fall
// on opposite sides of the norm still reads correctly.
// `sub` is the secondary figure — rendered inline on mobile, on its own
// line in the narrow desktop rail (see .insight-sub in ui.css).
let value, sub = null;
if (aMean != null && aHigh != null) {
value = `${fmt(aMean)} ${word(aMean)}`;
sub = `(${fmt(aHigh)} peak)`;
} else if (aMean != null) {
value = `${fmt(aMean)} ${word(aMean)}`;
} else {
value = `${fmt(aHigh)} peak ${word(aHigh)}`;
}
return out;
// Flagged off the high, matching the "5 degrees against the normal high" rule.
const alert = (aHigh != null ? aHigh : aMean) >= 5;
return [{ icon: '🌡', label: '1991-2020 Average', value, sub, alert, grp: 'ambient' }];
})();
// ── Good Driving Time ──────────────────────────────────────────────────