diff --git a/assets/js/app.js b/assets/js/app.js index 1ad65b4..495ec27 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -552,7 +552,9 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC
- ${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 ${visibleCols.utciP && html` - `} ${visibleCols.vehicleT && html` - `} ${indoorMode === 'on' && !indoorManaged && html` - `} ${indoorMode === 'on' && indoorManaged && html` - `} ${visibleCols.burn && html` @@ -685,7 +693,7 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC ${burnLabel(burnMins)} `} ${visibleCols.utci && html` - `} ${visibleCols.delta && html` @@ -695,20 +703,20 @@ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeC }}> ${delta > 0 ? '+' : ''}${fmt(delta)}${u('°C')} `} - ${visibleCols.tmrt && html``} + ${visibleCols.tmrt && html``} ${visibleCols.concreteT && html` - `} ${visibleCols.soilT && html` - `} + `} ${visibleCols.soilT6 && html` - `} + `} ${visibleCols.soilM && html` `} - ${visibleCols.air && html``} + ${visibleCols.air && html``} ${visibleCols.rh && html``} - ${visibleCols.dew && html``} + ${visibleCols.dew && html``} ${visibleCols.precip && html`
+ ${fmt(r.utciAdj)}${u('°C')} + ${fmt(r.vehicleT)}${u('°C')} + ${fmt(r.indoorT)}${u('°C')} + ${fmt(r.managedT)}${u('°C')} + ${fmt(r.utci)}${u('°C')} ${fmt(r.Tmrt)}${u('°C')}${fmt(r.Tmrt)}${u('°C')} + ${fmt(r.concreteT)}${u('°C')} ${fmt(r.soilT0)}${u('°C')}${fmt(r.soilT0)}${u('°C')}${fmt(r.soilT6)}${u('°C')}${fmt(r.soilT6)}${u('°C')}${r.soilM != null ? fmt(r.soilM * 100, 1) + u('%') : '—'}${fmt(r.Ta)}${u('°C')}${fmt(r.Ta)}${u('°C')}${Math.round(r.RH)}${u('%')}${fmt(r.dew)}${u('°C')}${fmt(r.dew)}${u('°C')} 0 ? scaleBg(r.snow, 0, 4, '90,140,210') : scaleBg(r.precip, 0, 8, '70,145,200') }}>