Author SHA1 Message Date
theoandClaude Opus 5 5d61b6ec90 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 <noreply@anthropic.com>
2026-09-13 11:33:14 +02:00
theoandClaude Opus 5 e2db301aef Merge feature/luchtdruk-assen: x- en y-as bij de luchtdrukgrafiek
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 11:30:15 +02:00
theoandClaude Opus 5 c0715b7cc9 feat: assen bij de luchtdrukgrafiek
De lijn had geen schaal, dus je kon niet zien hoeveel hPa de uitslag was
of wanneer een dip viel. Nu links de hoogste en laagste waarde van het
venster in hele hPa, uitgelijnd op boven- en onderkant van de lijn, en
eronder vijf tijdlabels (0/25/50/75/100% van het venster) met "nu"
rechts, ondersteund door drie lichte verticale hulplijnen. Posities
komen uit de pure functie axisMarks(), met tests.

De losse tekst "1009 – 1015 hPa in 24 u" in de kopregel vervalt: de
y-as zegt hetzelfde en de x-as maakt het venster duidelijk. De extra
labelrij past door de marge onder de grote temperatuur 8px te krappen;
er blijft 5px speling boven en onder de hero. Servicewerker naar v8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 11:30:11 +02:00
theoandClaude Opus 5 c95d198341 Merge feature/buienradar: regenradar-overlay bij een tik op het regenblok
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 11:13:32 +02:00
7 changed files with 112 additions and 14 deletions
+8
View File
@@ -251,6 +251,14 @@ De iconen `sun`, `partly`, `rain` en `showers` staan in de mockup. Maak `cloud`,
- De temperatuur staat groot, met 1 decimaal ("16,4°"). Het label heeft een trend-aanduiding: `up` → "↗ stijgend", `down` → "↘ dalend", `stable` → "→ stabiel".
- Daaronder de gradiëntbalk van `--cold` naar `--warm`. De marker staat op `(temp − min) / (max − min)`, begrensd op 0–100%. Bij `max == min` staat hij op 50%.
- 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 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
+17 -2
View File
@@ -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,
windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, hourTicks,
} from './format.js';
import { icon, windArrow } from './icons.js';
@@ -104,10 +104,25 @@ function renderRain(rain, now) {
function renderPressure(p) {
setText('press', fHpa(p.current));
setText('press-trend', trendLabel(p.trend));
const values = p.history.map((h) => h.hpa);
const range = seriesRange(values);
setText('press-range', range ? `${Math.round(range.min)} – ${Math.round(range.max)} hPa in 24 u` : '');
setText('press-max', range ? String(Math.round(range.max)) : '–');
setText('press-min', range ? String(Math.round(range.min)) : '–');
$('press-path').setAttribute('d', sparklinePath(values, 560, 56));
// 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 `<span style="${style}">${formatTime(t.at, state.tz)}</span>`;
})
.concat(last ? ['<span style="right:0">nu</span>'] : [])
.join('');
}
function renderForecast(days) {
+27
View File
@@ -219,3 +219,30 @@ export function pinRoom(rooms, pinnedId) {
copy.unshift(pinned);
return copy;
}
/**
* 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 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;
}
+13 -5
View File
@@ -36,11 +36,19 @@
</div>
</div>
<div class="a-pressure">
<p class="a-plabel">Luchtdruk <b class="num" id="press">–</b><span class="a-trend" id="press-trend"></span><span class="a-prange num" id="press-range"></span></p>
<svg class="a-spark" viewBox="0 0 560 56" preserveAspectRatio="none" aria-hidden="true">
<line class="a-spark-base" x1="0" y1="55.5" x2="560" y2="55.5"/>
<path class="a-spark-line" id="press-path" d=""/>
</svg>
<p class="a-plabel">Luchtdruk <b class="num" id="press">–</b><span class="a-trend" id="press-trend"></span></p>
<div class="a-pgrid">
<div class="a-py num"><span id="press-max">–</span><span id="press-min">–</span></div>
<svg class="a-spark" viewBox="0 0 560 56" preserveAspectRatio="none" aria-hidden="true">
<line class="a-spark-grid" x1="140" y1="0" x2="140" y2="56"/>
<line class="a-spark-grid" x1="280" y1="0" x2="280" y2="56"/>
<line class="a-spark-grid" x1="420" y1="0" x2="420" y2="56"/>
<line class="a-spark-base" x1="0" y1="55.5" x2="560" y2="55.5"/>
<path class="a-spark-line" id="press-path" d=""/>
</svg>
<div></div>
<div class="a-px num" id="press-hours"></div>
</div>
</div>
</section>
+9 -5
View File
@@ -80,7 +80,7 @@ html, body {
.a-hero { align-self: center; }
.a-label { font-size: 22px; color: var(--muted); font-weight: 500; display: flex; gap: 14px; align-items: baseline; }
.a-trend { font-size: 17px; font-weight: 400; }
.a-temp { font-size: 200px; font-weight: 200; letter-spacing: -.045em; line-height: .95; margin: 4px 0 18px -8px; }
.a-temp { font-size: 200px; font-weight: 200; letter-spacing: -.045em; line-height: .95; margin: 4px 0 10px -8px; }
.a-hum { font-size: 17px; font-weight: 400; }
.a-hum:not(:empty)::before { content: "·"; margin-right: 14px; }
.a-range { width: 560px; }
@@ -89,13 +89,17 @@ html, body {
.a-ends { display: flex; justify-content: space-between; margin-top: 14px; font-size: 17px; color: var(--muted); }
.a-ends b { font-size: 24px; font-weight: 600; margin-right: 6px; }
/* Luchtdruk, 24 uur */
.a-pressure { width: 560px; margin-top: 18px; }
/* Luchtdruk, 24 uur. Grafiek met een y-as (hoogste/laagste waarde) en tijdlabels op de x-as. */
.a-pressure { width: 560px; margin-top: 14px; }
.a-plabel { font-size: 17px; color: var(--muted); display: flex; gap: 12px; align-items: baseline; margin-bottom: 6px; }
.a-plabel b { font-size: 20px; font-weight: 600; color: var(--ink); }
.a-prange { margin-left: auto; font-size: 14px; }
.a-spark { display: block; width: 560px; height: 56px; overflow: visible; }
.a-pgrid { display: grid; grid-template-columns: 44px 1fr; column-gap: 8px; row-gap: 3px; }
.a-py { display: flex; flex-direction: column; justify-content: space-between; text-align: right; font-size: 13px; color: var(--muted); line-height: 1; padding: 1px 0; }
.a-px { position: relative; height: 17px; font-size: 13px; color: var(--muted); }
.a-px span { position: absolute; top: 0; white-space: nowrap; }
.a-spark { display: block; width: 100%; height: 56px; overflow: visible; }
.a-spark-base { stroke: var(--line); stroke-width: 1; }
.a-spark-grid { stroke: var(--line); stroke-width: 1; vector-effect: non-scaling-stroke; opacity: .6; }
.a-spark-line { fill: none; stroke: var(--rain); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
/* Binnen */
+1 -1
View File
@@ -1,6 +1,6 @@
/* Service worker: app-shell cache-first, API network-first met timeout en fallback op de laatste response. */
const VERSION = 'weerscherm-v7';
const VERSION = 'weerscherm-v9';
const SHELL = [
'./',
'./index.html',
+37 -1
View File
@@ -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,
windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, hourTicks,
} from '../../public/format.js';
test('co2Level uses the Netatmo thresholds', () => {
@@ -208,3 +208,39 @@ test('pinRoom does not mutate the input array', () => {
pinRoom(rooms, 'c');
assert.deepEqual(rooms, original);
});
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('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('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), []);
});