3.0 Beta 1

New simple mode display with graph. First render and scaling test.
This commit is contained in:
fraxle
2026-06-12 21:22:18 +01:00
parent f6e6efe747
commit 445fba033d
5 changed files with 341 additions and 8 deletions
+103 -1
View File
@@ -100,6 +100,7 @@ export function UTCIForecast() {
restoreOpen, openRestore, closeRestore,
showUnits, toggleShowUnits,
tableInterval, setTableInterval,
forecastView, setForecastView,
activityOptions, placeOptions, workOptions,
activityValue, activityLabel, placeValue, placeLabel, workValue, workLabel,
skinType, setSkinType,
@@ -658,8 +659,15 @@ export function UTCIForecast() {
<div class="table-with-rail">
<div class="table-main">
<div class=${'table-main' + (forecastView === 'simple' ? ' table-main--simple' : '')}>
<div class="col-toggles">
<span class="forecast-view-toggle">
<button type="button" class=${'fvt-btn' + (forecastView === 'simple' ? ' on' : '')} onClick=${() => setForecastView('simple')}>Simple</button>
<button type="button" class=${'fvt-btn' + (forecastView === 'table' ? ' on' : '')} onClick=${() => setForecastView('table')}>Table</button>
</span>
<span class="fvt-interval">
${[1, 2, 3, 4].map(n => html`<button key=${n} type="button" class=${'hour-interval-btn' + (tableInterval === n ? ' on' : '')} title=${n === 1 ? 'Every hour' : `Every ${n} hours`} onClick=${() => setTableInterval(n)}>${n}h</button>`)}
</span>
<span class="col-toggles-label">Columns:</span>
<button class=${'col-toggles-edit-btn' + (colTogglesOpen ? ' col-toggles-edit-btn--open' : '')} onClick=${() => setColTogglesOpen(v => !v)}>
<span class="col-toggles-edit-btn-label">${colTogglesOpen ? 'Done' : 'Edit columns'}</span>
@@ -831,6 +839,100 @@ export function UTCIForecast() {
</div>
</div>
<div class="forecast-simple-wrap">
<div class="forecast-simple-scroll">
<div class="forecast-simple-inner">
<div class="forecast-simple-cards" style=${{ gridTemplateColumns: `repeat(${tableRows.length}, minmax(55px, 1fr))` }}>
${tableRows.map(r => {
const cat = utciCategory(r.utciAdj);
const h24s = parseInt(r.iso.slice(11, 13), 10);
const localHHMMs = h24s === 0 ? '12am' : h24s < 12 ? `${h24s}am` : h24s === 12 ? '12pm' : `${h24s - 12}pm`;
const isNow = r.isoHours ? r.isoHours.includes(nowLocalISO) : r.iso.slice(0, 13) === nowLocalISO;
const domIcon = (() => {
if (r.precipProb > 20 && (r.precip > 0 || r.snow > 0))
return html`<${PrecipIcon} precip=${r.precip} snow=${r.snow} size=${42} />`;
if (r.cloudCat && r.cloudCat !== 'clear')
return html`<${CloudIcon} category=${r.cloudCat} elev=${r.elev} dt=${r.dt} size=${42} filled=${true} />`;
return null;
})();
const windMph = Math.round((r.gust ?? r.va) * 2.237);
return html`
<div key=${r.iso} class=${'fsc-card' + (isNow ? ' fsc-card--now' : '')}>
<div class="fsc-time">${localHHMMs}</div>
<div class="fsc-scope-wrap">
<${SkyScope} elev=${r.elev} dt=${r.dt} glob=${r.glob} size=${42} />
${domIcon && html`<div class="fsc-wx-overlay">${domIcon}</div>`}
</div>
<div class="fsc-meta">
<span class="fsc-meta-row fsc-meta-rain">${r.precipProb}%</span>
<span class="fsc-meta-row fsc-meta-wind">${windMph}mph</span>
<span class="fsc-meta-row fsc-meta-dir">${r.compass ? r.compass.label : '—'}</span>
</div>
<div class="fsc-temp">${Math.round(r.utciAdj)}°</div>
<div class="fsc-feel" style=${{background: cat.bg, color: cat.fg}}>${cat.label}</div>
</div>
`;
})}
</div>
${(() => {
if (!tableRows.length) return null;
const fscTemps = tableRows.map(r => r.utciAdj);
const fscNowFlags = tableRows.map(r => r.isoHours ? r.isoHours.includes(nowLocalISO) : r.iso.slice(0, 13) === nowLocalISO);
const fscMin = Math.min(...fscTemps) - 2;
const fscMax = Math.max(...fscTemps) + 2;
const fscN = fscTemps.length;
const fscSvgW = 1000, fscSvgH = 80;
const fscCardW = fscSvgW / fscN;
// Card-centre points — used for connector lines and the stroke curve
const fscPts = fscTemps.map((t, i) => ({
x: (i + 0.5) * fscCardW,
y: fscSvgH - ((t - fscMin) / (fscMax - fscMin)) * 44 - 6,
}));
// Extended points (edge-anchored) used only for the gradient fill
const fscFillPts = [{ x: 0, y: fscPts[0].y }, ...fscPts, { x: fscSvgW, y: fscPts[fscN - 1].y }];
// Stroke path through card centres
let fscLine = `M ${fscPts[0].x},${fscPts[0].y}`;
for (let i = 1; i < fscPts.length; i++) {
const p0 = fscPts[i - 1], p1 = fscPts[i];
const cpx = (p0.x + p1.x) / 2;
fscLine += ` C ${cpx},${p0.y} ${cpx},${p1.y} ${p1.x},${p1.y}`;
}
// Fill path through edge-anchored points
let fscFillLine = `M ${fscFillPts[0].x},${fscFillPts[0].y}`;
for (let i = 1; i < fscFillPts.length; i++) {
const p0 = fscFillPts[i - 1], p1 = fscFillPts[i];
const cpx = (p0.x + p1.x) / 2;
fscFillLine += ` C ${cpx},${p0.y} ${cpx},${p1.y} ${p1.x},${p1.y}`;
}
const fscFill = fscFillLine + ` L ${fscSvgW},${fscSvgH} L 0,${fscSvgH} Z`;
return html`
<svg class="fsc-temp-curve" viewBox="0 0 ${fscSvgW} ${fscSvgH}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="fsc-grad" x1="0" y1="0" x2="1" y2="0" gradientUnits="objectBoundingBox">
${fscTemps.map((t, i) => {
const fc = utciCategory(t);
return html`<stop key=${i} offset=${`${((i + 0.5) / fscN * 100).toFixed(1)}%`} stop-color=${fc.bg} />`;
})}
</linearGradient>
</defs>
${fscPts.map((pt, i) => {
const isNow = fscNowFlags[i];
return html`
<line key=${'c'+i} x1=${pt.x} y1="0" x2=${pt.x} y2=${pt.y}
stroke=${isNow ? '#c8922a' : '#b09870'}
stroke-width="1"
opacity=${isNow ? '1' : '0.55'}
/>
`;
})}
<path d=${fscFill} fill="url(#fsc-grad)" opacity="0.45" />
</svg>
`;
})()}
</div>
</div>
</div>
<div class=${`utci-table-wrap${tableCanScrollLeft ? ' scroll-fade-left' : ''}${tableCanScrollRight ? ' scroll-fade-right' : ''}${showUnits ? '' : ' no-units'}`} ref=${tableWrapRef}>
<span class="utci-scroll-chevron left" aria-hidden="true"></span>
<span class="utci-scroll-chevron right" aria-hidden="true"></span>