diff --git a/assets/css/table.css b/assets/css/table.css index 9baef47..04c7a91 100644 --- a/assets/css/table.css +++ b/assets/css/table.css @@ -1939,6 +1939,10 @@ width: max-content; min-width: 100%; } +/* Half-hour tracks across the whole day, plus a blank inset spacer at each + end (see fscPlot in app.js). Each card spans 2*interval tracks positioned + so it is centred on the hour it is labelled with; the outer cards overhang + into the spacers instead of off the edge. Columns are set inline. */ .forecast-simple-cards { display: grid; padding: 12px 0 0; diff --git a/assets/js/app.js b/assets/js/app.js index 490b670..bfae2da 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -573,6 +573,61 @@ export function UTCIForecast() { alert: false, })); + // ─── 3b. QUICK-VIEW PLOT GEOMETRY ──────────────────────────────────── + // Shared by the simple-view cards and the temperature curve beneath them + // so the two can never drift apart. + // + // The curve is drawn from the full-resolution hourly rows (all 24 hours, + // never resampled), but each card covers a bucket of `tableInterval` + // hours and is LABELLED with that bucket's LANDING hour — a 4h card + // reading "8pm" spans 8–11pm and prints the worst felt temp in that span, + // which on a cooling evening is 8pm's own value. So a card must sit over + // its landing hour, not over the bucket's temporal middle (which is where + // an evenly-divided row of cards puts it: at 4h the "8pm / 28°" card + // landed above 9:30pm, past sunset, and pointed into the cold green tail). + // + // Laying the row out in hour columns fixes that, but a card is `bucket` + // hours wide while its landing hour sits only half an hour in from the + // bucket's start, so the first card would hang off the left edge. Hence + // the inset spacers: a blank half-bucket of track at each end for the + // outer cards to overhang into. + // + // The grid is measured in HALF-hour tracks, because a card centred on its + // landing hour starts on a half-hour boundary. All tracks are identical, + // and each card SPANS 2*bucket of them (rather than sitting in one track + // at width:400%) — spanning divides a card's intrinsic width across the + // tracks it covers, so the grid's max-content width stays close to what + // it was and mobile doesn't gain a load of extra horizontal scroll. + // + // tracks = [ pad ][ hour 0 ][ hour 1 ] … [ hour H-1 ][ pad ] + // pad = bucket half-hours (>= the (bucket-1)/2 h overhang, + breathing room) + // hour j -> centre at (bucket + 2j + 1) / total + // card -> spans 2*bucket tracks, starting one half-hour after 2j + // => centre = 2j + 1 + bucket == hour j's centre ✓ + const fscPlot = (() => { + if (!tableRows.length) return null; + const cards = tableRows.length; + const hourly = visible.length > cards ? visible : tableRows; + const hours = hourly.length; + const bucket = Math.max(1, tableInterval || 1); + const total = 2 * hours + 2 * bucket; // half-hour tracks + // Landing-hour index of each card, accumulated so partial buckets at a + // day boundary stay correct rather than assuming i * bucket. + const landing = []; + let j = 0; + for (const r of tableRows) { landing.push(j); j += r.isoHours ? r.isoHours.length : 1; } + // A card spans 2*bucket tracks, so this keeps its 55px minimum. + const unit = 55 / (2 * bucket); + return { + hourly, hours, bucket, landing, + cols: `repeat(${total}, minmax(${unit.toFixed(2)}px, 1fr))`, + // Fraction of the track width at which hour j's data point sits. + hourAt: j2 => (bucket + 2 * j2 + 1) / total, + // grid-column for the card whose landing hour is j (lines are 1-based). + cardCol: j2 => `${2 * j2 + 2} / span ${2 * bucket}`, + }; + })(); + // ─── 4. JSX RETURN ─────────────────────────────────────────────────── // Everything below is the actual page markup, written as one big HTM // template. Search tips: @@ -1092,8 +1147,8 @@ export function UTCIForecast() {