1.9.1.3
Gradient columns
This commit is contained in:
+53
-45
@@ -552,7 +552,9 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC
|
||||
<div class="utci-tbody-scroll" ref=${bodyScrollRef} onScroll=${handleBodyScroll}>
|
||||
<table class="utci-table utci-table-body" ref=${bodyTableRef}>
|
||||
<tbody>
|
||||
${visible.map((r) => {
|
||||
${visible.map((r, rowIdx) => {
|
||||
const rPrev = visible[rowIdx - 1];
|
||||
const rNext = visible[rowIdx + 1];
|
||||
const cat = utciCategory(r.utci);
|
||||
const isNight = r.elev < 0;
|
||||
const isNow = r.iso.slice(0, 13) === nowLocalISO;
|
||||
@@ -570,45 +572,51 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC
|
||||
const fmt = (v, dp = 1) => v == null ? '—' : (showDecimals ? v.toFixed(dp) : String(Math.round(v)));
|
||||
const u = (unit) => showUnits ? unit : '';
|
||||
// Returns a background tint based on temperature value - all share the same
|
||||
// Continuous temperature colour scale — used for all temp columns
|
||||
// (air, dew, indoor, vehicle, concrete, soil, UTCI, SunSoak…).
|
||||
// Fully opaque, richly saturated stops so every degree is distinct.
|
||||
const airTempBg = (t) => {
|
||||
if (t == null) return 'transparent';
|
||||
// [temp °C, [r,g,b]]
|
||||
const stops = [
|
||||
[-10, [ 90, 155, 220]],
|
||||
[ 0, [140, 195, 235]],
|
||||
[ 10, [155, 215, 195]],
|
||||
[ 16, [140, 210, 140]],
|
||||
[ 20, [195, 225, 110]],
|
||||
[ 24, [240, 225, 80]],
|
||||
[ 28, [250, 175, 65]],
|
||||
[ 32, [240, 120, 55]],
|
||||
[ 36, [220, 70, 50]],
|
||||
[ 40, [185, 30, 30]],
|
||||
[ 50, [130, 0, 20]],
|
||||
];
|
||||
// Continuous temperature colour scale — used for all temp columns.
|
||||
// airTempRgb(t) returns the whiteblended [r,g,b] for a temperature.
|
||||
// airTempBg(t, tPrev, tNext) returns a top→bottom gradient that
|
||||
// flows from the previous row's colour through this row's colour to
|
||||
// the next row's colour, making the whole column one seamless heatmap.
|
||||
const TEMP_STOPS = [
|
||||
[-10, [ 90, 155, 220]],
|
||||
[ 0, [140, 195, 235]],
|
||||
[ 10, [155, 215, 195]],
|
||||
[ 16, [140, 210, 140]],
|
||||
[ 20, [195, 225, 110]],
|
||||
[ 24, [240, 225, 80]],
|
||||
[ 28, [250, 175, 65]],
|
||||
[ 32, [240, 120, 55]],
|
||||
[ 36, [220, 70, 50]],
|
||||
[ 40, [185, 30, 30]],
|
||||
[ 50, [130, 0, 20]],
|
||||
];
|
||||
const airTempRgb = (t) => {
|
||||
if (t == null) return null;
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
let i = stops.findIndex(s => t < s[0]);
|
||||
if (i === -1) i = stops.length;
|
||||
const baseRgb = i === 0 ? stops[0][1]
|
||||
: i >= stops.length ? stops[stops.length-1][1]
|
||||
let i = TEMP_STOPS.findIndex(s => t < s[0]);
|
||||
if (i === -1) i = TEMP_STOPS.length;
|
||||
const raw = i === 0 ? TEMP_STOPS[0][1]
|
||||
: i >= TEMP_STOPS.length ? TEMP_STOPS[TEMP_STOPS.length-1][1]
|
||||
: (() => {
|
||||
const [t0, c0] = stops[i-1];
|
||||
const [t1, c1] = stops[i];
|
||||
const [t0, c0] = TEMP_STOPS[i-1];
|
||||
const [t1, c1] = TEMP_STOPS[i];
|
||||
const x = clamp((t - t0) / (t1 - t0), 0, 1);
|
||||
const lerp = (a, b) => Math.round(a + (b - a) * x);
|
||||
return [lerp(c0[0],c1[0]), lerp(c0[1],c1[1]), lerp(c0[2],c1[2])];
|
||||
})();
|
||||
// Blend base colour 25% toward white before applying the sweep.
|
||||
const [r,g,b] = baseRgb.map(c => Math.min(255, Math.round(c + (255 - c) * 0.66)));
|
||||
const lighten = (c) => Math.min(255, Math.round(c + (255 - c) * 0.30));
|
||||
const darken = (c) => Math.round(c * 0.93);
|
||||
const light = `rgb(${lighten(r)},${lighten(g)},${lighten(b)})`;
|
||||
const mid = `rgb(${r},${g},${b})`;
|
||||
const dark = `rgb(${darken(r)},${darken(g)},${darken(b)})`;
|
||||
return `linear-gradient(135deg, ${light} 0%, ${mid} 60%, ${dark} 100%)`;
|
||||
// Blend 66% toward white for the pastel look.
|
||||
return raw.map(c => Math.min(255, Math.round(c + (255 - c) * 0.66)));
|
||||
};
|
||||
const toRgb = (rgb) => rgb ? `rgb(${rgb[0]},${rgb[1]},${rgb[2]})` : 'transparent';
|
||||
const airTempBg = (t, tPrev, tNext) => {
|
||||
if (t == null) return 'transparent';
|
||||
// Top colour = midpoint temp between prev row and this row.
|
||||
// Bottom colour = midpoint temp between this row and next row.
|
||||
// This guarantees the boundary colour is identical on both sides
|
||||
// of every cell edge, giving a perfectly seamless column gradient.
|
||||
const tTop = tPrev != null ? (tPrev + t) / 2 : t;
|
||||
const tBot = tNext != null ? (t + tNext) / 2 : t;
|
||||
return `linear-gradient(to bottom, ${toRgb(airTempRgb(tTop))} 0%, ${toRgb(airTempRgb(tBot))} 100%)`;
|
||||
};
|
||||
const airTempFontColor = () => '#1a1a1a';
|
||||
// Returns any extra band styles (fontWeight, textShadow) for a given temp.
|
||||
@@ -665,19 +673,19 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC
|
||||
</span>
|
||||
</td>
|
||||
${visibleCols.utciP && html`
|
||||
<td class=${groupStart('utciP')} style=${{ background: airTempBg(r.utciAdj), color: airTempFontColor(r.utciAdj), fontSize: '13px' }}>
|
||||
<td class=${groupStart('utciP')} style=${{ background: airTempBg(r.utciAdj, rPrev?.utciAdj, rNext?.utciAdj), color: airTempFontColor(), fontSize: '13px' }}>
|
||||
${fmt(r.utciAdj)}${u('°C')}
|
||||
</td>`}
|
||||
${visibleCols.vehicleT && html`
|
||||
<td class=${groupStart('vehicleT')} style=${{ color: airTempFontColor(r.vehicleT), background: airTempBg(r.vehicleT) }}>
|
||||
<td class=${groupStart('vehicleT')} style=${{ color: airTempFontColor(), background: airTempBg(r.vehicleT, rPrev?.vehicleT, rNext?.vehicleT) }}>
|
||||
${fmt(r.vehicleT)}${u('°C')}
|
||||
</td>`}
|
||||
${indoorMode === 'on' && !indoorManaged && html`
|
||||
<td class=${groupStart('indoorT')} style=${{ color: airTempFontColor(r.indoorT), background: airTempBg(r.indoorT) }}>
|
||||
<td class=${groupStart('indoorT')} style=${{ color: airTempFontColor(), background: airTempBg(r.indoorT, rPrev?.indoorT, rNext?.indoorT) }}>
|
||||
${fmt(r.indoorT)}${u('°C')}
|
||||
</td>`}
|
||||
${indoorMode === 'on' && indoorManaged && html`
|
||||
<td class=${groupStart('managedT')} style=${{ color: airTempFontColor(r.managedT), background: airTempBg(r.managedT) }}>
|
||||
<td class=${groupStart('managedT')} style=${{ color: airTempFontColor(), background: airTempBg(r.managedT, rPrev?.managedT, rNext?.managedT) }}>
|
||||
${fmt(r.managedT)}${u('°C')}
|
||||
</td>`}
|
||||
${visibleCols.burn && html`
|
||||
@@ -685,7 +693,7 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC
|
||||
${burnLabel(burnMins)}
|
||||
</td>`}
|
||||
${visibleCols.utci && html`
|
||||
<td class=${groupStart('utci')} style=${{ background: airTempBg(r.utci), color: airTempFontColor(r.utci) }}>
|
||||
<td class=${groupStart('utci')} style=${{ background: airTempBg(r.utci, rPrev?.utci, rNext?.utci), color: airTempFontColor() }}>
|
||||
${fmt(r.utci)}${u('°C')}
|
||||
</td>`}
|
||||
${visibleCols.delta && html`
|
||||
@@ -695,20 +703,20 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC
|
||||
}}>
|
||||
${delta > 0 ? '+' : ''}${fmt(delta)}${u('°C')}
|
||||
</td>`}
|
||||
${visibleCols.tmrt && html`<td class=${groupStart('tmrt')} style=${{ background: airTempBg(r.Tmrt), color: airTempFontColor(r.Tmrt) }}>${fmt(r.Tmrt)}${u('°C')}</td>`}
|
||||
${visibleCols.tmrt && html`<td class=${groupStart('tmrt')} style=${{ background: airTempBg(r.Tmrt, rPrev?.Tmrt, rNext?.Tmrt), color: airTempFontColor() }}>${fmt(r.Tmrt)}${u('°C')}</td>`}
|
||||
${visibleCols.concreteT && html`
|
||||
<td class=${groupStart('concreteT')} style=${{ color: airTempFontColor(r.concreteT), background: airTempBg(r.concreteT) }}>
|
||||
<td class=${groupStart('concreteT')} style=${{ color: airTempFontColor(), background: airTempBg(r.concreteT, rPrev?.concreteT, rNext?.concreteT) }}>
|
||||
${fmt(r.concreteT)}${u('°C')}
|
||||
</td>`}
|
||||
${visibleCols.soilT && html`
|
||||
<td class=${groupStart('soilT')} style=${{ color: airTempFontColor(r.soilT0), background: airTempBg(r.soilT0) }}>${fmt(r.soilT0)}${u('°C')}</td>`}
|
||||
<td class=${groupStart('soilT')} style=${{ color: airTempFontColor(), background: airTempBg(r.soilT0, rPrev?.soilT0, rNext?.soilT0) }}>${fmt(r.soilT0)}${u('°C')}</td>`}
|
||||
${visibleCols.soilT6 && html`
|
||||
<td class=${groupStart('soilT6')} style=${{ color: airTempFontColor(r.soilT6), background: airTempBg(r.soilT6) }}>${fmt(r.soilT6)}${u('°C')}</td>`}
|
||||
<td class=${groupStart('soilT6')} style=${{ color: airTempFontColor(), background: airTempBg(r.soilT6, rPrev?.soilT6, rNext?.soilT6) }}>${fmt(r.soilT6)}${u('°C')}</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) + u('%') : '—'}</td>`}
|
||||
${visibleCols.air && html`<td class=${groupStart('air')} style=${{ background: airTempBg(r.Ta), color: airTempFontColor(r.Ta) }}>${fmt(r.Ta)}${u('°C')}</td>`}
|
||||
${visibleCols.air && html`<td class=${groupStart('air')} style=${{ background: airTempBg(r.Ta, rPrev?.Ta, rNext?.Ta), color: airTempFontColor() }}>${fmt(r.Ta)}${u('°C')}</td>`}
|
||||
${visibleCols.rh && html`<td class=${groupStart('rh')} style=${{ background: scaleBg(r.RH, 30, 100, '70,145,200') }}>${Math.round(r.RH)}${u('%')}</td>`}
|
||||
${visibleCols.dew && html`<td class=${groupStart('dew')} style=${{ background: airTempBg(r.dew), color: airTempFontColor(r.dew) }}>${fmt(r.dew)}${u('°C')}</td>`}
|
||||
${visibleCols.dew && html`<td class=${groupStart('dew')} style=${{ background: airTempBg(r.dew, rPrev?.dew, rNext?.dew), color: airTempFontColor() }}>${fmt(r.dew)}${u('°C')}</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' }}>
|
||||
|
||||
Reference in New Issue
Block a user