feat: uuroverzicht per dag (temperatuur, regen, wind)
Tik op een dag in de verwachtingsstrook voor een uuroverzicht van
temperatuur, regen en wind (richting + snelheid), om de 2 uur, 12
punten per dag. Sluit na 20 seconden vanzelf of bij een tik op de
overlay zelf.
Backend: Open-Meteo levert hourly-data in dezelfde call als daily
(geen extra verzoek, dezelfde 60min-cache). Nieuwe HourlyForecastMapper
groepeert per kalenderdatum en bemonstert om het andere uur, robuust
voor de zomertijddag (23/25 uur). OpenMeteoClient::dailyForecast()
hernoemd naar forecast(), geeft nu {daily, hourly} terug. Elke dag in
de forecast-array krijgt een hourly-lijst.
Frontend: temperatuurlijn hergebruikt sparklinePath (nu ook als
sparklinePoints voor exacte uitlijning met de 12-koloms grid), regen
als kleine staafjes, windpijl (nieuw icoon) wijst waarheen de wind
waait. 20s-balk via CSS transition met reflow-restart. Servicewerker
naar v5.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+61
-2
@@ -2,8 +2,9 @@ import {
|
||||
f1, fInt, fmm, markerPos, barHeights, wmoIcon, trendLabel,
|
||||
formatDate, formatTime, dayAbbr, monthName, isOlderThan, msUntil,
|
||||
co2Level, fPct, fHpa, seriesRange, sparklinePath,
|
||||
windArrowDeg, fKmh, formatHour, formatFullDate,
|
||||
} from './format.js';
|
||||
import { icon } from './icons.js';
|
||||
import { icon, windArrow } from './icons.js';
|
||||
|
||||
const CANVAS_W = 1133;
|
||||
const CANVAS_H = 744;
|
||||
@@ -13,6 +14,8 @@ const FETCH_TIMEOUT_MS = 10000;
|
||||
const STALE_MINUTES = 30;
|
||||
const RELOAD_HOUR = 4;
|
||||
const FALLBACK_TZ = 'Europe/Berlin';
|
||||
const HOURLY_OVERLAY_MS = 20000;
|
||||
const HOURLY_WIDTH = 1045; // = stage-breedte 1133 min 2x 44px padding, zelfde marge als de rest van het scherm
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const stage = $('stage');
|
||||
@@ -104,7 +107,7 @@ function renderPressure(p) {
|
||||
}
|
||||
|
||||
function renderForecast(days) {
|
||||
$('forecast').innerHTML = days.map((d) => `<div class="a-day">
|
||||
$('forecast').innerHTML = days.map((d) => `<div class="a-day" data-date="${d.date}">
|
||||
<p class="a-dname">${dayAbbr(d.date, state.tz)}</p>
|
||||
<div class="a-icon">${icon(wmoIcon(d.weather_code))}</div>
|
||||
<p class="a-hl num">${fInt(d.temp_max)}°<span>${fInt(d.temp_min)}°</span></p>
|
||||
@@ -112,6 +115,56 @@ function renderForecast(days) {
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
/* ---------- uuroverzicht per dag ---------- */
|
||||
|
||||
let hourlyOverlayTimer = null;
|
||||
|
||||
function renderHourlyCharts(hourly) {
|
||||
const temps = hourly.map((h) => h.temp);
|
||||
$('ho-temp-path').setAttribute('d', sparklinePath(temps, HOURLY_WIDTH, 64, HOURLY_WIDTH / 24));
|
||||
$('ho-temp-nums').innerHTML = hourly.map((h) => `<span class="num">${fInt(h.temp)}°</span>`).join('');
|
||||
|
||||
const heights = barHeights(hourly.map((h) => h.precip_mm), 40, 2);
|
||||
$('ho-rain').innerHTML = hourly.map((h, i) => `<div class="ho-rbar">
|
||||
<i style="height:${heights[i].toFixed(1)}px"></i><span class="num">${fmm(h.precip_mm)}</span>
|
||||
</div>`).join('');
|
||||
|
||||
$('ho-wind').innerHTML = hourly.map((h) => {
|
||||
const deg = windArrowDeg(h.wind_dir);
|
||||
const arrow = deg === null
|
||||
? '<span class="ho-warrow off">–</span>'
|
||||
: `<span class="ho-warrow" style="transform:rotate(${deg}deg)">${windArrow()}</span>`;
|
||||
return `<div class="ho-wcell">${arrow}<span class="num">${fKmh(h.wind_kmh)}</span></div>`;
|
||||
}).join('');
|
||||
|
||||
$('ho-hours').innerHTML = hourly.map((h) => `<span class="num">${formatHour(h.at, state.tz)}</span>`).join('');
|
||||
}
|
||||
|
||||
function openHourOverlay(date) {
|
||||
const day = (state.data?.forecast || []).find((d) => d.date === date);
|
||||
if (!day) return;
|
||||
|
||||
clearTimeout(hourlyOverlayTimer);
|
||||
setText('ho-date', formatFullDate(day.date, state.tz));
|
||||
renderHourlyCharts(day.hourly || []);
|
||||
|
||||
const fill = $('ho-bar-fill');
|
||||
fill.style.transition = 'none';
|
||||
fill.style.transform = 'scaleX(1)';
|
||||
fill.getBoundingClientRect(); // forceer reflow zodat de volgende transitie opnieuw start
|
||||
fill.style.transition = `transform ${HOURLY_OVERLAY_MS}ms linear`;
|
||||
fill.style.transform = 'scaleX(0)';
|
||||
|
||||
$('hour-overlay').hidden = false;
|
||||
hourlyOverlayTimer = setTimeout(closeHourOverlay, HOURLY_OVERLAY_MS);
|
||||
}
|
||||
|
||||
function closeHourOverlay() {
|
||||
clearTimeout(hourlyOverlayTimer);
|
||||
hourlyOverlayTimer = null;
|
||||
$('hour-overlay').hidden = true;
|
||||
}
|
||||
|
||||
function renderBanner() {
|
||||
const banner = $('banner');
|
||||
const errors = state.data ? state.data.errors : [];
|
||||
@@ -259,6 +312,12 @@ function checkForUpdate() {
|
||||
|
||||
/* ---------- start ---------- */
|
||||
|
||||
$('forecast').addEventListener('click', (e) => {
|
||||
const day = e.target.closest('.a-day');
|
||||
if (day) openHourOverlay(day.dataset.date);
|
||||
});
|
||||
$('hour-overlay').addEventListener('click', closeHourOverlay);
|
||||
|
||||
window.addEventListener('resize', fit);
|
||||
window.addEventListener('orientationchange', () => setTimeout(fit, 50));
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
|
||||
Reference in New Issue
Block a user