Add table tick boxes for decimals and degree symbols in data
Change Soil Moist to %
This commit is contained in:
fraxle
2026-05-24 12:33:02 +01:00
parent d7bfaf11b6
commit 69788f1069
5 changed files with 85 additions and 27 deletions
+5 -5
View File
@@ -153,7 +153,7 @@
"name": "How can I tell if the ground is too wet for a motorhome or caravan?", "name": "How can I tell if the ground is too wet for a motorhome or caravan?",
"acceptedAnswer": { "acceptedAnswer": {
"@type": "Answer", "@type": "Answer",
"text": "SunScope's soil moisture column shows volumetric water content in m³/m³. A reading above 0.4 indicates saturated ground where a heavy vehicle is likely to sink or churn the surface. Below 0.2 is dry and firm. The Vehicle profile includes soil moisture by default for motorhome and caravan users assessing pitch suitability." "text": "SunScope's soil moisture column shows water saturation of the top 1 cm of soil as a percentage. A reading above 40% indicates saturated ground where a heavy vehicle is likely to sink or churn the surface. Below 20% is dry and firm. The Vehicle profile includes soil moisture by default for motorhome and caravan users assessing pitch suitability."
} }
}, },
{ {
@@ -411,7 +411,7 @@
temperature</strong> give context, <strong>precipitation</strong> covers driving temperature</strong> give context, <strong>precipitation</strong> covers driving
conditions, and <strong>UTCI+P</strong> tells you what it will feel like when you step conditions, and <strong>UTCI+P</strong> tells you what it will feel like when you step
outside. <strong>Soil moisture</strong> is included specifically for motorhome and caravan outside. <strong>Soil moisture</strong> is included specifically for motorhome and caravan
users — values above 0.4 m³/m³ indicate saturated ground where pitching or manoeuvring users — values above 40% indicate saturated ground where pitching or manoeuvring
is likely to cause problems. is likely to cause problems.
</p> </p>
</div> </div>
@@ -476,7 +476,7 @@
<strong>UV-A</strong> and <strong>burn time</strong> for daytime sun protection, <strong>UV-A</strong> and <strong>burn time</strong> for daytime sun protection,
<strong>precipitation</strong> for planning, <strong>air quality index</strong> and <strong>precipitation</strong> for planning, <strong>air quality index</strong> and
<strong>pollen</strong> — particularly relevant for multi-day outdoor exposure — and <strong>pollen</strong> — particularly relevant for multi-day outdoor exposure — and
<strong>soil moisture</strong> — the mud indicator. Values above 0.4 m³/m³ mean the ground <strong>soil moisture</strong> — the mud indicator. Values above 40% mean the ground
is saturated and vehicle access, camping areas, and walkways will be affected. is saturated and vehicle access, camping areas, and walkways will be affected.
</p> </p>
<p> <p>
@@ -652,7 +652,7 @@
<tr><td><strong>Precip mm</strong></td><td>Expected rainfall or snowfall in mm per hour. Even light drizzle meaningfully reduces felt temperature when combined with wind.</td></tr> <tr><td><strong>Precip mm</strong></td><td>Expected rainfall or snowfall in mm per hour. Even light drizzle meaningfully reduces felt temperature when combined with wind.</td></tr>
<tr><td><strong>Soil °C surface</strong></td><td>Temperature of the soil at the surface (0 cm depth). Most seeds germinate above 710 °C. Useful for sowing decisions and frost assessment.</td></tr> <tr><td><strong>Soil °C surface</strong></td><td>Temperature of the soil at the surface (0 cm depth). Most seeds germinate above 710 °C. Useful for sowing decisions and frost assessment.</td></tr>
<tr><td><strong>Soil °C 6 cm</strong></td><td>Soil temperature at 6 cm depth — the root zone for many crops and seedlings. Lags behind the surface by several hours.</td></tr> <tr><td><strong>Soil °C 6 cm</strong></td><td>Soil temperature at 6 cm depth — the root zone for many crops and seedlings. Lags behind the surface by several hours.</td></tr>
<tr><td><strong>Soil moisture</strong></td><td>Volumetric water content of the top 1 cm of soil in m³/m³. Above 0.4 indicates saturated ground; below 0.2 is dry. Useful for assessing whether ground is workable, and for motorhome or caravan pitch suitability.</td></tr> <tr><td><strong>Soil moisture</strong></td><td>Water saturation of the top 1 cm of soil as a percentage. Above 40% indicates saturated ground; below 20% is dry. Useful for assessing whether ground is workable, and for motorhome or caravan pitch suitability.</td></tr>
<tr><td><strong>Concrete °C</strong></td><td>Estimated temperature of sun-exposed urban paving. Concrete absorbs more solar energy than grass and cannot cool itself through evaporation — surface temps can run 1525 °C above air temperature on sunny days.</td></tr> <tr><td><strong>Concrete °C</strong></td><td>Estimated temperature of sun-exposed urban paving. Concrete absorbs more solar energy than grass and cannot cool itself through evaporation — surface temps can run 1525 °C above air temperature on sunny days.</td></tr>
<tr><td><strong>Vehicle °C</strong></td><td>Estimated ambient cabin temperature inside a sealed, parked vehicle — adjusted for your chosen vehicle type. Values above 35 °C are dangerous for children and pets; above 45 °C potentially fatal within minutes.</td></tr> <tr><td><strong>Vehicle °C</strong></td><td>Estimated ambient cabin temperature inside a sealed, parked vehicle — adjusted for your chosen vehicle type. Values above 35 °C are dangerous for children and pets; above 45 °C potentially fatal within minutes.</td></tr>
<tr><td><strong>Indoors °C</strong></td><td>Estimated temperature inside a building with windows closed and no active cooling. Select your building type from the dropdown — brick, modern insulated, Victorian terrace, stone cottage, timber frame, top-floor flat, or conservatory. Each type has its own insulation, retained warmth, thermal mass, and glazing characteristics. Indoor peak typically lags the outdoor peak by 24 hours due to thermal mass.</td></tr> <tr><td><strong>Indoors °C</strong></td><td>Estimated temperature inside a building with windows closed and no active cooling. Select your building type from the dropdown — brick, modern insulated, Victorian terrace, stone cottage, timber frame, top-floor flat, or conservatory. Each type has its own insulation, retained warmth, thermal mass, and glazing characteristics. Indoor peak typically lags the outdoor peak by 24 hours due to thermal mass.</td></tr>
@@ -801,7 +801,7 @@
<details class="faq-block"> <details class="faq-block">
<summary class="faq-q">How can I tell if the ground is too wet for a motorhome or caravan?</summary> <summary class="faq-q">How can I tell if the ground is too wet for a motorhome or caravan?</summary>
<p class="faq-a">SunScope's soil moisture column shows the volumetric water content of the top centimetre of soil in m³/m³. A reading above 0.4 indicates saturated ground where a heavy vehicle is likely to sink or churn the surface. Below 0.2 is dry and firm. The Vehicle profile includes soil moisture by default precisely for motorhome and caravan users who need to assess pitch suitability before arriving on site.</p> <p class="faq-a">SunScope's soil moisture column shows the water saturation of the top centimetre of soil as a percentage. A reading above 40% indicates saturated ground where a heavy vehicle is likely to sink or churn the surface. Below 20% is dry and firm. The Vehicle profile includes soil moisture by default precisely for motorhome and caravan users who need to assess pitch suitability before arriving on site.</p>
</details> </details>
<details class="faq-block"> <details class="faq-block">
+22
View File
@@ -679,6 +679,26 @@
text-decoration: underline; text-decoration: underline;
} }
/* display-options row - decimal and degree symbol toggles */
.display-options {
display: flex;
justify-content: flex-end;
gap: 14px;
padding: 4px 10px 2px;
font-size: 12px;
color: var(--c-muted, #888);
}
.display-opt {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
user-select: none;
}
.display-opt input[type="checkbox"] {
accent-color: var(--c-accent, #e07b2a);
cursor: pointer;
}
/* ── 12. RESPONSIVE (mobile) ───────────────────────────────────────── */ /* ── 12. RESPONSIVE (mobile) ───────────────────────────────────────── */
/* On screens narrower than 800px, stack the header vertically and /* On screens narrower than 800px, stack the header vertically and
@@ -727,6 +747,8 @@
} }
/* -- CUSTOM SELECT BUTTON - mobile label clipping ---------------------- /* -- CUSTOM SELECT BUTTON - mobile label clipping ----------------------
On narrow screens buttons are fixed ~33% width. The .cs-btn-label span On narrow screens buttons are fixed ~33% width. The .cs-btn-label span
wraps the text node so ellipsis can work (text-overflow needs a block wraps the text node so ellipsis can work (text-overflow needs a block
+33 -21
View File
@@ -52,6 +52,8 @@ export function UTCIForecast() {
isPro, setIsPro, isPro, setIsPro,
activeProfile, setActiveProfile, activateProfile, activeCols, activeProfile, setActiveProfile, activateProfile, activeCols,
visibleCols, setVisibleCols, toggleCol, visibleCols, setVisibleCols, toggleCol,
showDecimals, toggleShowDecimals,
showDegree, toggleShowDegree,
activityOptions, placeOptions, activityOptions, placeOptions,
activityValue, activityLabel, placeValue, placeLabel, activityValue, activityLabel, placeValue, placeLabel,
skinType, setSkinType, skinType, setSkinType,
@@ -459,8 +461,16 @@ ${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html`<button c
</div>`} </div>`}
<div class="display-options">
<label class="display-opt">
<input type="checkbox" checked=${showDecimals} onChange=${toggleShowDecimals} />
Decimals
</label>
<label class="display-opt">
<input type="checkbox" checked=${showDegree} onChange=${toggleShowDegree} />
°&nbsp;symbol
</label>
</div>
<div class=${`utci-table-wrap${tableCanScrollLeft ? ' scroll-fade-left' : ''}${tableCanScrollRight ? ' scroll-fade-right' : ''}`} ref=${tableWrapRef}> <div class=${`utci-table-wrap${tableCanScrollLeft ? ' scroll-fade-left' : ''}${tableCanScrollRight ? ' scroll-fade-right' : ''}`} ref=${tableWrapRef}>
<span class="utci-scroll-chevron left" aria-hidden="true"></span> <span class="utci-scroll-chevron left" aria-hidden="true"></span>
@@ -484,7 +494,7 @@ ${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html`<button c
${visibleCols.concreteT && html`<th class=${`col-info-th ${groupStart('concreteT')} ${groupColor('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.concreteT && html`<th class=${`col-info-th ${groupStart('concreteT')} ${groupColor('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.soilT && html`<th class=${`col-info-th ${groupStart('soilT')} ${groupColor('soilT')}`} scope="col" onClick=${(e) => handleThClick('soilT', e)} onMouseEnter=${(e) => handleThEnter('soilT', e)} onMouseLeave=${handleThLeave}>Soil °C <span class="col-unit">surface</span></th>`} ${visibleCols.soilT && html`<th class=${`col-info-th ${groupStart('soilT')} ${groupColor('soilT')}`} scope="col" onClick=${(e) => handleThClick('soilT', e)} onMouseEnter=${(e) => handleThEnter('soilT', e)} onMouseLeave=${handleThLeave}>Soil °C <span class="col-unit">surface</span></th>`}
${visibleCols.soilT6 && html`<th class=${`col-info-th ${groupStart('soilT6')} ${groupColor('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.soilT6 && html`<th class=${`col-info-th ${groupStart('soilT6')} ${groupColor('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.soilM && html`<th class=${`col-info-th ${groupStart('soilM')} ${groupColor('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.soilM && html`<th class=${`col-info-th ${groupStart('soilM')} ${groupColor('soilM')}`} scope="col" onClick=${(e) => handleThClick('soilM', e)} onMouseEnter=${(e) => handleThEnter('soilM', e)} onMouseLeave=${handleThLeave}>Soil moist <span class="col-unit">%</span></th>`}
${visibleCols.air && html`<th class=${`utci-tight-head col-info-th ${groupStart('air')} ${groupColor('air')}`} scope="col" onClick=${(e) => handleThClick('air', e)} onMouseEnter=${(e) => handleThEnter('air', e)} onMouseLeave=${handleThLeave}>Air <span class="col-unit">°C</span></th>`} ${visibleCols.air && html`<th class=${`utci-tight-head col-info-th ${groupStart('air')} ${groupColor('air')}`} scope="col" onClick=${(e) => handleThClick('air', e)} onMouseEnter=${(e) => handleThEnter('air', e)} onMouseLeave=${handleThLeave}>Air <span class="col-unit">°C</span></th>`}
${visibleCols.rh && html`<th class=${`col-info-th ${groupStart('rh')} ${groupColor('rh')}`} scope="col" onClick=${(e) => handleThClick('rh', e)} onMouseEnter=${(e) => handleThEnter('rh', e)} onMouseLeave=${handleThLeave}>RH <span class="col-unit">%</span></th>`} ${visibleCols.rh && html`<th class=${`col-info-th ${groupStart('rh')} ${groupColor('rh')}`} scope="col" onClick=${(e) => handleThClick('rh', e)} onMouseEnter=${(e) => handleThEnter('rh', e)} onMouseLeave=${handleThLeave}>RH <span class="col-unit">%</span></th>`}
${visibleCols.dew && html`<th class=${`col-info-th ${groupStart('dew')} ${groupColor('dew')}`} scope="col" onClick=${(e) => handleThClick('dew', e)} onMouseEnter=${(e) => handleThEnter('dew', e)} onMouseLeave=${handleThLeave}>Dew <span class="col-unit">°C</span></th>`} ${visibleCols.dew && html`<th class=${`col-info-th ${groupStart('dew')} ${groupColor('dew')}`} scope="col" onClick=${(e) => handleThClick('dew', e)} onMouseEnter=${(e) => handleThEnter('dew', e)} onMouseLeave=${handleThLeave}>Dew <span class="col-unit">°C</span></th>`}
@@ -525,6 +535,8 @@ ${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html`<button c
const b = parseInt(h.slice(4,6),16); const b = parseInt(h.slice(4,6),16);
return `rgba(${r},${g},${b},${a})`; return `rgba(${r},${g},${b},${a})`;
}; };
const fmt = (v, dp = 1) => v == null ? '—' : (showDecimals ? v.toFixed(dp) : String(Math.round(v)));
const deg = showDegree ? '°' : '';
// Returns a background tint based on temperature value - all share the same // Returns a background tint based on temperature value - all share the same
// Thermal Stress Bands colors with 50% alpha for consistency. Uses exact legend ranges. // Thermal Stress Bands colors with 50% alpha for consistency. Uses exact legend ranges.
// Converts a hex colour - r,g,b components for rgba(). // Converts a hex colour - r,g,b components for rgba().
@@ -608,19 +620,19 @@ ${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html`<button c
</td> </td>
${visibleCols.utciP && html` ${visibleCols.utciP && html`
<td class=${groupStart('utciP')} style=${{ background: hexToRgba(adjCat.bg, 0.50), color: tempFontColorStrong(r.utciAdj), fontWeight: 700, fontSize: '13px' }}> <td class=${groupStart('utciP')} style=${{ background: hexToRgba(adjCat.bg, 0.50), color: tempFontColorStrong(r.utciAdj), fontWeight: 700, fontSize: '13px' }}>
${r.utciAdj.toFixed(1)}° ${fmt(r.utciAdj)}${deg}
</td>`} </td>`}
${visibleCols.vehicleT && html` ${visibleCols.vehicleT && html`
<td class=${groupStart('vehicleT')} style=${{ color: tempFontColorStrong(r.vehicleT), fontWeight: 'bold', background: tempBgStrong(r.vehicleT) }}> <td class=${groupStart('vehicleT')} style=${{ color: tempFontColorStrong(r.vehicleT), fontWeight: 'bold', background: tempBgStrong(r.vehicleT) }}>
${r.vehicleT != null ? r.vehicleT.toFixed(1) : '—'} ${fmt(r.vehicleT)}${deg}
</td>`} </td>`}
${indoorMode === 'on' && !indoorManaged && html` ${indoorMode === 'on' && !indoorManaged && html`
<td class=${groupStart('indoorT')} style=${{ color: tempFontColorStrong(r.indoorT), fontWeight: 'bold', background: tempBgStrong(r.indoorT) }}> <td class=${groupStart('indoorT')} style=${{ color: tempFontColorStrong(r.indoorT), fontWeight: 'bold', background: tempBgStrong(r.indoorT) }}>
${r.indoorT != null ? r.indoorT.toFixed(1) : '—'} ${fmt(r.indoorT)}${deg}
</td>`} </td>`}
${indoorMode === 'on' && indoorManaged && html` ${indoorMode === 'on' && indoorManaged && html`
<td class=${groupStart('managedT')} style=${{ color: tempFontColorStrong(r.managedT), fontWeight: 'bold', background: tempBgStrong(r.managedT) }}> <td class=${groupStart('managedT')} style=${{ color: tempFontColorStrong(r.managedT), fontWeight: 'bold', background: tempBgStrong(r.managedT) }}>
${r.managedT != null ? r.managedT.toFixed(1) : '—'} ${fmt(r.managedT)}${deg}
</td>`} </td>`}
${visibleCols.burn && html` ${visibleCols.burn && html`
<td class=${groupStart('burn')} style=${{ color: r.uv > 0 ? (burnMins < 30 ? '#c44a3a' : '#c8601a') : '#4a3218', background: burnBg(burnMins, r.uv) }}> <td class=${groupStart('burn')} style=${{ color: r.uv > 0 ? (burnMins < 30 ? '#c44a3a' : '#c8601a') : '#4a3218', background: burnBg(burnMins, r.uv) }}>
@@ -628,7 +640,7 @@ ${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html`<button c
</td>`} </td>`}
${visibleCols.utci && html` ${visibleCols.utci && html`
<td class=${groupStart('utci')} style=${{ background: hexToRgba(cat.bg, 0.35), color: tempFontColorStrong(r.utci), fontWeight: 600 }}> <td class=${groupStart('utci')} style=${{ background: hexToRgba(cat.bg, 0.35), color: tempFontColorStrong(r.utci), fontWeight: 600 }}>
${r.utci.toFixed(1)}° ${fmt(r.utci)}${deg}
</td>`} </td>`}
${visibleCols.delta && html` ${visibleCols.delta && html`
<td class=${groupStart('delta')} style=${{ <td class=${groupStart('delta')} style=${{
@@ -636,28 +648,28 @@ ${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html`<button c
fontWeight: 600, fontWeight: 600,
background: deltaBg(delta), background: deltaBg(delta),
}}> }}>
${delta > 0 ? '+' : ''}${delta.toFixed(1)} ${delta > 0 ? '+' : ''}${fmt(delta)}
</td>`} </td>`}
${visibleCols.tmrt && html`<td class=${groupStart('tmrt')} style=${{ background: tempBgStrong(r.Tmrt), color: tempFontColorStrong(r.Tmrt) }}>${r.Tmrt.toFixed(1)}</td>`} ${visibleCols.tmrt && html`<td class=${groupStart('tmrt')} style=${{ background: tempBgStrong(r.Tmrt), color: tempFontColorStrong(r.Tmrt) }}>${fmt(r.Tmrt)}${deg}</td>`}
${visibleCols.concreteT && html` ${visibleCols.concreteT && html`
<td class=${groupStart('concreteT')} style=${{ color: tempFontColorStrong(r.concreteT), fontWeight: 'bold', background: tempBgStrong(r.concreteT) }}> <td class=${groupStart('concreteT')} style=${{ color: tempFontColorStrong(r.concreteT), fontWeight: 'bold', background: tempBgStrong(r.concreteT) }}>
${r.concreteT != null ? r.concreteT.toFixed(1) : '—'} ${fmt(r.concreteT)}${deg}
</td>`} </td>`}
${visibleCols.soilT && html` ${visibleCols.soilT && html`
<td class=${groupStart('soilT')} style=${{ color: tempFontColorStrong(r.soilT0), background: tempBgStrong(r.soilT0) }}>${r.soilT0 != null ? r.soilT0.toFixed(1) : '—'}</td>`} <td class=${groupStart('soilT')} style=${{ color: tempFontColorStrong(r.soilT0), background: tempBgStrong(r.soilT0) }}>${fmt(r.soilT0)}${deg}</td>`}
${visibleCols.soilT6 && html` ${visibleCols.soilT6 && html`
<td class=${groupStart('soilT6')} style=${{ color: tempFontColorStrong(r.soilT6), background: tempBgStrong(r.soilT6) }}>${r.soilT6 != null ? r.soilT6.toFixed(1) : '—'}</td>`} <td class=${groupStart('soilT6')} style=${{ color: tempFontColorStrong(r.soilT6), background: tempBgStrong(r.soilT6) }}>${fmt(r.soilT6)}${deg}</td>`}
${visibleCols.soilM && html` ${visibleCols.soilM && html`
<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>`} <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) + '%' : '—'}</td>`}
${visibleCols.air && html`<td class=${groupStart('air')} style=${{ background: tempBg(r.Ta), color: tempFontColorStrong(r.Ta) }}>${r.Ta.toFixed(1)}</td>`} ${visibleCols.air && html`<td class=${groupStart('air')} style=${{ background: tempBg(r.Ta), color: tempFontColorStrong(r.Ta) }}>${fmt(r.Ta)}${deg}</td>`}
${visibleCols.rh && html`<td class=${groupStart('rh')} style=${{ background: scaleBg(r.RH, 30, 100, '70,145,200') }}>${Math.round(r.RH)}</td>`} ${visibleCols.rh && html`<td class=${groupStart('rh')} style=${{ background: scaleBg(r.RH, 30, 100, '70,145,200') }}>${Math.round(r.RH)}</td>`}
${visibleCols.dew && html`<td class=${groupStart('dew')} style=${{ background: tempBgStrong(r.dew), color: tempFontColorStrong(r.dew) }}>${r.dew != null ? r.dew.toFixed(1) : '—'}</td>`} ${visibleCols.dew && html`<td class=${groupStart('dew')} style=${{ background: tempBgStrong(r.dew), color: tempFontColorStrong(r.dew) }}>${fmt(r.dew)}${deg}</td>`}
${visibleCols.precip && html` ${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') }}> <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' }}> <span style=${{ display: 'inline-flex', alignItems: 'center', gap: '5px', verticalAlign: 'middle' }}>
<${PrecipIcon} precip=${r.precip} snow=${r.snow} size=${28} /> <${PrecipIcon} precip=${r.precip} snow=${r.snow} size=${28} />
<span style=${{ color: r.snow > 0 ? '#2a5fa8' : r.precip > 0 ? '#2a6a90' : '#7a5c30' }}> <span style=${{ color: r.snow > 0 ? '#2a5fa8' : r.precip > 0 ? '#2a6a90' : '#7a5c30' }}>
${r.snow > 0 ? r.snow.toFixed(1) + 'cm' : r.precip > 0 ? r.precip.toFixed(1) : '—'} ${r.snow > 0 ? fmt(r.snow) + 'cm' : r.precip > 0 ? fmt(r.precip) : '—'}
</span> </span>
</span> </span>
</td>`} </td>`}
@@ -682,7 +694,7 @@ ${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html`<button c
: 'transparent'; : 'transparent';
const color = v != null && v < 4 ? '#8a3a1a' : '#4a3218'; const color = v != null && v < 4 ? '#8a3a1a' : '#4a3218';
return html`<td class=${groupStart('vis')} style=${{ background: bg, color }}> return html`<td class=${groupStart('vis')} style=${{ background: bg, color }}>
${v != null ? (v < 10 ? v.toFixed(1) : Math.round(v)) : '—'} ${v != null ? fmt(v) : '—'}
</td>`; </td>`;
})()} })()}
${visibleCols.wind && html`<td class=${groupStart('wind')} style=${{ background: scaleBg((r.gust ?? r.va) * 2.237, 0, 40, '85,130,180') }}> ${visibleCols.wind && html`<td class=${groupStart('wind')} style=${{ background: scaleBg((r.gust ?? r.va) * 2.237, 0, 40, '85,130,180') }}>
@@ -741,13 +753,13 @@ ${isPro && (activeProfile === 'custom' || activeCols['utciP']) && html`<button c
})()} })()}
${visibleCols.uvA && html` ${visibleCols.uvA && html`
<td class=${groupStart('uvA')} style=${{ color: r.uvA > 0 ? '#c8922a' : '#4a3218', background: scaleBg(r.uvA, 0, 8, '220,155,40') }}> <td class=${groupStart('uvA')} style=${{ color: r.uvA > 0 ? '#c8922a' : '#4a3218', background: scaleBg(r.uvA, 0, 8, '220,155,40') }}>
${r.uvA > 0 ? r.uvA.toFixed(1) : '—'} ${r.uvA > 0 ? fmt(r.uvA) : '—'}
</td>`} </td>`}
${visibleCols.uvB && html` ${visibleCols.uvB && html`
<td class=${groupStart('uvB')} style=${{ color: r.uvB > 0 ? '#c44a3a' : '#4a3218', background: scaleBg(r.uvB, 0, 1.2, '210,70,50') }}> <td class=${groupStart('uvB')} style=${{ color: r.uvB > 0 ? '#c44a3a' : '#4a3218', background: scaleBg(r.uvB, 0, 1.2, '210,70,50') }}>
${r.uvB > 0 ? r.uvB.toFixed(2) : '—'} ${r.uvB > 0 ? fmt(r.uvB, 2) : '—'}
</td>`} </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.sun && html`<td class=${groupStart('sun')} style=${{ background: scaleBg(r.elev > 0 ? r.elev : null, 0, 70, '225,160,45') }}>${r.elev > 0 ? fmt(r.elev) + deg : '—'}</td>`}
${visibleCols.direct && html`<td class=${groupStart('direct')} style=${{ background: scaleBg(r.dir, 0, 850, '230,155,35') }}>${Math.round(r.dir)}</td>`} ${visibleCols.direct && html`<td class=${groupStart('direct')} style=${{ background: scaleBg(r.dir, 0, 850, '230,155,35') }}>${Math.round(r.dir)}</td>`}
${visibleCols.diffuse && html`<td class=${groupStart('diffuse')} style=${{ background: scaleBg(r.dif, 0, 450, '230,190,70') }}>${Math.round(r.dif)}</td>`} ${visibleCols.diffuse && html`<td class=${groupStart('diffuse')} style=${{ background: scaleBg(r.dif, 0, 450, '230,190,70') }}>${Math.round(r.dif)}</td>`}
</tr>`; </tr>`;
+1 -1
View File
@@ -179,7 +179,7 @@ export const COL_DESCRIPTIONS = {
precipProb: { title: 'Rain Probability', desc: 'Probability of precipitation for this hour as a percentage (0-100%). Derived from Open-Meteo ensemble model runs. A high value means rain is likely even if the expected rate is low - useful for spotting showers that the deterministic forecast may miss.' }, precipProb: { title: 'Rain Probability', desc: 'Probability of precipitation for this hour as a percentage (0-100%). Derived from Open-Meteo ensemble model runs. A high value means rain is likely even if the expected rate is low - useful for spotting showers that the deterministic forecast may miss.' },
soilT: { title: 'Soil Temperature', desc: 'Temperature of the soil at the surface (0 cm depth). Useful for planting decisions — most seeds germinate above 710 °C.' }, soilT: { title: 'Soil Temperature', desc: 'Temperature of the soil at the surface (0 cm depth). Useful for planting decisions — most seeds germinate above 710 °C.' },
soilT6: { title: 'Soil Temp 6 cm', desc: 'Temperature of the soil at 6 cm depth, the root zone for many crops and seedlings. Lags behind surface temperature by several hours.' }, soilT6: { title: 'Soil Temp 6 cm', desc: 'Temperature of the soil at 6 cm depth, the root zone for many crops and seedlings. Lags behind surface temperature by several hours.' },
soilM: { title: 'Soil Moisture', desc: 'Volumetric water content of the top 1 cm of soil (m³/m³). Values above 0.4 suggest saturated ground; below 0.2 indicates dry conditions.' }, soilM: { title: 'Soil Moisture', desc: 'Water saturation of the top 1 cm of soil as a percentage. Above 40% suggests saturated ground; below 20% indicates dry conditions.' },
concreteT: { title: 'Concrete Surface', desc: 'Estimated temperature of sun-exposed urban concrete or paving. Concrete absorbs more solar energy than grass and cannot cool itself through evaporation — surface temps can run 1525 °C above air temperature on sunny days.' }, concreteT: { title: 'Concrete Surface', desc: 'Estimated temperature of sun-exposed urban concrete or paving. Concrete absorbs more solar energy than grass and cannot cool itself through evaporation — surface temps can run 1525 °C above air temperature on sunny days.' },
vehicleT: { title: 'Vehicle Interior', desc: 'Estimated ambient cabin temperature inside a parked vehicle. Cars heat rapidly through thin body panels and glass; motorhomes and caravans are modelled as insulated occupied living spaces with retained warmth, lower glass gain, and slower heat response. Dangerous for children and pets above 35 °C; potentially fatal above 45 °C.' }, vehicleT: { title: 'Vehicle Interior', desc: 'Estimated ambient cabin temperature inside a parked vehicle. Cars heat rapidly through thin body panels and glass; motorhomes and caravans are modelled as insulated occupied living spaces with retained warmth, lower glass gain, and slower heat response. Dangerous for children and pets above 35 °C; potentially fatal above 45 °C.' },
indoorT: { title: 'Indoors', desc: 'Estimated ambient temperature inside a selected building type with windows closed and no air conditioning. Accounts for retained warmth, window solar gain, internal gains, and thermal lag without repeatedly accumulating solar heat hour after hour.' }, indoorT: { title: 'Indoors', desc: 'Estimated ambient temperature inside a selected building type with windows closed and no air conditioning. Accounts for retained warmth, window solar gain, internal gains, and thermal lag without repeatedly accumulating solar heat hour after hour.' },
+24
View File
@@ -208,6 +208,28 @@ export function useAppState() {
setPollenType(v); setPollenType(v);
}; };
const [showDecimals, setShowDecimals] = useState(() => {
try { const s = localStorage.getItem('sunscope_show_decimals'); return s === null ? false : s !== '0'; } catch (e) { return false; }
});
const toggleShowDecimals = () => setShowDecimals(v => {
const next = !v;
try { localStorage.setItem('sunscope_show_decimals', next ? '1' : '0'); } catch (e) { /* ignore */ }
return next;
});
const [showDegree, setShowDegree] = useState(() => {
try {
const s = localStorage.getItem('sunscope_show_degree');
if (s !== null) return s !== '0';
return typeof window !== 'undefined' ? window.innerWidth >= 640 : true;
} catch (e) { return true; }
});
const toggleShowDegree = () => setShowDegree(v => {
const next = !v;
try { localStorage.setItem('sunscope_show_degree', next ? '1' : '0'); } catch (e) { /* ignore */ }
return next;
});
const searchTimeout = useRef(null); const searchTimeout = useRef(null);
// Derived selectors used by profile controls // Derived selectors used by profile controls
@@ -429,6 +451,8 @@ export function useAppState() {
indoorManaged, setIndoorManaged, indoorManaged, setIndoorManaged,
indoorMode, setIndoorMode, indoorMode, setIndoorMode,
pollenType, setPollenTypeAndSave, pollenType, setPollenTypeAndSave,
showDecimals, toggleShowDecimals,
showDegree, toggleShowDegree,
// table refs // table refs
headStickyRef, headTrackRef, headTableRef, headStickyRef, headTrackRef, headTableRef,
bodyScrollRef, bodyTableRef, tableWrapRef, bodyScrollRef, bodyTableRef, tableWrapRef,