From 5d61b6ec90d7bc99b1d609c0a28b76f8dbdd2238 Mon Sep 17 00:00:00 2001 From: Theo Weijmer Date: Sun, 13 Sep 2026 11:33:14 +0200 Subject: [PATCH] feat: tijdlabels op hele uren in plaats van de ruwe meettijden De labels toonden de werkelijke meettijdstippen van Netatmo (11:41, 17:41, 23:41), wat onrustig leest. Nu markeringen op hele uren die op de klok zijn uitgelijnd: elke 6 uur vanaf middernacht, dus 00:00, 06:00, 12:00, 18:00, elk op zijn echte tijdspositie in het venster. "nu" blijft op de rechterrand; een markering die daar te dicht bij komt valt weg. axisMarks() is vervangen door hourTicks(), die uur voor uur door het venster loopt en telkens de lokale klok raadpleegt. Daardoor blijven de labels ook op de zomertijdwissel hele lokale uren, waar optellen in vaste stappen er een uur naast zou zitten; daar is een test voor. Een label vlak op de linkerrand wordt niet gecentreerd, zodat het binnen de grafiek blijft. Servicewerker naar v9. Co-Authored-By: Claude Opus 5 --- docs/weerscherm-spec.md | 9 +++++--- public/app.js | 22 ++++++++++-------- public/format.js | 29 ++++++++++++++++++----- public/sw.js | 2 +- tests/js/format.test.js | 51 +++++++++++++++++++++++++---------------- 5 files changed, 74 insertions(+), 39 deletions(-) diff --git a/docs/weerscherm-spec.md b/docs/weerscherm-spec.md index d781375..6ee268c 100644 --- a/docs/weerscherm-spec.md +++ b/docs/weerscherm-spec.md @@ -253,9 +253,12 @@ De iconen `sun`, `partly`, `rain` en `showers` staan in de mockup. Maak `cloud`, - Onder de balk: "9,2° laagst om 06:10" en "19,8° hoogst om 13:05". - Daaronder de luchtdruk: "Luchtdruk 1014 hPa ↗ stijgend" met een grafiek van de afgelopen 24 uur. _Assen toegevoegd 2026-09-13: links de hoogste en laagste waarde van het venster in hele hPa, onder de - lijn vijf tijdlabels op 0/25/50/75/100% met "nu" rechts, plus drie lichte verticale hulplijnen. De losse - tekst "1009 – 1015 hPa in 24 u" in de kopregel is daarmee vervallen. Zie `axisMarks()` in - `public/format.js`._ + lijn tijdlabels op hele uren die op de klok zijn uitgelijnd (elke 6 uur vanaf middernacht, dus 00:00, + 06:00, 12:00, 18:00) op hun echte tijdspositie, met "nu" op de rechterrand, plus drie lichte verticale + hulplijnen. Een markering te dicht bij het einde valt weg omdat "nu" daar staat. De losse tekst + "1009 – 1015 hPa in 24 u" in de kopregel is daarmee vervallen. Zie `hourTicks()` in `public/format.js`; + die loopt uur voor uur door het venster en raadpleegt telkens de lokale klok, zodat de zomertijdwissel + vanzelf goed gaat._ - **Binnen (rechts):** 3 rijen met naam, temperatuur (1 decimaal) en "↓ min° ↑ max°". Een onbereikbare module toont "–" in `--muted`. _Toegevoegd 2026-09-13: een module is tikbaar. Een tik zet die module bovenaan de lijst (de overige twee behouden hun onderlinge volgorde) en vergroot alleen het temperatuurgetal van 50 naar 92px. Nogmaals tikken diff --git a/public/app.js b/public/app.js index 41c6c84..25e417a 100644 --- a/public/app.js +++ b/public/app.js @@ -2,7 +2,7 @@ import { f1, fInt, fmm, markerPos, barHeights, wmoIcon, trendLabel, formatDate, formatTime, dayAbbr, monthName, isOlderThan, msUntil, co2Level, fPct, fHpa, seriesRange, sparklinePath, - windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, axisMarks, + windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, hourTicks, } from './format.js'; import { icon, windArrow } from './icons.js'; @@ -111,14 +111,18 @@ function renderPressure(p) { setText('press-min', range ? String(Math.round(range.min)) : '–'); $('press-path').setAttribute('d', sparklinePath(values, 560, 56)); - // Tijdlabels op dezelfde posities als de punten waar ze bij horen; de laatste is "nu". - const marks = axisMarks(p.history.length); - $('press-hours').innerHTML = marks.map((m, i) => { - const last = i === marks.length - 1; - const label = last ? 'nu' : formatTime(p.history[m.index].at, state.tz); - const style = i === 0 ? 'left:0' : last ? 'right:0' : `left:${m.percent}%;transform:translateX(-50%)`; - return `${label}`; - }).join(''); + // Hele uren op hun echte tijdspositie, plus "nu" op de rechterrand. + const first = p.history[0]; + const last = p.history[p.history.length - 1]; + const ticks = first && last ? hourTicks(first.at, last.at, state.tz) : []; + $('press-hours').innerHTML = ticks + .map((t) => { + // Een markering vlak op de linkerrand niet centreren, anders steekt het label buiten de grafiek. + const style = t.percent < 3 ? 'left:0' : `left:${t.percent.toFixed(1)}%;transform:translateX(-50%)`; + return `${formatTime(t.at, state.tz)}`; + }) + .concat(last ? ['nu'] : []) + .join(''); } function renderForecast(days) { diff --git a/public/format.js b/public/format.js index be1673a..c0b0bf8 100644 --- a/public/format.js +++ b/public/format.js @@ -221,11 +221,28 @@ export function pinRoom(rooms, pinnedId) { } /** - * Posities voor labels langs een as. Geeft per fractie de bijbehorende index in een reeks van - * `length` punten, plus het percentage waarop dat label hoort te staan. Onder de 2 punten valt er - * niets zinnigs te labelen. + * Markeringen op hele uren binnen een tijdvenster, uitgelijnd op de klok: elke `stepHours` uur + * gerekend vanaf middernacht in de gegeven tijdzone, dus 00:00, 06:00, 12:00, 18:00 bij een stap van 6. + * Per markering het tijdstip en de positie in procenten van het venster. + * + * Er wordt uur voor uur door het venster gelopen en telkens de lokale klok geraadpleegd, zodat de + * zomertijdwissel vanzelf goed gaat: een dag van 23 of 25 uur levert gewoon de juiste hele uren op. + * Markeringen binnen `minGapPercent` van het einde vallen weg; daar staat het label "nu". */ -export function axisMarks(length, fractions = [0, 0.25, 0.5, 0.75, 1]) { - if (length < 2) return []; - return fractions.map((f) => ({ index: Math.round(f * (length - 1)), percent: f * 100 })); +export function hourTicks(startIso, endIso, tz, stepHours = 6, minGapPercent = 9) { + const t0 = new Date(startIso).getTime(); + const t1 = new Date(endIso).getTime(); + if (Number.isNaN(t0) || Number.isNaN(t1) || t1 <= t0) return []; + + const hourMs = 3600000; + const span = t1 - t0; + const ticks = []; + for (let t = Math.ceil(t0 / hourMs) * hourMs; t <= t1; t += hourMs) { + const at = new Date(t).toISOString(); + if (Number(formatHour(at, tz)) % stepHours !== 0) continue; + const percent = ((t - t0) / span) * 100; + if (percent > 100 - minGapPercent) break; + ticks.push({ at, percent }); + } + return ticks; } diff --git a/public/sw.js b/public/sw.js index 9c8b662..d7f1f75 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,6 +1,6 @@ /* Service worker: app-shell cache-first, API network-first met timeout en fallback op de laatste response. */ -const VERSION = 'weerscherm-v8'; +const VERSION = 'weerscherm-v9'; const SHELL = [ './', './index.html', diff --git a/tests/js/format.test.js b/tests/js/format.test.js index c2b6f7a..5604b44 100644 --- a/tests/js/format.test.js +++ b/tests/js/format.test.js @@ -121,7 +121,7 @@ test('msUntil computes the wait until the next local time', () => { import { co2Level, fPct, fHpa, seriesRange, sparklinePath, sparklinePoints, - windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, axisMarks, + windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, hourTicks, } from '../../public/format.js'; test('co2Level uses the Netatmo thresholds', () => { @@ -209,27 +209,38 @@ test('pinRoom does not mutate the input array', () => { assert.deepEqual(rooms, original); }); -test('axisMarks maps fractions to indices and percentages', () => { - // 49 punten = 24 uur per half uur - assert.deepEqual(axisMarks(49), [ - { index: 0, percent: 0 }, - { index: 12, percent: 25 }, - { index: 24, percent: 50 }, - { index: 36, percent: 75 }, - { index: 48, percent: 100 }, - ]); + +test('hourTicks puts marks on clock-aligned whole hours inside the window', () => { + const ticks = hourTicks('2026-09-12T11:41:00+02:00', '2026-09-13T11:41:00+02:00', TZ); + + assert.deepEqual(ticks.map((t) => formatTime(t.at, TZ)), ['12:00', '18:00', '00:00', '06:00']); + assert.deepEqual(ticks.map((t) => Number(t.percent.toFixed(2))), [1.32, 26.32, 51.32, 76.32]); }); -test('axisMarks honours custom fractions and rounds to the nearest point', () => { - assert.deepEqual(axisMarks(10, [0, 0.5, 1]), [ - { index: 0, percent: 0 }, - { index: 5, percent: 50 }, - { index: 9, percent: 100 }, - ]); - assert.deepEqual(axisMarks(4, [0.33]), [{ index: 1, percent: 33 }]); +test('hourTicks drops a mark that would collide with the "nu" label', () => { + // Venster eindigt om 12:05, dus de markering van 12:00 zit op 99,7% en valt weg. + const ticks = hourTicks('2026-09-12T12:05:00+02:00', '2026-09-13T12:05:00+02:00', TZ); + + assert.deepEqual(ticks.map((t) => formatTime(t.at, TZ)), ['18:00', '00:00', '06:00']); }); -test('axisMarks gives nothing to label below two points', () => { - assert.deepEqual(axisMarks(1), []); - assert.deepEqual(axisMarks(0), []); +test('hourTicks stays on local whole hours across the daylight saving change', () => { + // 25 oktober 2026: de klok gaat om 03:00 terug naar 02:00, dus dat venster duurt 25 uur. + // Zonder de lokale klok te raadplegen zou 00:00 hier op 01:00 uitkomen. + const ticks = hourTicks('2026-10-24T12:00:00+02:00', '2026-10-25T12:00:00+01:00', TZ); + + assert.deepEqual(ticks.map((t) => formatTime(t.at, TZ)), ['12:00', '18:00', '00:00', '06:00']); + assert.equal(ticks[0].percent, 0, 'een venster dat precies op een heel uur begint krijgt daar een markering'); +}); + +test('hourTicks honours a different step', () => { + const ticks = hourTicks('2026-09-13T09:30:00+02:00', '2026-09-13T21:30:00+02:00', TZ, 3); + + assert.deepEqual(ticks.map((t) => formatTime(t.at, TZ)), ['12:00', '15:00', '18:00']); +}); + +test('hourTicks gives nothing for an empty or invalid window', () => { + assert.deepEqual(hourTicks('2026-09-13T10:00:00+02:00', '2026-09-13T10:00:00+02:00', TZ), []); + assert.deepEqual(hourTicks('kapot', '2026-09-13T10:00:00+02:00', TZ), []); + assert.deepEqual(hourTicks(undefined, undefined, TZ), []); });