New column sort and groups
This commit is contained in:
fraxle
2026-05-22 21:15:24 +01:00
parent d6eedabbf9
commit ba98eaef2a
2 changed files with 201 additions and 168 deletions
+4
View File
@@ -701,6 +701,10 @@
letter-spacing: 0.06em; letter-spacing: 0.06em;
} }
/* Faint vertical divider before each new column group in the forecast table. */
.utci-table .col-group-start {
border-left: 2px solid rgba(74, 50, 24, 0.18);
}
/* ── FILTER PROFILES + COLUMN TOGGLES — mobile fixes ───────────────── */ /* ── FILTER PROFILES + COLUMN TOGGLES — mobile fixes ───────────────── */
@media (max-width: 640px) { @media (max-width: 640px) {
+197 -168
View File
@@ -81,6 +81,35 @@ export function UTCIForecast() {
bannerSlideTo, dismissBanner, bannerSlideTo, dismissBanner,
} = useAppState(); } = useAppState();
// 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 = {
solar: ['sun', 'direct', 'diffuse', 'uvA', 'uvB', 'burn'],
sky: ['cloud', 'vis', 'aqi', 'pollen'],
wind: ['wind', 'dir'],
ambient: ['air', 'rh', 'dew'],
felt: ['tmrt', 'utci', 'delta', 'utciP'],
surface: ['concreteT', 'vehicleT', 'indoorT', 'managedT', 'soilT', 'soilT6', 'soilM'],
precip: ['precip'],
};
const GROUP_OF = Object.fromEntries(
Object.entries(GROUP_ORDER).flatMap(([g, keys]) => keys.map(k => [k, g]))
);
// Returns col-group-start when this column is the leftmost VISIBLE member
// of its group. If the canonical first member is hidden the border migrates
// to the next visible column in the same group.
const isColVisible = (k) => {
if (k === 'indoorT') return indoorMode === 'on' && !indoorManaged;
if (k === 'managedT') return indoorMode === 'on' && indoorManaged;
return !!visibleCols[k];
};
const groupStart = (key) => {
const group = GROUP_OF[key];
if (!group) return '';
const first = GROUP_ORDER[group].find(isColVisible);
return first === key ? 'col-group-start' : '';
};
// ─── 4. JSX RETURN ─────────────────────────────────────────────────── // ─── 4. JSX RETURN ───────────────────────────────────────────────────
// Everything below is the actual page markup, written as one big HTM // Everything below is the actual page markup, written as one big HTM
// template. Search tips: // template. Search tips:
@@ -422,34 +451,34 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html`<button cla
<table class="utci-table utci-table-head" ref=${headTableRef}> <table class="utci-table utci-table-head" ref=${headTableRef}>
<thead> <thead>
<tr> <tr>
<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('hour', e)} onMouseEnter=${(e) => handleThEnter('hour', e)} onMouseLeave=${handleThLeave}>Hour</th> <th class="col-info-th" scope="col" onClick=${(e) => handleThClick('hour', e)} onMouseEnter=${(e) => handleThEnter('hour', e)} onMouseLeave=${handleThLeave}>Hour</th>
${visibleCols.air && html`<th class="utci-tight-head col-info-th" scope="col" onClick=${(e) => handleThClick('air', e)} onMouseEnter=${(e) => handleThEnter('air', e)} onMouseLeave=${handleThLeave}>Air <span class="col-unit">°C</span></th>`} ${visibleCols.sun && html`<th class=${`col-info-th ${groupStart('sun')}`} scope="col" onClick=${(e) => handleThClick('sun', e)} onMouseEnter=${(e) => handleThEnter('sun', e)} onMouseLeave=${handleThLeave}>Sun <span class="col-unit">elev°</span></th>`}
${visibleCols.rh && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('rh', e)} onMouseEnter=${(e) => handleThEnter('rh', e)} onMouseLeave=${handleThLeave}>RH <span class="col-unit">%</span></th>`} ${visibleCols.direct && html`<th class=${`col-info-th ${groupStart('direct')}`} scope="col" onClick=${(e) => handleThClick('direct', e)} onMouseEnter=${(e) => handleThEnter('direct', e)} onMouseLeave=${handleThLeave}>Direct <span class="col-unit">W/m²</span></th>`}
${visibleCols.dew && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('dew', e)} onMouseEnter=${(e) => handleThEnter('dew', e)} onMouseLeave=${handleThLeave}>Dew <span class="col-unit">°C</span></th>`} ${visibleCols.diffuse && html`<th class=${`col-info-th ${groupStart('diffuse')}`} scope="col" onClick=${(e) => handleThClick('diffuse', e)} onMouseEnter=${(e) => handleThEnter('diffuse', e)} onMouseLeave=${handleThLeave}>Diffuse <span class="col-unit">W/m²</span></th>`}
${visibleCols.soilT && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('soilT', e)} onMouseEnter=${(e) => handleThEnter('soilT', e)} onMouseLeave=${handleThLeave}>Soil °C <span class="col-unit">surface</span></th>`} ${visibleCols.uvA && html`<th class=${`col-info-th ${groupStart('uvA')}`} scope="col" onClick=${(e) => handleThClick('uvA', e)} onMouseEnter=${(e) => handleThEnter('uvA', e)} onMouseLeave=${handleThLeave}>UV-A <span class="col-unit">est. idx</span></th>`}
${visibleCols.soilT6 && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('soilT6', e)} onMouseEnter=${(e) => handleThEnter('soilT6', e)} onMouseLeave=${handleThLeave}>Soil 6cm <span class="col-unit">°C root</span></th>`} ${visibleCols.uvB && html`<th class=${`col-info-th ${groupStart('uvB')}`} scope="col" onClick=${(e) => handleThClick('uvB', e)} onMouseEnter=${(e) => handleThEnter('uvB', e)} onMouseLeave=${handleThLeave}>UV-B <span class="col-unit">est. idx</span></th>`}
${visibleCols.soilM && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('soilM', e)} onMouseEnter=${(e) => handleThEnter('soilM', e)} onMouseLeave=${handleThLeave}>Soil moist <span class="col-unit">m³/m³</span></th>`} ${visibleCols.burn && html`<th class=${`col-info-th ${groupStart('burn')}`} scope="col" onClick=${(e) => handleThClick('burn', e)} onMouseEnter=${(e) => handleThEnter('burn', e)} onMouseLeave=${handleThLeave}>Burn <span class="col-unit">to MED</span></th>`}
${visibleCols.concreteT && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('concreteT', e)} onMouseEnter=${(e) => handleThEnter('concreteT', e)} onMouseLeave=${handleThLeave}>Concrete <span class="col-unit">°C surface</span></th>`} ${visibleCols.cloud && html`<th class=${`col-info-th ${groupStart('cloud')}`} scope="col" onClick=${(e) => handleThClick('cloud', e)} onMouseEnter=${(e) => handleThEnter('cloud', e)} onMouseLeave=${handleThLeave}>Cloud <span class="col-unit">%</span></th>`}
${visibleCols.wind && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('wind', e)} onMouseEnter=${(e) => handleThEnter('wind', e)} onMouseLeave=${handleThLeave}>Wind <span class="col-unit">mph (gust)</span></th>`} ${visibleCols.vis && html`<th class=${`utci-tight-head col-info-th ${groupStart('vis')}`} scope="col" onClick=${(e) => handleThClick('vis', e)} onMouseEnter=${(e) => handleThEnter('vis', e)} onMouseLeave=${handleThLeave}>Vis <span class="col-unit">km</span></th>`}
${visibleCols.dir && html`<th class="utci-dir-cell col-info-th" scope="col" onClick=${(e) => handleThClick('dir', e)} onMouseEnter=${(e) => handleThEnter('dir', e)} onMouseLeave=${handleThLeave}>Dir <span class="col-unit">-</span></th>`} ${visibleCols.aqi && html`<th class=${`col-info-th ${groupStart('aqi')}`} scope="col" onClick=${(e) => handleThClick('aqi', e)} onMouseEnter=${(e) => handleThEnter('aqi', e)} onMouseLeave=${handleThLeave}>AQI <span class="col-unit">EU idx</span></th>`}
${visibleCols.cloud && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('cloud', e)} onMouseEnter=${(e) => handleThEnter('cloud', e)} onMouseLeave=${handleThLeave}>Cloud <span class="col-unit">%</span></th>`} ${visibleCols.pollen && html`<th class=${`col-info-th ${groupStart('pollen')}`} scope="col" onClick=${(e) => handleThClick('pollen', e)} onMouseEnter=${(e) => handleThEnter('pollen', e)} onMouseLeave=${handleThLeave}>${pollenType === 'all_pollen' ? 'Pollen' : POLLEN_TYPES[pollenType]?.name.replace(' pollen','') ?? 'Pollen'} <span class="col-unit">grains/m³</span></th>`}
${visibleCols.vis && html`<th class="utci-tight-head col-info-th" scope="col" onClick=${(e) => handleThClick('vis', e)} onMouseEnter=${(e) => handleThEnter('vis', e)} onMouseLeave=${handleThLeave}>Vis <span class="col-unit">km</span></th>`} ${visibleCols.wind && html`<th class=${`col-info-th ${groupStart('wind')}`} scope="col" onClick=${(e) => handleThClick('wind', e)} onMouseEnter=${(e) => handleThEnter('wind', e)} onMouseLeave=${handleThLeave}>Wind <span class="col-unit">mph (gust)</span></th>`}
${visibleCols.aqi && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('aqi', e)} onMouseEnter=${(e) => handleThEnter('aqi', e)} onMouseLeave=${handleThLeave}>AQI <span class="col-unit">EU idx</span></th>`} ${visibleCols.dir && html`<th class=${`utci-dir-cell col-info-th ${groupStart('dir')}`} scope="col" onClick=${(e) => handleThClick('dir', e)} onMouseEnter=${(e) => handleThEnter('dir', e)} onMouseLeave=${handleThLeave}>Dir <span class="col-unit">-</span></th>`}
${visibleCols.pollen && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('pollen', e)} onMouseEnter=${(e) => handleThEnter('pollen', e)} onMouseLeave=${handleThLeave}>${pollenType === 'all_pollen' ? 'Pollen' : POLLEN_TYPES[pollenType]?.name.replace(' pollen','') ?? 'Pollen'} <span class="col-unit">grains/m³</span></th>`} ${visibleCols.air && html`<th class=${`utci-tight-head col-info-th ${groupStart('air')}`} scope="col" onClick=${(e) => handleThClick('air', e)} onMouseEnter=${(e) => handleThEnter('air', e)} onMouseLeave=${handleThLeave}>Air <span class="col-unit">°C</span></th>`}
${visibleCols.sun && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('sun', e)} onMouseEnter=${(e) => handleThEnter('sun', e)} onMouseLeave=${handleThLeave}>Sun <span class="col-unit">elev°</span></th>`} ${visibleCols.rh && html`<th class=${`col-info-th ${groupStart('rh')}`} scope="col" onClick=${(e) => handleThClick('rh', e)} onMouseEnter=${(e) => handleThEnter('rh', e)} onMouseLeave=${handleThLeave}>RH <span class="col-unit">%</span></th>`}
${visibleCols.direct && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('direct', e)} onMouseEnter=${(e) => handleThEnter('direct', e)} onMouseLeave=${handleThLeave}>Direct <span class="col-unit">W/m²</span></th>`} ${visibleCols.dew && html`<th class=${`col-info-th ${groupStart('dew')}`} scope="col" onClick=${(e) => handleThClick('dew', e)} onMouseEnter=${(e) => handleThEnter('dew', e)} onMouseLeave=${handleThLeave}>Dew <span class="col-unit">°C</span></th>`}
${visibleCols.diffuse && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('diffuse', e)} onMouseEnter=${(e) => handleThEnter('diffuse', e)} onMouseLeave=${handleThLeave}>Diffuse <span class="col-unit">W/m²</span></th>`} ${visibleCols.tmrt && html`<th class=${`col-info-th ${groupStart('tmrt')}`} scope="col" onClick=${(e) => handleThClick('tmrt', e)} onMouseEnter=${(e) => handleThEnter('tmrt', e)} onMouseLeave=${handleThLeave}>Tmrt <span class="col-unit">°C</span></th>`}
${visibleCols.tmrt && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('tmrt', e)} onMouseEnter=${(e) => handleThEnter('tmrt', e)} onMouseLeave=${handleThLeave}>Tmrt <span class="col-unit">°C</span></th>`} ${visibleCols.utci && html`<th class=${`col-info-th ${groupStart('utci')}`} scope="col" onClick=${(e) => handleThClick('utci', e)} onMouseEnter=${(e) => handleThEnter('utci', e)} onMouseLeave=${handleThLeave}>UTCI <span class="col-unit">°C felt</span></th>`}
${visibleCols.delta && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('delta', e)} onMouseEnter=${(e) => handleThEnter('delta', e)} onMouseLeave=${handleThLeave}>Δ <span class="col-unit">UTCIAir</span></th>`} ${visibleCols.delta && html`<th class=${`col-info-th ${groupStart('delta')}`} scope="col" onClick=${(e) => handleThClick('delta', e)} onMouseEnter=${(e) => handleThEnter('delta', e)} onMouseLeave=${handleThLeave}>Δ <span class="col-unit">UTCIAir</span></th>`}
${visibleCols.utci && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('utci', e)} onMouseEnter=${(e) => handleThEnter('utci', e)} onMouseLeave=${handleThLeave}>UTCI <span class="col-unit">°C felt</span></th>`} ${visibleCols.utciP && html`<th class=${`col-info-th ${groupStart('utciP')}`} scope="col" onClick=${(e) => handleThClick('utciP', e)} onMouseEnter=${(e) => handleThEnter('utciP', e)} onMouseLeave=${handleThLeave}>UTCI+P <span class="col-unit">°C adj.</span></th>`}
${visibleCols.uvA && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('uvA', e)} onMouseEnter=${(e) => handleThEnter('uvA', e)} onMouseLeave=${handleThLeave}>UV-A <span class="col-unit">est. idx</span></th>`} ${visibleCols.concreteT && html`<th class=${`col-info-th ${groupStart('concreteT')}`} scope="col" onClick=${(e) => handleThClick('concreteT', e)} onMouseEnter=${(e) => handleThEnter('concreteT', e)} onMouseLeave=${handleThLeave}>Concrete <span class="col-unit">°C surface</span></th>`}
${visibleCols.uvB && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('uvB', e)} onMouseEnter=${(e) => handleThEnter('uvB', e)} onMouseLeave=${handleThLeave}>UV-B <span class="col-unit">est. idx</span></th>`} ${visibleCols.vehicleT && html`<th class=${`col-info-th ${groupStart('vehicleT')}`} scope="col" onClick=${(e) => handleThClick('vehicleT', e)} onMouseEnter=${(e) => handleThEnter('vehicleT', e)} onMouseLeave=${handleThLeave}>Vehicle <span class="col-unit">°C peak</span></th>`}
${visibleCols.burn && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('burn', e)} onMouseEnter=${(e) => handleThEnter('burn', e)} onMouseLeave=${handleThLeave}>Burn <span class="col-unit">to MED</span></th>`} ${indoorMode === 'on' && !indoorManaged && html`<th class=${`col-info-th ${groupStart('indoorT')}`} scope="col" onClick=${(e) => handleThClick('indoorT', e)} onMouseEnter=${(e) => handleThEnter('indoorT', e)} onMouseLeave=${handleThLeave}>Indoors <span class="col-unit">°C est.</span></th>`}
${visibleCols.vehicleT && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('vehicleT', e)} onMouseEnter=${(e) => handleThEnter('vehicleT', e)} onMouseLeave=${handleThLeave}>Vehicle <span class="col-unit">°C peak</span></th>`} ${indoorMode === 'on' && indoorManaged && html`<th class=${`col-info-th ${groupStart('managedT')}`} scope="col" onClick=${(e) => handleThClick('managedT', e)} onMouseEnter=${(e) => handleThEnter('managedT', e)} onMouseLeave=${handleThLeave}>Managed <span class="col-unit">°C est.</span></th>`}
${indoorMode === 'on' && !indoorManaged && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('indoorT', e)} onMouseEnter=${(e) => handleThEnter('indoorT', e)} onMouseLeave=${handleThLeave}>Indoors <span class="col-unit">°C est.</span></th>`} ${visibleCols.soilT && html`<th class=${`col-info-th ${groupStart('soilT')}`} scope="col" onClick=${(e) => handleThClick('soilT', e)} onMouseEnter=${(e) => handleThEnter('soilT', e)} onMouseLeave=${handleThLeave}>Soil °C <span class="col-unit">surface</span></th>`}
${indoorMode === 'on' && indoorManaged && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('managedT', e)} onMouseEnter=${(e) => handleThEnter('managedT', e)} onMouseLeave=${handleThLeave}>Managed <span class="col-unit">°C est.</span></th>`} ${visibleCols.soilT6 && html`<th class=${`col-info-th ${groupStart('soilT6')}`} scope="col" onClick=${(e) => handleThClick('soilT6', e)} onMouseEnter=${(e) => handleThEnter('soilT6', e)} onMouseLeave=${handleThLeave}>Soil 6cm <span class="col-unit">°C root</span></th>`}
${visibleCols.precip && html`<th class="utci-tight-head col-info-th" scope="col" onClick=${(e) => handleThClick('precip', e)} onMouseEnter=${(e) => handleThEnter('precip', e)} onMouseLeave=${handleThLeave}>Pcpt <span class="col-unit">mm/h</span></th>`} ${visibleCols.soilM && html`<th class=${`col-info-th ${groupStart('soilM')}`} scope="col" onClick=${(e) => handleThClick('soilM', e)} onMouseEnter=${(e) => handleThEnter('soilM', e)} onMouseLeave=${handleThLeave}>Soil moist <span class="col-unit">m³/m³</span></th>`}
${visibleCols.utciP && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('utciP', e)} onMouseEnter=${(e) => handleThEnter('utciP', e)} onMouseLeave=${handleThLeave}>UTCI+P <span class="col-unit">°C adj.</span></th>`} ${visibleCols.precip && html`<th class=${`utci-tight-head col-info-th ${groupStart('precip')}`} scope="col" onClick=${(e) => handleThClick('precip', e)} onMouseEnter=${(e) => handleThEnter('precip', e)} onMouseLeave=${handleThLeave}>Pcpt <span class="col-unit">mm/h</span></th>`}
</tr> </tr>
</thead> </thead>
</table> </table>
@@ -542,146 +571,146 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html`<button cla
})()} })()}
</span> </span>
</td> </td>
${visibleCols.air && html`<td style=${{ background: tempBg(r.Ta) }}>${r.Ta.toFixed(1)}</td>`} ${visibleCols.sun && html`<td class=${groupStart('sun')} style=${{ background: scaleBg(r.elev > 0 ? r.elev : null, 0, 70, '225,160,45') }}>${r.elev > 0 ? r.elev.toFixed(1) : '—'}</td>`}
${visibleCols.rh && html`<td style=${{ background: scaleBg(r.RH, 30, 100, '70,145,200') }}>${Math.round(r.RH)}</td>`} ${visibleCols.direct && html`<td class=${groupStart('direct')} style=${{ background: scaleBg(r.dir, 0, 850, '230,155,35') }}>${Math.round(r.dir)}</td>`}
${visibleCols.dew && html`<td style=${{ background: tempBg(r.dew) }}>${r.dew != null ? r.dew.toFixed(1) : '—'}</td>`} ${visibleCols.diffuse && html`<td class=${groupStart('diffuse')} style=${{ background: scaleBg(r.dif, 0, 450, '230,190,70') }}>${Math.round(r.dif)}</td>`}
${visibleCols.soilT && html` ${visibleCols.uvA && html`
<td style=${{ color: '#6b4a1c', background: tempBg(r.soilT0) }}>${r.soilT0 != null ? r.soilT0.toFixed(1) : '—'}</td>`} <td class=${groupStart('uvA')} style=${{ color: r.uvA > 0 ? '#c8922a' : '#4a3218', background: scaleBg(r.uvA, 0, 8, '220,155,40') }}>
${visibleCols.soilT6 && html` ${r.uvA > 0 ? r.uvA.toFixed(1) : '—'}
<td style=${{ color: '#6b4a1c', background: tempBg(r.soilT6) }}>${r.soilT6 != null ? r.soilT6.toFixed(1) : '—'}</td>`} </td>`}
${visibleCols.soilM && html` ${visibleCols.uvB && html`
<td style=${{ color: '#2a6a90', background: scaleBg(r.soilM, 0.12, 0.45, '50,125,190') }}>${r.soilM != null ? r.soilM.toFixed(3) : '—'}</td>`} <td class=${groupStart('uvB')} style=${{ color: r.uvB > 0 ? '#c44a3a' : '#4a3218', background: scaleBg(r.uvB, 0, 1.2, '210,70,50') }}>
${visibleCols.concreteT && html` ${r.uvB > 0 ? r.uvB.toFixed(2) : '—'}
<td style=${{ color: r.concreteT != null && r.concreteT > 40 ? '#c0392b' : r.concreteT != null && r.concreteT > 30 ? '#e67e22' : '#7f8c8d', fontWeight: 'bold', background: tempBg(r.concreteT) }}> </td>`}
${r.concreteT != null ? r.concreteT.toFixed(1) : '—'} ${visibleCols.burn && html`
</td>`} <td class=${groupStart('burn')} style=${{ color: r.uv > 0 ? (burnMins < 30 ? '#c44a3a' : '#c8601a') : '#4a3218', background: burnBg(burnMins, r.uv) }}>
${visibleCols.wind && html`<td style=${{ background: scaleBg((r.gust ?? r.va) * 2.237, 0, 40, '85,130,180') }}> ${burnLabel(burnMins)}
${Math.round(r.va * 2.237)}${r.gust != null && r.gust > r.va + 0.5 </td>`}
? (gm => html`<span style=${{ marginLeft: '4px', fontWeight: gm >= 25 ? 700 : 'normal', opacity: gm >= 25 ? 1 : 0.65, color: gm >= 55 ? '#b81010' : gm >= 40 ? '#d44010' : gm >= 25 ? '#c47a00' : 'inherit' }}>(${Math.round(gm)})</span>`)(r.gust * 2.237) ${visibleCols.cloud && html`<td class=${groupStart('cloud')} style=${{ background: scaleBg(r.cc, 0, 100, '110,130,150', 0.07), verticalAlign: 'middle' }}>
: ''} <span style=${{ display: 'inline-flex', alignItems: 'center', gap: '6px', verticalAlign: 'middle' }}>
</td>`} <span style=${{ position: 'relative', top: '6px' }}>
${visibleCols.dir && html`<td class="utci-dir-cell"> <${CloudIcon} category=${r.cloudCat} elev=${r.elev} dt=${r.dt} size=${28} />
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '6px', verticalAlign: 'middle' }}> </span>
<${WindVane} bearing=${r.wd} size=${28} /> <span>${Math.round(r.cc)}</span>
<span class="wind-dir-label" style=${{ fontFamily: 'Manrope, sans-serif', fontSize: '11px', fontWeight: 700 }}>${r.compass.label}</span> </span>
</span> </td>`}
</td>`} ${visibleCols.vis && (() => {
${visibleCols.cloud && html`<td style=${{ background: scaleBg(r.cc, 0, 100, '110,130,150', 0.07), verticalAlign: 'middle' }}> const v = r.visKm;
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '6px', verticalAlign: 'middle' }}> const bg = v == null ? 'transparent'
<span style=${{ position: 'relative', top: '6px' }}> : v < 1 ? 'rgba(180,80,80,0.18)'
<${CloudIcon} category=${r.cloudCat} elev=${r.elev} dt=${r.dt} size=${28} /> : v < 4 ? 'rgba(210,140,50,0.15)'
</span> : v < 10 ? 'rgba(200,190,80,0.12)'
<span>${Math.round(r.cc)}</span> : 'transparent';
</span> const color = v != null && v < 4 ? '#8a3a1a' : '#4a3218';
</td>`} return html`<td class=${groupStart('vis')} style=${{ background: bg, color }}>
${visibleCols.vis && (() => { ${v != null ? (v < 10 ? v.toFixed(1) : Math.round(v)) : '—'}
const v = r.visKm; </td>`;
const bg = v == null ? 'transparent' })()}
: v < 1 ? 'rgba(180,80,80,0.18)' ${visibleCols.aqi && (() => {
: v < 4 ? 'rgba(210,140,50,0.15)' const v = r.aqi;
: v < 10 ? 'rgba(200,190,80,0.12)' const bg = v == null ? 'transparent'
: 'transparent'; : v < 20 ? 'rgba(80,180,100,0.15)'
const color = v != null && v < 4 ? '#8a3a1a' : '#4a3218'; : v < 40 ? 'rgba(140,200,100,0.13)'
return html`<td style=${{ background: bg, color }}> : v < 60 ? 'rgba(220,200,60,0.15)'
${v != null ? (v < 10 ? v.toFixed(1) : Math.round(v)) : '—'} : v < 80 ? 'rgba(220,130,50,0.18)'
</td>`; : v < 100 ? 'rgba(200,70,50,0.18)'
})()} : 'rgba(160,30,100,0.20)';
${visibleCols.aqi && (() => { const color = v != null && v >= 60 ? '#7a2010' : v != null && v >= 40 ? '#7a4a10' : '#2a4a20';
const v = r.aqi; const label = v == null ? '—'
const bg = v == null ? 'transparent' : v < 20 ? `${v} Good`
: v < 20 ? 'rgba(80,180,100,0.15)' : v < 40 ? `${v} Fair`
: v < 40 ? 'rgba(140,200,100,0.13)' : v < 60 ? `${v} Mod`
: v < 60 ? 'rgba(220,200,60,0.15)' : v < 80 ? `${v} Poor`
: v < 80 ? 'rgba(220,130,50,0.18)' : v < 100 ? `${v} V.Poor`
: v < 100 ? 'rgba(200,70,50,0.18)' : `${v} Hazard`;
: 'rgba(160,30,100,0.20)'; return html`<td class=${groupStart('aqi')} style=${{ background: bg, color, fontWeight: v != null && v >= 60 ? 600 : 400 }}>${label}</td>`;
const color = v != null && v >= 60 ? '#7a2010' : v != null && v >= 40 ? '#7a4a10' : '#2a4a20'; })()}
const label = v == null ? '—' ${visibleCols.pollen && (() => {
: v < 20 ? `${v} Good` const pollenMap = {
: v < 40 ? `${v} Fair` all_pollen: [r.grassPollen, r.birchPollen, r.alderPollen, r.mugwortPollen, r.olivePollen, r.ragweedPollen].reduce((s, x) => x != null ? s + x : s, null),
: v < 60 ? `${v} Mod` grass_pollen: r.grassPollen,
: v < 80 ? `${v} Poor` birch_pollen: r.birchPollen,
: v < 100 ? `${v} V.Poor` alder_pollen: r.alderPollen,
: `${v} Hazard`; mugwort_pollen: r.mugwortPollen,
return html`<td style=${{ background: bg, color, fontWeight: v != null && v >= 60 ? 600 : 400 }}>${label}</td>`; olive_pollen: r.olivePollen,
})()} ragweed_pollen: r.ragweedPollen,
${visibleCols.pollen && (() => { };
const pollenMap = { const v = pollenMap[pollenType] ?? null;
all_pollen: [r.grassPollen, r.birchPollen, r.alderPollen, r.mugwortPollen, r.olivePollen, r.ragweedPollen].reduce((s, x) => x != null ? s + x : s, null), const bg = v == null ? 'transparent'
grass_pollen: r.grassPollen, : v < 10 ? 'transparent'
birch_pollen: r.birchPollen, : v < 50 ? 'rgba(180,200,80,0.13)'
alder_pollen: r.alderPollen, : v < 200 ? 'rgba(210,150,50,0.16)'
mugwort_pollen: r.mugwortPollen, : 'rgba(200,70,50,0.18)';
olive_pollen: r.olivePollen, const color = v != null && v >= 200 ? '#8a2010' : v != null && v >= 50 ? '#7a4a10' : '#4a3218';
ragweed_pollen: r.ragweedPollen, const label = v == null ? '—'
}; : v < 10 ? `${Math.round(v)} Low`
const v = pollenMap[pollenType] ?? null; : v < 50 ? `${Math.round(v)} Mod`
const bg = v == null ? 'transparent' : v < 200 ? `${Math.round(v)} High`
: v < 10 ? 'transparent' : `${Math.round(v)} V.High`;
: v < 50 ? 'rgba(180,200,80,0.13)' return html`<td class=${groupStart('pollen')} style=${{ background: bg, color, fontWeight: v != null && v >= 50 ? 600 : 400 }}>${label}</td>`;
: v < 200 ? 'rgba(210,150,50,0.16)' })()}
: 'rgba(200,70,50,0.18)'; ${visibleCols.wind && html`<td class=${groupStart('wind')} style=${{ background: scaleBg((r.gust ?? r.va) * 2.237, 0, 40, '85,130,180') }}>
const color = v != null && v >= 200 ? '#8a2010' : v != null && v >= 50 ? '#7a4a10' : '#4a3218'; ${Math.round(r.va * 2.237)}${r.gust != null && r.gust > r.va + 0.5
const label = v == null ? '—' ? (gm => html`<span style=${{ marginLeft: '4px', fontWeight: gm >= 25 ? 700 : 'normal', opacity: gm >= 25 ? 1 : 0.65, color: gm >= 55 ? '#b81010' : gm >= 40 ? '#d44010' : gm >= 25 ? '#c47a00' : 'inherit' }}>(${Math.round(gm)})</span>`)(r.gust * 2.237)
: v < 10 ? `${Math.round(v)} Low` : ''}
: v < 50 ? `${Math.round(v)} Mod` </td>`}
: v < 200 ? `${Math.round(v)} High` ${visibleCols.dir && html`<td class=${`utci-dir-cell ${groupStart('dir')}`}>
: `${Math.round(v)} V.High`; <span style=${{ display: 'inline-flex', alignItems: 'center', gap: '6px', verticalAlign: 'middle' }}>
return html`<td style=${{ background: bg, color, fontWeight: v != null && v >= 50 ? 600 : 400 }}>${label}</td>`; <${WindVane} bearing=${r.wd} size=${28} />
})()} <span class="wind-dir-label" style=${{ fontFamily: 'Manrope, sans-serif', fontSize: '11px', fontWeight: 700 }}>${r.compass.label}</span>
${visibleCols.sun && html`<td style=${{ background: scaleBg(r.elev > 0 ? r.elev : null, 0, 70, '225,160,45') }}>${r.elev > 0 ? r.elev.toFixed(1) : '—'}</td>`} </span>
${visibleCols.direct && html`<td style=${{ background: scaleBg(r.dir, 0, 850, '230,155,35') }}>${Math.round(r.dir)}</td>`} </td>`}
${visibleCols.diffuse && html`<td style=${{ background: scaleBg(r.dif, 0, 450, '230,190,70') }}>${Math.round(r.dif)}</td>`} ${visibleCols.air && html`<td class=${groupStart('air')} style=${{ background: tempBg(r.Ta) }}>${r.Ta.toFixed(1)}</td>`}
${visibleCols.tmrt && html`<td style=${{ background: tempBg(r.Tmrt) }}>${r.Tmrt.toFixed(1)}</td>`} ${visibleCols.rh && html`<td class=${groupStart('rh')} style=${{ background: scaleBg(r.RH, 30, 100, '70,145,200') }}>${Math.round(r.RH)}</td>`}
${visibleCols.delta && html` ${visibleCols.dew && html`<td class=${groupStart('dew')} style=${{ background: tempBg(r.dew) }}>${r.dew != null ? r.dew.toFixed(1) : '—'}</td>`}
<td style=${{ ${visibleCols.tmrt && html`<td class=${groupStart('tmrt')} style=${{ background: tempBg(r.Tmrt) }}>${r.Tmrt.toFixed(1)}</td>`}
color: delta > 3 ? '#c8601a' : delta < -3 ? '#3f73c4' : '#4a3218', ${visibleCols.utci && html`
fontWeight: 600, <td class=${groupStart('utci')} style=${{ background: hexToRgba(cat.bg, 0.22), color: cat.fg === '#fff' ? cat.fg : '#2a1a08', fontWeight: 600 }}>
background: deltaBg(delta), ${r.utci.toFixed(1)}°
}}> </td>`}
${delta > 0 ? '+' : ''}${delta.toFixed(1)} ${visibleCols.delta && html`
</td>`} <td class=${groupStart('delta')} style=${{
${visibleCols.utci && html` color: delta > 3 ? '#c8601a' : delta < -3 ? '#3f73c4' : '#4a3218',
<td style=${{ background: hexToRgba(cat.bg, 0.22), color: cat.fg === '#fff' ? cat.fg : '#2a1a08', fontWeight: 600 }}> fontWeight: 600,
${r.utci.toFixed(1)}° background: deltaBg(delta),
</td>`} }}>
${visibleCols.uvA && html` ${delta > 0 ? '+' : ''}${delta.toFixed(1)}
<td style=${{ color: r.uvA > 0 ? '#c8922a' : '#4a3218', background: scaleBg(r.uvA, 0, 8, '220,155,40') }}> </td>`}
${r.uvA > 0 ? r.uvA.toFixed(1) : '—'} ${visibleCols.utciP && html`
</td>`} <td class=${groupStart('utciP')} style=${{ background: hexToRgba(adjCat.bg, 0.55), color: adjCat.fg === '#fff' ? adjCat.fg : '#2a1a08', fontWeight: 700, fontSize: '13px' }}>
${visibleCols.uvB && html` ${r.utciAdj.toFixed(1)}°
<td style=${{ color: r.uvB > 0 ? '#c44a3a' : '#4a3218', background: scaleBg(r.uvB, 0, 1.2, '210,70,50') }}> </td>`}
${r.uvB > 0 ? r.uvB.toFixed(2) : '—'} ${visibleCols.concreteT && html`
</td>`} <td class=${groupStart('concreteT')} style=${{ color: r.concreteT != null && r.concreteT > 40 ? '#c0392b' : r.concreteT != null && r.concreteT > 30 ? '#e67e22' : '#7f8c8d', fontWeight: 'bold', background: tempBg(r.concreteT) }}>
${visibleCols.burn && html` ${r.concreteT != null ? r.concreteT.toFixed(1) : '—'}
<td style=${{ color: r.uv > 0 ? (burnMins < 30 ? '#c44a3a' : '#c8601a') : '#4a3218', background: burnBg(burnMins, r.uv) }}> </td>`}
${burnLabel(burnMins)} ${visibleCols.vehicleT && html`
</td>`} <td class=${groupStart('vehicleT')} style=${{ color: r.vehicleT != null && r.vehicleT > 45 ? '#c0392b' : r.vehicleT != null && r.vehicleT > 35 ? '#e67e22' : '#7f8c8d', fontWeight: 'bold', background: tempBgStrong(r.vehicleT) }}>
${visibleCols.vehicleT && html` ${r.vehicleT != null ? r.vehicleT.toFixed(1) : '—'}
<td style=${{ color: r.vehicleT != null && r.vehicleT > 45 ? '#c0392b' : r.vehicleT != null && r.vehicleT > 35 ? '#e67e22' : '#7f8c8d', fontWeight: 'bold', background: tempBgStrong(r.vehicleT) }}> </td>`}
${r.vehicleT != null ? r.vehicleT.toFixed(1) : '—'} ${indoorMode === 'on' && !indoorManaged && html`
</td>`} <td class=${groupStart('indoorT')} style=${{ color: r.indoorT != null && r.indoorT > 32 ? '#c0392b' : r.indoorT != null && r.indoorT > 26 ? '#e67e22' : '#4a7a4a', fontWeight: 'bold', background: tempBgStrong(r.indoorT) }}>
${indoorMode === 'on' && !indoorManaged && html` ${r.indoorT != null ? r.indoorT.toFixed(1) : '—'}
<td style=${{ color: r.indoorT != null && r.indoorT > 32 ? '#c0392b' : r.indoorT != null && r.indoorT > 26 ? '#e67e22' : '#4a7a4a', fontWeight: 'bold', background: tempBgStrong(r.indoorT) }}> </td>`}
${r.indoorT != null ? r.indoorT.toFixed(1) : '—'} ${indoorMode === 'on' && indoorManaged && html`
</td>`} <td class=${groupStart('managedT')} style=${{ color: r.managedT != null && r.managedT > 32 ? '#c0392b' : r.managedT != null && r.managedT > 26 ? '#e67e22' : '#4a7a4a', fontWeight: 'bold', background: tempBgStrong(r.managedT) }}>
${indoorMode === 'on' && indoorManaged && html` ${r.managedT != null ? r.managedT.toFixed(1) : '—'}
<td style=${{ color: r.managedT != null && r.managedT > 32 ? '#c0392b' : r.managedT != null && r.managedT > 26 ? '#e67e22' : '#4a7a4a', fontWeight: 'bold', background: tempBgStrong(r.managedT) }}> </td>`}
${r.managedT != null ? r.managedT.toFixed(1) : '—'} ${visibleCols.soilT && html`
</td>`} <td class=${groupStart('soilT')} style=${{ color: '#6b4a1c', background: tempBg(r.soilT0) }}>${r.soilT0 != null ? r.soilT0.toFixed(1) : '—'}</td>`}
${visibleCols.precip && html` ${visibleCols.soilT6 && html`
<td style=${{ background: r.snow > 0 ? scaleBg(r.snow, 0, 4, '90,140,210') : scaleBg(r.precip, 0, 8, '70,145,200') }}> <td class=${groupStart('soilT6')} style=${{ color: '#6b4a1c', background: tempBg(r.soilT6) }}>${r.soilT6 != null ? r.soilT6.toFixed(1) : '—'}</td>`}
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '5px', verticalAlign: 'middle' }}> ${visibleCols.soilM && html`
<${PrecipIcon} precip=${r.precip} snow=${r.snow} size=${28} /> <td class=${groupStart('soilM')} style=${{ color: '#2a6a90', background: scaleBg(r.soilM, 0.12, 0.45, '50,125,190') }}>${r.soilM != null ? r.soilM.toFixed(3) : '—'}</td>`}
<span style=${{ color: r.snow > 0 ? '#2a5fa8' : r.precip > 0 ? '#2a6a90' : '#7a5c30' }}> ${visibleCols.precip && html`
${r.snow > 0 ? r.snow.toFixed(1) + 'cm' : r.precip > 0 ? r.precip.toFixed(1) : '—'} <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> <span style=${{ display: 'inline-flex', alignItems: 'center', gap: '5px', verticalAlign: 'middle' }}>
</span> <${PrecipIcon} precip=${r.precip} snow=${r.snow} size=${28} />
</td>`} <span style=${{ color: r.snow > 0 ? '#2a5fa8' : r.precip > 0 ? '#2a6a90' : '#7a5c30' }}>
${visibleCols.utciP && html` ${r.snow > 0 ? r.snow.toFixed(1) + 'cm' : r.precip > 0 ? r.precip.toFixed(1) : '—'}
<td style=${{ background: hexToRgba(adjCat.bg, 0.55), color: adjCat.fg === '#fff' ? adjCat.fg : '#2a1a08', fontWeight: 700, fontSize: '13px' }}> </span>
${r.utciAdj.toFixed(1)}° </span>
</td>`} </td>`}
</tr>`; </tr>`;
})} })}
</tbody> </tbody>