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}`;