New table temperature colour gradients
This commit is contained in:
Fraxle
2026-05-24 19:19:15 +01:00
parent 69788f1069
commit 5574690887
5 changed files with 91 additions and 88 deletions
+34 -45
View File
@@ -23,7 +23,7 @@
import { h, render, Fragment } from '../vendor/preact.js';
import htm from '../vendor/htm.js';
import {
utciCategory, UTCI_BANDS,
utciCategory, UTCI_BANDS, bandGradient,
SKIN_TYPES, sunburnMinutes, burnLabel,
VEHICLE_TYPES, BUILDING_TYPES,
confidenceBand, moonGlyph,
@@ -128,7 +128,7 @@ export function UTCIForecast() {
const span = GROUP_ORDER[g].filter(isColVisible).length;
if (span === 0) continue;
const labels = { felt: 'Felt', surface: 'Surface', ambient: 'Ambient', precip: 'Precip', sky: 'Sky', wind: 'Wind', airqual: 'Air quality', solar: 'Solar' };
cells.push(html`<th class=${`grp-label grp-label-${g}`} colspan=${span} scope="colgroup">${labels[g]}</th>`);
cells.push(html`<th class=${`grp-label grp-label-${g} grp-${g}`} colspan=${span} scope="colgroup">${labels[g]}</th>`);
}
return html`<tr class="grp-label-row">${cells}</tr>`;
};
@@ -468,7 +468,7 @@ ${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html`<button c
</label>
<label class="display-opt">
<input type="checkbox" checked=${showDegree} onChange=${toggleShowDegree} />
°&nbsp;symbol
° symbol
</label>
</div>
@@ -538,41 +538,28 @@ ${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html`<button c
const fmt = (v, dp = 1) => v == null ? '—' : (showDecimals ? v.toFixed(dp) : String(Math.round(v)));
const deg = showDegree ? '°' : '';
// Returns a background tint based on temperature value - all share the same
// Thermal Stress Bands colors with 50% alpha for consistency. Uses exact legend ranges.
// Converts a hex colour - r,g,b components for rgba().
const hexToRgb = (hex) => {
const n = parseInt(hex.replace('#',''), 16);
return [(n>>16)&255, (n>>8)&255, n&255];
};
// Air + UTCI columns - 35% alpha light tint.
// Thermal Stress Bands - solid colours matching the legend exactly.
// All band-coloured cells use the full bg/fg from UTCI_BANDS.
const tempBg = (t) => {
if (t == null) return 'transparent';
const band = utciCategory(t);
if (!band) return 'transparent';
const [r,g,b] = hexToRgb(band.bg);
return `rgba(${r},${g},${b},0.35)`;
return band ? bandGradient(band.bg, band.darkenAmt) : 'transparent';
};
// All other coloured columns - 50% alpha.
const tempBgStrong = (t) => {
if (t == null) return 'transparent';
const tempBgStrong = tempBg;
const tempFontColor = (t) => {
if (t == null) return '#1a1200';
const band = utciCategory(t);
if (!band) return 'transparent';
const [r,g,b] = hexToRgb(band.bg);
return `rgba(${r},${g},${b},0.5)`;
return band ? band.fg : '#1a1200';
};
// Returns perceived luminance 0..1 from a hex colour.
const luminance = (hex) => {
const [r,g,b] = hexToRgb(hex);
return (0.299*r + 0.587*g + 0.114*b) / 255;
};
// Light tint (35% alpha) - always dark text, background is never dark enough.
const tempFontColor = (t) => '#4a3218';
// Medium tint (50% alpha) - use light text on dark bands, dark text on light bands.
const tempFontColorStrong = (t) => {
if (t == null) return '#4a3218';
const tempFontColorStrong = tempFontColor;
// Returns any extra band styles (fontWeight, textShadow) for a given temp.
const bandExtras = (t) => {
const band = utciCategory(t);
if (!band) return '#4a3218';
return luminance(band.bg) < 0.35 ? '#ffffff' : '#4a3218';
if (!band) return {};
return {
...(band.fontWeight ? { fontWeight: band.fontWeight } : {}),
...(band.textShadow ? { textShadow: band.textShadow } : {}),
};
};
const scaleBg = (v, min, max, rgb, maxAlpha = 0.14) => {
if (v == null || isNaN(v)) return 'transparent';
@@ -619,19 +606,19 @@ ${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html`<button c
</span>
</td>
${visibleCols.utciP && html`
<td class=${groupStart('utciP')} style=${{ background: hexToRgba(adjCat.bg, 0.50), color: tempFontColorStrong(r.utciAdj), fontWeight: 700, fontSize: '13px' }}>
<td class=${groupStart('utciP')} style=${{ background: bandGradient(adjCat.bg, adjCat.darkenAmt), color: adjCat.fg, fontSize: '13px', ...bandExtras(r.utciAdj) }}>
${fmt(r.utciAdj)}${deg}
</td>`}
${visibleCols.vehicleT && html`
<td class=${groupStart('vehicleT')} style=${{ color: tempFontColorStrong(r.vehicleT), fontWeight: 'bold', background: tempBgStrong(r.vehicleT) }}>
<td class=${groupStart('vehicleT')} style=${{ color: tempFontColorStrong(r.vehicleT), background: tempBgStrong(r.vehicleT), ...bandExtras(r.vehicleT) }}>
${fmt(r.vehicleT)}${deg}
</td>`}
${indoorMode === 'on' && !indoorManaged && html`
<td class=${groupStart('indoorT')} style=${{ color: tempFontColorStrong(r.indoorT), fontWeight: 'bold', background: tempBgStrong(r.indoorT) }}>
<td class=${groupStart('indoorT')} style=${{ color: tempFontColorStrong(r.indoorT), background: tempBgStrong(r.indoorT), ...bandExtras(r.indoorT) }}>
${fmt(r.indoorT)}${deg}
</td>`}
${indoorMode === 'on' && indoorManaged && html`
<td class=${groupStart('managedT')} style=${{ color: tempFontColorStrong(r.managedT), fontWeight: 'bold', background: tempBgStrong(r.managedT) }}>
<td class=${groupStart('managedT')} style=${{ color: tempFontColorStrong(r.managedT), background: tempBgStrong(r.managedT), ...bandExtras(r.managedT) }}>
${fmt(r.managedT)}${deg}
</td>`}
${visibleCols.burn && html`
@@ -639,31 +626,30 @@ ${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html`<button c
${burnLabel(burnMins)}
</td>`}
${visibleCols.utci && html`
<td class=${groupStart('utci')} style=${{ background: hexToRgba(cat.bg, 0.35), color: tempFontColorStrong(r.utci), fontWeight: 600 }}>
<td class=${groupStart('utci')} style=${{ background: bandGradient(cat.bg, cat.darkenAmt), color: cat.fg, ...bandExtras(r.utci) }}>
${fmt(r.utci)}${deg}
</td>`}
${visibleCols.delta && html`
<td class=${groupStart('delta')} style=${{
color: delta > 3 ? '#c8601a' : delta < -3 ? '#3f73c4' : '#4a3218',
fontWeight: 600,
background: deltaBg(delta),
}}>
${delta > 0 ? '+' : ''}${fmt(delta)}
</td>`}
${visibleCols.tmrt && html`<td class=${groupStart('tmrt')} style=${{ background: tempBgStrong(r.Tmrt), color: tempFontColorStrong(r.Tmrt) }}>${fmt(r.Tmrt)}${deg}</td>`}
${visibleCols.tmrt && html`<td class=${groupStart('tmrt')} style=${{ background: tempBgStrong(r.Tmrt), color: tempFontColorStrong(r.Tmrt), ...bandExtras(r.Tmrt) }}>${fmt(r.Tmrt)}${deg}</td>`}
${visibleCols.concreteT && html`
<td class=${groupStart('concreteT')} style=${{ color: tempFontColorStrong(r.concreteT), fontWeight: 'bold', background: tempBgStrong(r.concreteT) }}>
<td class=${groupStart('concreteT')} style=${{ color: tempFontColorStrong(r.concreteT), background: tempBgStrong(r.concreteT), ...bandExtras(r.concreteT) }}>
${fmt(r.concreteT)}${deg}
</td>`}
${visibleCols.soilT && html`
<td class=${groupStart('soilT')} style=${{ color: tempFontColorStrong(r.soilT0), background: tempBgStrong(r.soilT0) }}>${fmt(r.soilT0)}${deg}</td>`}
<td class=${groupStart('soilT')} style=${{ color: tempFontColorStrong(r.soilT0), background: tempBgStrong(r.soilT0), ...bandExtras(r.soilT0) }}>${fmt(r.soilT0)}${deg}</td>`}
${visibleCols.soilT6 && html`
<td class=${groupStart('soilT6')} style=${{ color: tempFontColorStrong(r.soilT6), background: tempBgStrong(r.soilT6) }}>${fmt(r.soilT6)}${deg}</td>`}
<td class=${groupStart('soilT6')} style=${{ color: tempFontColorStrong(r.soilT6), background: tempBgStrong(r.soilT6), ...bandExtras(r.soilT6) }}>${fmt(r.soilT6)}${deg}</td>`}
${visibleCols.soilM && html`
<td class=${groupStart('soilM')} style=${{ color: '#2a6a90', background: scaleBg(r.soilM, 0.12, 0.45, '50,125,190') }}>${r.soilM != null ? fmt(r.soilM * 100, 1) + '%' : '—'}</td>`}
${visibleCols.air && html`<td class=${groupStart('air')} style=${{ background: tempBg(r.Ta), color: tempFontColorStrong(r.Ta) }}>${fmt(r.Ta)}${deg}</td>`}
${visibleCols.air && html`<td class=${groupStart('air')} style=${{ background: tempBg(r.Ta), color: tempFontColorStrong(r.Ta), ...bandExtras(r.Ta) }}>${fmt(r.Ta)}${deg}</td>`}
${visibleCols.rh && html`<td class=${groupStart('rh')} style=${{ background: scaleBg(r.RH, 30, 100, '70,145,200') }}>${Math.round(r.RH)}</td>`}
${visibleCols.dew && html`<td class=${groupStart('dew')} style=${{ background: tempBgStrong(r.dew), color: tempFontColorStrong(r.dew) }}>${fmt(r.dew)}${deg}</td>`}
${visibleCols.dew && html`<td class=${groupStart('dew')} style=${{ background: tempBgStrong(r.dew), color: tempFontColorStrong(r.dew), ...bandExtras(r.dew) }}>${fmt(r.dew)}${deg}</td>`}
${visibleCols.precip && html`
<td class=${groupStart('precip')} style=${{ background: r.snow > 0 ? scaleBg(r.snow, 0, 4, '90,140,210') : scaleBg(r.precip, 0, 8, '70,145,200') }}>
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '5px', verticalAlign: 'middle' }}>
@@ -824,11 +810,14 @@ ${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html`<button c
</div>`;
})()}
<div class="utci-legend-row">
<div class="utci-legend">
<span class="utci-legend-label">Thermal stress bands</span>
<div class="utci-legend-row">
${UTCI_BANDS.map((b, i) => html`
<span key=${i} class="utci-legend-item" style=${{ background: b.bg, color: b.fg }}>
<span key=${i} class="utci-legend-item" style=${{ background: bandGradient(b.bg), color: b.fg, ...(b.fontWeight ? { fontWeight: b.fontWeight } : {}), ...(b.textShadow ? { textShadow: b.textShadow } : {}) }}>
${b.label}
</span>`)}
</div>
</div>