diff --git a/assets/js/app.js b/assets/js/app.js index d9fc5a1..1450cde 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -379,9 +379,9 @@ export function UTCIForecast() { // Columns that mark the start of a logical group - used to draw a faint // vertical border separating groups in the forecast table. const GROUP_ORDER = { - felt: ['utciP', 'shadeT', 'vehicleT', 'indoorT', 'managedT', 'petHomeT', 'burn', 'utci', 'delta', 'tmrt'], + felt: ['utciP', 'shadeT', 'vehicleT', 'indoorT', 'managedT', 'burn', 'utci', 'delta', 'tmrt'], surface: ['concreteT', 'soilT', 'soilT6', 'soilM', 'furSurfaceT', 'pawT'], - ambient: ['petShadeT', 'air', 'rh', 'dew'], + ambient: ['petShadeT', 'petHomeT', 'air', 'rh', 'dew'], precip: ['precip', 'precipProb', 'lightning'], sky: ['cloud', 'vis'], wind: ['wind', 'dir'], @@ -1160,7 +1160,7 @@ export function UTCIForecast() { ${colOffered('furSurfaceT') && html``} ${colOffered('pawT') && html``} ${colOffered('petShadeT') && html``} - ${colOffered('petHomeT') && html``} + ${colOffered('petHomeT') && html``} ${colOffered('burn') && html``} ${colOffered('utci') && html``} ${colOffered('delta') && html``} @@ -1355,7 +1355,6 @@ export function UTCIForecast() { ${visibleCols.vehicleT && html` handleThClick('vehicleT', e)} onMouseEnter=${(e) => handleThEnter('vehicleT', e)} onMouseLeave=${handleThLeave}>Vehicle °C peak`} ${indoorMode === 'on' && !indoorManaged && html` handleThClick('indoorT', e)} onMouseEnter=${(e) => handleThEnter('indoorT', e)} onMouseLeave=${handleThLeave}>Indoors °C est.`} ${indoorMode === 'on' && indoorManaged && html` handleThClick('managedT', e)} onMouseEnter=${(e) => handleThEnter('managedT', e)} onMouseLeave=${handleThLeave}>Managed °C est.`} - ${visibleCols.petHomeT && html` handleThClick('petHomeT', e)} onMouseEnter=${(e) => handleThEnter('petHomeT', e)} onMouseLeave=${handleThLeave}>Pet Home °C est.`} ${visibleCols.burn && html` handleThClick('burn', e)} onMouseEnter=${(e) => handleThEnter('burn', e)} onMouseLeave=${handleThLeave}>Burn to MED`} ${visibleCols.utci && html` handleThClick('utci', e)} onMouseEnter=${(e) => handleThEnter('utci', e)} onMouseLeave=${handleThLeave}>UTCI °C felt`} ${visibleCols.delta && html` handleThClick('delta', e)} onMouseEnter=${(e) => handleThEnter('delta', e)} onMouseLeave=${handleThLeave}>Δ UTCI−Air`} @@ -1367,6 +1366,7 @@ export function UTCIForecast() { ${visibleCols.soilT6 && html` handleThClick('soilT6', e)} onMouseEnter=${(e) => handleThEnter('soilT6', e)} onMouseLeave=${handleThLeave}>Soil 6cm °C root`} ${visibleCols.soilM && html` handleThClick('soilM', e)} onMouseEnter=${(e) => handleThEnter('soilM', e)} onMouseLeave=${handleThLeave}>Soil moist %`} ${visibleCols.petShadeT && html` handleThClick('petShadeT', e)} onMouseEnter=${(e) => handleThEnter('petShadeT', e)} onMouseLeave=${handleThLeave}>Pet Shade °C`} + ${visibleCols.petHomeT && html` handleThClick('petHomeT', e)} onMouseEnter=${(e) => handleThEnter('petHomeT', e)} onMouseLeave=${handleThLeave}>Pet Home °C est.`} ${visibleCols.air && html` handleThClick('air', e)} onMouseEnter=${(e) => handleThEnter('air', e)} onMouseLeave=${handleThLeave}>Air °C`} ${visibleCols.rh && html` handleThClick('rh', e)} onMouseEnter=${(e) => handleThEnter('rh', e)} onMouseLeave=${handleThLeave}>RH %`} ${visibleCols.dew && html` handleThClick('dew', e)} onMouseEnter=${(e) => handleThEnter('dew', e)} onMouseLeave=${handleThLeave}>Dew °C`} @@ -1537,10 +1537,6 @@ export function UTCIForecast() { ${fmt(r.managedT)}${u('°C')} `} - ${visibleCols.petHomeT && html` - - ${fmt(r.indoorT)}${u('°C')} - `} ${visibleCols.burn && html` 0 ? (burnMins < 30 ? '#c44a3a' : '#c8601a') : '#4a3218', background: burnBg(burnMins, r.uv) }}> ${burnLabel(burnMins)} @@ -1579,6 +1575,10 @@ export function UTCIForecast() { ${r.petShadeT != null ? fmt(r.petShadeT) + u('°C') : '—'} `} + ${visibleCols.petHomeT && html` + + ${fmt(r.indoorT)}${u('°C')} + `} ${visibleCols.air && html`${fmt(r.Ta)}${u('°C')}`} ${visibleCols.rh && html`${Math.round(r.RH)}${u('%')}`} ${visibleCols.dew && html`${fmt(r.dew)}${u('°C')}`} diff --git a/assets/js/compute.js b/assets/js/compute.js index 2713306..679d944 100644 --- a/assets/js/compute.js +++ b/assets/js/compute.js @@ -86,9 +86,20 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v // concreteT is now stamped in the two-pass section below (thermal lag). // vehicleT is stamped in the two-pass section below (thermal lag). const eh = vaporPressureHpa(Ta, RH); + // UTCI: the standard, unmodified benchmark. Computed from the raw + // open-ground observation only - NO Solar Model anywhere in it. Its whole + // job is to be the fixed reference SunSoak is read against, so it must + // not move when the user switches environment; if both numbers shifted + // together the comparison would say nothing. Only the SunSoak family + // (utciAdj, Shade, Tmrt) responds to the Solar Model. + const TmrtStd = calcTmrt(Ta, dir, dif, glob, elev); + const utci = utciApprox(Ta, TmrtStd, va, eh); + // SunSoak: same peer-reviewed polynomial, every input shifted by the + // active Solar Model (air temp via taOffset, radiation via dir/dif/glob + // factors), then the rain/snow soak penalty on top. const Tmrt = calcTmrt(TaEnv, dirEnv, difEnv, globEnv, elev); - const utci = utciApprox(TaEnv, Tmrt, va, eh); - const utciAdj = utci + precipPenalty(precip, snow, va); + const utciEnv = utciApprox(TaEnv, Tmrt, va, eh); + const utciAdj = utciEnv + precipPenalty(precip, snow, va); // Shade air temp: what a thermometer in this Solar Model's typical shade // (building shadow, beach umbrella, canopy...) would read. Shares // SunSoak's TaEnv baseline and env-reduced radiation, so the two stay @@ -757,7 +768,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols label: 'Pet at home', value: peakIndoor ? `${Math.round(peakIndoor.indoorT)}° at ${hhmm(peakIndoor.iso)}` : '-', alert: !!(peakIndoor && peakIndoor.indoorT >= 26), - grp: 'felt', + grp: 'ambient', }] : []), ...(show('vehicleT') ? [{ icon: '🧒', diff --git a/assets/js/config.js b/assets/js/config.js index f3003f0..0bf44cc 100644 --- a/assets/js/config.js +++ b/assets/js/config.js @@ -274,7 +274,7 @@ export const COL_DESCRIPTIONS = { diffuse: { title: 'Diffuse Radiation', desc: 'Scattered solar radiation arriving from all directions across the sky (W/m²). Present even under cloud; contributes to overall solar load.' }, tmrt: { title: 'Mean Radiant Temp', desc: 'The combined heat from direct sun, scattered sky light, and ground reflection expressed as a temperature. Typically well above air temperature even on overcast days, and is one of the inputs used to calculate UTCI.' }, delta: { title: 'UTCI − Air Delta', desc: 'The difference between the UTCI felt temperature and the plain air temperature. A large positive value means solar radiation is adding significant heat stress beyond what the thermometer shows.' }, - utci: { title: 'UTCI', desc: 'Universal Thermal Climate Index — the raw felt temperature combining air temp, humidity, wind, and solar radiation. Does not include precipitation effects.' }, + utci: { title: 'UTCI', desc: 'Universal Thermal Climate Index — the peer-reviewed standard felt temperature, combining air temp, humidity, wind, and solar radiation. Always computed from the raw open-ground reading: it does NOT move when you change the Solar Model, and it does not include precipitation. That is the point of it — a fixed benchmark to read SunSoak against, so the gap between the two columns shows exactly what your surroundings and the weather are adding.' }, uvA: { title: 'UV-A Index', desc: 'Estimated UV-A radiation index. UV-A penetrates deeper into the skin and contributes to long-term ageing and some skin cancers, even through glass.' }, uvB: { title: 'UV-B Index', desc: 'Estimated UV-B radiation index. UV-B causes sunburn and is the main driver of vitamin D production. Intensity depends strongly on solar elevation and cloud cover.' }, burn: { title: 'Burn Time', desc: 'Estimated time to reach one Minimal Erythemal Dose (MED) — the threshold for sunburn — based on the UV index and your selected skin type. This is a guide, not a medical measurement.' }, diff --git a/columns.html b/columns.html index a7e0835..8f913e1 100644 --- a/columns.html +++ b/columns.html @@ -96,7 +96,7 @@ Diffuse W/m²Solar radiation scattered from all directions across the sky. Present even under cloud cover; contributes to the overall solar heat load. Tmrt °CMean Radiant Temperature — the combined heat from direct sun, scattered sky light, and ground reflection expressed as a temperature. Can exceed air temperature by 20–30 °C on a sunny day; this is why shade feels so much cooler, and it is one of the inputs to UTCI. UTCI−Air ΔThe gap between the UTCI felt temperature and the plain air temperature. A large positive value means solar radiation is adding significant heat stress beyond what the thermometer shows. - UTCI °CUniversal Thermal Climate Index — the raw felt temperature combining air temp, humidity, wind, and solar radiation. Does not include precipitation effects. + UTCI °CUniversal Thermal Climate Index — the peer-reviewed standard felt temperature, combining air temp, humidity, wind, and solar radiation. Always computed from the raw open-ground reading: it does not move when you change the Solar Model, and it excludes precipitation. That is deliberate — it is the fixed benchmark to read SunSoak against, so the gap between the two shows exactly what your surroundings and the weather are adding. UV-AEstimated UV-A radiation index. UV-A penetrates deeper into the skin and contributes to long-term ageing and some skin cancers, even through glass. Present throughout daylight hours. UV-BEstimated UV-B radiation index. Causes sunburn and drives vitamin D production. Intensity depends strongly on solar elevation and cloud cover, falling sharply at low sun angles. Burn timeEstimated time to reach one Minimal Erythemal Dose (MED) — the threshold for sunburn — based on the UV index and your selected Fitzpatrick skin type (chosen from a dropdown in the column bar). A guide, not a medical measurement.