Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5058bb850d | ||
|
|
5d61b6ec90 | ||
|
|
e2db301aef | ||
|
|
c0715b7cc9 | ||
|
|
c95d198341 |
@@ -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".
|
- 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%.
|
- 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".
|
- 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`.
|
- **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
|
_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
|
behouden hun onderlinge volgorde) en vergroot alleen het temperatuurgetal van 50 naar 92px. Nogmaals tikken
|
||||||
|
|||||||
+17
-2
@@ -2,7 +2,7 @@ import {
|
|||||||
f1, fInt, fmm, markerPos, barHeights, wmoIcon, trendLabel,
|
f1, fInt, fmm, markerPos, barHeights, wmoIcon, trendLabel,
|
||||||
formatDate, formatTime, dayAbbr, monthName, isOlderThan, msUntil,
|
formatDate, formatTime, dayAbbr, monthName, isOlderThan, msUntil,
|
||||||
co2Level, fPct, fHpa, seriesRange, sparklinePath,
|
co2Level, fPct, fHpa, seriesRange, sparklinePath,
|
||||||
windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom,
|
windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, hourTicks,
|
||||||
} from './format.js';
|
} from './format.js';
|
||||||
import { icon, windArrow } from './icons.js';
|
import { icon, windArrow } from './icons.js';
|
||||||
|
|
||||||
@@ -104,10 +104,25 @@ function renderRain(rain, now) {
|
|||||||
function renderPressure(p) {
|
function renderPressure(p) {
|
||||||
setText('press', fHpa(p.current));
|
setText('press', fHpa(p.current));
|
||||||
setText('press-trend', trendLabel(p.trend));
|
setText('press-trend', trendLabel(p.trend));
|
||||||
|
|
||||||
const values = p.history.map((h) => h.hpa);
|
const values = p.history.map((h) => h.hpa);
|
||||||
const range = seriesRange(values);
|
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));
|
$('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) {
|
function renderForecast(days) {
|
||||||
|
|||||||
@@ -219,3 +219,30 @@ export function pinRoom(rooms, pinnedId) {
|
|||||||
copy.unshift(pinned);
|
copy.unshift(pinned);
|
||||||
return copy;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
+9
-1
@@ -36,11 +36,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="a-pressure">
|
<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>
|
<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">
|
<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"/>
|
<line class="a-spark-base" x1="0" y1="55.5" x2="560" y2="55.5"/>
|
||||||
<path class="a-spark-line" id="press-path" d=""/>
|
<path class="a-spark-line" id="press-path" d=""/>
|
||||||
</svg>
|
</svg>
|
||||||
|
<div></div>
|
||||||
|
<div class="a-px num" id="press-hours"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
+9
-5
@@ -80,7 +80,7 @@ html, body {
|
|||||||
.a-hero { align-self: center; }
|
.a-hero { align-self: center; }
|
||||||
.a-label { font-size: 22px; color: var(--muted); font-weight: 500; display: flex; gap: 14px; align-items: baseline; }
|
.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-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 { font-size: 17px; font-weight: 400; }
|
||||||
.a-hum:not(:empty)::before { content: "·"; margin-right: 14px; }
|
.a-hum:not(:empty)::before { content: "·"; margin-right: 14px; }
|
||||||
.a-range { width: 560px; }
|
.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 { 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; }
|
.a-ends b { font-size: 24px; font-weight: 600; margin-right: 6px; }
|
||||||
|
|
||||||
/* Luchtdruk, 24 uur */
|
/* Luchtdruk, 24 uur. Grafiek met een y-as (hoogste/laagste waarde) en tijdlabels op de x-as. */
|
||||||
.a-pressure { width: 560px; margin-top: 18px; }
|
.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 { 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-plabel b { font-size: 20px; font-weight: 600; color: var(--ink); }
|
||||||
.a-prange { margin-left: auto; font-size: 14px; }
|
.a-pgrid { display: grid; grid-template-columns: 44px 1fr; column-gap: 8px; row-gap: 3px; }
|
||||||
.a-spark { display: block; width: 560px; height: 56px; overflow: visible; }
|
.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-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; }
|
.a-spark-line { fill: none; stroke: var(--rain); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
|
||||||
|
|
||||||
/* Binnen */
|
/* Binnen */
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/* Service worker: app-shell cache-first, API network-first met timeout en fallback op de laatste response. */
|
/* 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 = [
|
const SHELL = [
|
||||||
'./',
|
'./',
|
||||||
'./index.html',
|
'./index.html',
|
||||||
|
|||||||
+37
-1
@@ -121,7 +121,7 @@ test('msUntil computes the wait until the next local time', () => {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
co2Level, fPct, fHpa, seriesRange, sparklinePath, sparklinePoints,
|
co2Level, fPct, fHpa, seriesRange, sparklinePath, sparklinePoints,
|
||||||
windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom,
|
windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, hourTicks,
|
||||||
} from '../../public/format.js';
|
} from '../../public/format.js';
|
||||||
|
|
||||||
test('co2Level uses the Netatmo thresholds', () => {
|
test('co2Level uses the Netatmo thresholds', () => {
|
||||||
@@ -208,3 +208,39 @@ test('pinRoom does not mutate the input array', () => {
|
|||||||
pinRoom(rooms, 'c');
|
pinRoom(rooms, 'c');
|
||||||
assert.deepEqual(rooms, original);
|
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), []);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user