diff --git a/app/public/dashboard-ui.css b/app/public/dashboard-ui.css index bf2ddcc..1517609 100644 --- a/app/public/dashboard-ui.css +++ b/app/public/dashboard-ui.css @@ -115,3 +115,10 @@ canvas.sparkline { max-width: 100%; } .tab:hover, .logout:hover { border-color: var(--ui); color: var(--ui); } .chart-reset:hover { border-color: var(--ui); } a { color: var(--ui); } + +/* Per-chart controls */ +.chart-ctl { display: inline-flex; flex-wrap: wrap; gap: 0.4rem 0.8rem; align-items: center; text-transform: none; letter-spacing: 0; font-weight: 500; font-size: 0.74rem; color: var(--ink-light); } +.chart-ctl label { display: inline-flex; align-items: center; gap: 0.35rem; } +.chart-ctl select { font: inherit; font-size: 0.76rem; padding: 0.25rem 0.4rem; min-height: 30px; border-radius: 8px; border: 1px solid var(--border); background: var(--paper); color: var(--ink); } +@media (pointer: coarse) { .chart-ctl select { font-size: 16px; min-height: 40px; } } +.chart-card .chart-title { flex-wrap: wrap; row-gap: 0.4rem; } diff --git a/app/public/dashboard.html b/app/public/dashboard.html index deb2a95..2ffe0fd 100644 --- a/app/public/dashboard.html +++ b/app/public/dashboard.html @@ -611,6 +611,7 @@ let charts = {}; // ── Formatting helpers ────────────────────────────────────────────────────── function fmtTime(ts, range) { const d = new Date(ts); + if (range === '2d') return d.toLocaleDateString('en-US', { weekday: 'short' }) + ' ' + d.toLocaleTimeString('en-US', { hour: 'numeric', hour12: true }); if (['7d','30d','90d','1y','2y','3y'].includes(range)) { return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } @@ -779,6 +780,64 @@ function drawWindRose(canvas, readings) { })(t0); } +// ── Pressure tendency (own range + change window) ──────────────────────────── +let presTendRange = 'follow', presTendWin = 3; +try { presTendRange = localStorage.getItem('ptRange') || 'follow'; presTendWin = parseInt(localStorage.getItem('ptWin')) || 3; } catch {} +const presTendCache = {}; +let presTendLast = { pts: null, range: null }; + +async function drawPresTend(pagePts, pageRange) { + const canvas = document.getElementById('cPresTend'); + if (!canvas) return; + let pts = pagePts, range = pageRange; + if (presTendRange !== 'follow') { + range = presTendRange; + const hit = presTendCache[range]; + if (hit && Date.now() - hit.at < 120000) pts = hit.data; + else { + try { + const bucket = ['7d', '30d'].includes(range) ? '&points=720' : ''; + const r = await fetch(`/api/station/history?range=${range}${bucket}`); + pts = r.ok ? await r.json() : []; + } catch { pts = []; } + presTendCache[range] = { at: Date.now(), data: pts }; + } + if (document.getElementById('cPresTend') !== canvas) return; // charts were re-rendered meanwhile + } + if (pts.length > 900) { const step = Math.ceil(pts.length / 720); pts = pts.filter((_, i) => i % step === 0); } + const winMs = presTendWin * 3600000; + const c = chartColors(); + const tend = pts.map(r => { + const t = r.dateutc - winMs; + if (!pts.length || t < pts[0].dateutc) return null; + let lo = 0, hi = pts.length - 1; + while (lo < hi) { const m = (lo + hi) >> 1; if (pts[m].dateutc < t) lo = m + 1; else hi = m; } + const base = pts[lo].baromrelin; + return r.baromrelin != null && base != null ? +(r.baromrelin - base).toFixed(3) : null; + }); + const ds = barDataset(`${presTendWin}h change`, tend, c.cool); + ds.backgroundColor = tend.map(v => v == null ? 'transparent' : (v >= 0 ? c.cool + 'bb' : c.accent + 'bb')); + ds.borderWidth = 0; + makeChart('cPresTend', 'bar', [ds], pts.map(r => fmtTime(r.dateutc, range)), `inHg / ${presTendWin}h`); + const title = document.getElementById('ptTitle'); + if (title) title.textContent = `Pressure Tendency · ${presTendWin}-hour change (inHg)`; + if (!tend.some(v => v != null) && title) title.textContent += ' — not enough data for this window'; +} + +function initPresTend(pagePts, pageRange) { + presTendLast = { pts: pagePts, range: pageRange }; + const selRange = document.getElementById('ptRange'), selWin = document.getElementById('ptWin'); + if (selRange) { + selRange.value = presTendRange; + selRange.addEventListener('change', () => { presTendRange = selRange.value; try { localStorage.setItem('ptRange', presTendRange); } catch {} drawPresTend(presTendLast.pts, presTendLast.range); }); + } + if (selWin) { + selWin.value = String(presTendWin); + selWin.addEventListener('change', () => { presTendWin = parseInt(selWin.value); try { localStorage.setItem('ptWin', presTendWin); } catch {} drawPresTend(presTendLast.pts, presTendLast.range); }); + } + drawPresTend(pagePts, pageRange); +} + // ── Render charts ──────────────────────────────────────────────────────────── function renderCharts(readings, range, yoy = null) { const area = document.getElementById('chartsArea'); @@ -835,7 +894,15 @@ function renderCharts(readings, range, yoy = null) {
-
Pressure Tendency · 3-hour change (inHg)
+
Pressure Tendency (inHg) + + + +
@@ -921,21 +988,8 @@ function renderCharts(readings, range, yoy = null) { } makeChart('cSolar', 'line', solarSets, lbls, 'W/m²', { scales: { y: { min: 0 } } }); - // Pressure tendency: change vs 3 hours earlier (what forecasters watch) - if (hasPres) { - const tend = pts.map(r => { - const t = r.dateutc - 3 * 3600000; - let lo = 0, hi = pts.length - 1; - if (t < pts[0].dateutc) return null; - while (lo < hi) { const m = (lo + hi) >> 1; if (pts[m].dateutc < t) lo = m + 1; else hi = m; } - const base = pts[lo].baromrelin; - return r.baromrelin != null && base != null ? +(r.baromrelin - base).toFixed(3) : null; - }); - const ds = barDataset('3h change', tend, c.cool); - ds.backgroundColor = tend.map(v => v == null ? 'transparent' : (v >= 0 ? c.cool + 'bb' : c.accent + 'bb')); - ds.borderWidth = 0; - makeChart('cPresTend', 'bar', [ds], lbls, 'inHg / 3h'); - } + // Pressure tendency: change vs N hours earlier (what forecasters watch); has its own range + window + if (hasPres) initPresTend(pts, range); // Indoor vs outdoor if (pts.some(r => r.tempinf != null)) {