diff --git a/assets/css/header.css b/assets/css/header.css index 051f6a6..7a1e804 100644 --- a/assets/css/header.css +++ b/assets/css/header.css @@ -242,6 +242,7 @@ gap: 4px; width: 100%; max-width: 270px; /* matches .scope-ring desktop size */ + padding-bottom: 32px; /* compensates for the extra Air temp row in the Why It Feels panel */ } /* Unified pill: icon on left, bar fills the right */ diff --git a/assets/css/ui.css b/assets/css/ui.css index 1501fee..b066740 100644 --- a/assets/css/ui.css +++ b/assets/css/ui.css @@ -1607,6 +1607,16 @@ color: #3a78a8; /* cool blue - cooling contribution */ } +.delta-base { + color: #4a3420; /* neutral — absolute air temperature, not a delta */ +} + +.insight-row--base { + opacity: 0.75; + border-bottom: 1px dashed rgba(74,52,32,0.2); + margin-bottom: 2px; +} + /* Values for "Today at a glance" */ .insight-value { font-family: JetBrains Mono, monospace; diff --git a/assets/js/app.js b/assets/js/app.js index 722ba24..dca39f6 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -742,6 +742,7 @@ export function UTCIForecast() { ${(() => { const _wflItems = [ + { label: 'Air temperature', icon: '🌡', value: currentRow.Ta, isBase: true }, { label: 'Sun and sky', icon: '☀', value: whyFeelsLike.sunAndSky }, { label: 'Wind', icon: '🌬', value: whyFeelsLike.wind }, { label: 'Humidity', icon: '💧', value: whyFeelsLike.humidity }, @@ -752,19 +753,20 @@ export function UTCIForecast() { { label: 'Precipitation', icon: '🌧', value: whyFeelsLike.precipitation } ] : []), ]; - const _wflMax = Math.max(..._wflItems.map(i => Math.abs(i.value)), 0.1); - return _wflItems.map(({ label, icon, value }) => html` -
+ const _wflDeltaItems = _wflItems.filter(i => !i.isBase); + const _wflMax = Math.max(..._wflDeltaItems.map(i => Math.abs(i.value)), 0.1); + return _wflItems.map(({ label, icon, value, isBase }) => html` +
${icon} ${label} - + ${!isBase && html` = 0 ? 'bar-pos' : 'bar-neg')} style=${{ width: `${Math.round(Math.abs(value) / _wflMax * 100)}%` }}> - + `} - = 0 ? 'delta-pos' : 'delta-neg'}`}> - ${value >= 0 ? '+' : ''}${value.toFixed(1)}° + = 0 ? 'delta-pos' : 'delta-neg'}`}> + ${isBase ? '' : value >= 0 ? '+' : ''}${value.toFixed(1)}°
`); diff --git a/assets/js/compute.js b/assets/js/compute.js index 5fcecc5..435a4b3 100644 --- a/assets/js/compute.js +++ b/assets/js/compute.js @@ -330,15 +330,27 @@ export function computeWhyFeelsLike(row, env) { const { Ta, Tmrt, va, eh, precip, snow } = row; const r1 = (v) => Math.round(v * 10) / 10; - // Radiation - mean radiant temp above or below air temp. - const sunAndSky = r1(Tmrt - Ta); + // Environment-adjusted air temp (same as used in buildHourlyRows). + const TaEnv = Ta + (env ? env.taOffset : 0); - // Wind - UTCI with actual wind vs calm, holding Tmrt = Ta and RH neutral. - const ehNeutral = vaporPressureHpa(Ta, 50); - const wind = r1(utciApprox(Ta, Ta, va, ehNeutral) - utciApprox(Ta, Ta, 0, ehNeutral)); + // Sequential UTCI isolation — all deltas are in the same UTCI-polynomial + // space so they add up: Ta + environment + sunAndSky + wind + humidity + // + precipitation ≈ SunSoak (utciAdj), within rounding. + // + // Tmrt already has env radiation scaling (dirFactor/difFactor/globFactor) + // baked in from buildHourlyRows, so each delta automatically reflects + // the active Solar Model without any extra work here. + const ehNeutral = vaporPressureHpa(TaEnv, 50); + const base = utciApprox(TaEnv, TaEnv, 0, ehNeutral); // ≈ TaEnv - // Humidity - actual vapour pressure vs neutral RH 50%, no wind or sun. - const humidity = r1(utciApprox(Ta, Ta, 0, eh) - utciApprox(Ta, Ta, 0, ehNeutral)); + // Radiation: effect of actual Tmrt vs no-radiation baseline (wind=0, RH=50%). + const sunAndSky = r1(utciApprox(TaEnv, Tmrt, 0, ehNeutral) - base); + + // Wind: cooling effect of actual wind with solar present (RH still neutral). + const wind = r1(utciApprox(TaEnv, Tmrt, va, ehNeutral) - utciApprox(TaEnv, Tmrt, 0, ehNeutral)); + + // Humidity: actual vapour pressure vs neutral RH 50%, with solar and wind. + const humidity = r1(utciApprox(TaEnv, Tmrt, va, eh) - utciApprox(TaEnv, Tmrt, va, ehNeutral)); // Environment modifier - explicit temperature offset from the env config. const environment = env ? r1(env.taOffset) : 0; @@ -346,7 +358,7 @@ export function computeWhyFeelsLike(row, env) { // Precipitation soak penalty - negative or zero. const precipitation = precipPenalty(precip, snow, va); - return { sunAndSky, wind, humidity, environment, precipitation }; + return { base: r1(base), sunAndSky, wind, humidity, environment, precipitation }; } // ------------------------------------------------------------------------