diff --git a/src/static/dashboard.html b/src/static/dashboard.html index a418c63..cd7d470 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -197,7 +197,7 @@

PostgreSQL history

Select a RID flow station to load the last 7 days

-
+
@@ -324,19 +324,41 @@ const response = await fetch(`/measurements/history/${encodeURIComponent(stationCode)}?hours=${$('history-range').value}`); if (!response.ok) throw new Error((await response.json()).detail || `HTTP ${response.status}`); const rows = await response.json(); + + // Downsample to daily averages to prevent browser freezing with 50k+ points + const downsample = (data) => { + const buckets = {}; + data.forEach((row, i) => { + const date = new Date(row.timestamp); + const key = `${date.getUTCFullYear()}-${date.getUTCMonth()}-${date.getUTCDate()}`; + if (!buckets[key]) buckets[key] = { ts: row.timestamp, discharge: [], level: [], count: 0 }; + const b = buckets[key]; + if (row.discharge != null) b.discharge.push(row.discharge); + if (row.water_level != null) b.level.push(row.water_level); + b.count++; + }); + return Object.values(buckets).map((b) => ({ + timestamp: b.ts, + discharge: b.discharge.length ? b.discharge.reduce((a, c) => a + c, 0) / b.discharge.length : null, + water_level: b.level.length ? b.level.reduce((a, c) => a + c, 0) / b.level.length : null, + })); + }; + + const sampled = rows.length > 2000 ? downsample(rows) : rows; + if (state.historyChart) state.historyChart.destroy(); state.historyChart = new Chart($('history-chart'), { type: 'line', data: { - labels: rows.map((row) => new Date(row.timestamp).toLocaleString('en-TH', { timeZone: 'Asia/Bangkok', month: 'short', day: 'numeric', year: '2-digit', hour: '2-digit' })), + labels: sampled.map((row) => new Date(row.timestamp).toLocaleString('en-TH', { timeZone: 'Asia/Bangkok', month: 'short', day: 'numeric', year: '2-digit', hour: '2-digit' })), datasets: [ - { label: 'Discharge (m³/s)', data: rows.map((row) => row.discharge), borderColor: '#087da5', backgroundColor: 'rgba(8,125,165,.12)', yAxisID: 'flow', pointRadius: 0, tension: .25 }, - { label: 'Water level (m)', data: rows.map((row) => row.water_level), borderColor: '#d99018', yAxisID: 'level', pointRadius: 0, tension: .25 } + { label: 'Discharge (m³/s)', data: sampled.map((row) => row.discharge), borderColor: '#087da5', backgroundColor: 'rgba(8,125,165,.12)', yAxisID: 'flow', pointRadius: 0, tension: .25, parsing: false }, + { label: 'Water level (m)', data: sampled.map((row) => row.water_level), borderColor: '#d99018', yAxisID: 'level', pointRadius: 0, tension: .25, parsing: false } ] }, - options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, scales: { flow: { type: 'linear', position: 'left' }, level: { type: 'linear', position: 'right', grid: { drawOnChartArea: false } }, x: { ticks: { maxTicksLimit: 10 } } } } + options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0 }, interaction: { mode: 'index', intersect: false }, scales: { flow: { type: 'linear', position: 'left' }, level: { type: 'linear', position: 'right', grid: { drawOnChartArea: false } }, x: { ticks: { maxTicksLimit: 12 } } } } }); - $('history-status').textContent = rows.length ? `${rows.length} measurements from PostgreSQL` : 'No PostgreSQL measurements in this period'; + $('history-status').textContent = rows.length ? `${rows.length} measurements (${sampled.length} daily) from PostgreSQL` : 'No PostgreSQL measurements in this period'; $('history-card').scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (error) { $('history-status').textContent = `History unavailable: ${error.message}`;