ui: mode-aware chart per device (Temp/Light/Door/Vibration/Magnet)

This commit is contained in:
2026-07-03 18:14:37 +02:00
parent 12d80eb63b
commit d432d9607e
+75 -14
View File
@@ -30,11 +30,11 @@
<section class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8" id="devices"></section> <section class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8" id="devices"></section>
<!-- Chart --> <!-- Chart -->
<section class="bg-white rounded-lg border border-slate-200 mb-8 overflow-hidden"> <section id="chart-section" class="bg-white rounded-lg border border-slate-200 mb-8 overflow-hidden">
<div class="px-5 py-4 border-b border-slate-200 flex items-center justify-between"> <div class="px-5 py-4 border-b border-slate-200 flex items-center justify-between">
<div> <div>
<h2 class="text-sm font-semibold text-slate-900">History</h2> <h2 class="text-sm font-semibold text-slate-900">History</h2>
<p class="text-xs text-slate-500 mt-0.5">Temperature & humidity over time</p> <p id="chart-subtitle" class="text-xs text-slate-500 mt-0.5"></p>
</div> </div>
<select id="chart-device" class="text-sm bg-white border border-slate-200 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-2 focus:ring-slate-900/10"></select> <select id="chart-device" class="text-sm bg-white border border-slate-200 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-2 focus:ring-slate-900/10"></select>
</div> </div>
@@ -191,25 +191,86 @@ function renderMessages() {
`).join(''); `).join('');
} }
const chartPresets = {
Temperature: {
subtitle: 'Temperature & humidity over time',
datasets: (msgs) => [
makeDs('Temperature °C', msgs.map(m => m.decoded?.temperature_c ?? null), '#2563eb', 'y'),
makeDs('Humidity %', msgs.map(m => m.decoded?.humidity_pct ?? null), '#059669', 'y1'),
],
},
Light: {
subtitle: 'Brightness over time',
datasets: (msgs) => [
makeDs('Brightness (lux)', msgs.map(m => m.decoded?.brightness_lux ?? null), '#d97706', 'y'),
],
},
Door: {
subtitle: 'Cumulative openings',
datasets: (msgs) => [
makeDs('Event count', msgs.map(m => m.decoded?.event_count ?? null), '#7c3aed', 'y'),
],
},
Vibration: {
subtitle: 'Cumulative vibration events',
datasets: (msgs) => [
makeDs('Event count', msgs.map(m => m.decoded?.event_count ?? null), '#e11d48', 'y'),
],
},
Move: {
subtitle: 'Cumulative movement events',
datasets: (msgs) => [
makeDs('Move events', msgs.map(m => m.decoded?.move_events ?? null), '#e11d48', 'y'),
],
},
Magnet: {
subtitle: 'Cumulative magnet events',
datasets: (msgs) => [
makeDs('Event count', msgs.map(m => m.decoded?.event_count ?? null), '#7c3aed', 'y'),
],
},
'Reed Switch': {
subtitle: 'Cumulative reed switch events',
datasets: (msgs) => [
makeDs('Reed events', msgs.map(m => m.decoded?.reed_events ?? null), '#7c3aed', 'y'),
],
},
};
function makeDs(label, data, color, yID) {
return {
label, data, borderColor: color, backgroundColor: color + '14',
yAxisID: yID, tension: 0.35, borderWidth: 2, fill: true,
spanGaps: true, pointRadius: 2, pointHoverRadius: 4,
};
}
function renderChart() { function renderChart() {
const dev = state.selectedDevice || document.getElementById('chart-device').value; const dev = state.selectedDevice || document.getElementById('chart-device').value;
if (!dev) return; const section = document.getElementById('chart-section');
const subtitle = document.getElementById('chart-subtitle');
if (!dev) { section.style.display = 'none'; return; }
const msgs = (state.messagesByDevice[dev] || []).slice().reverse(); const msgs = (state.messagesByDevice[dev] || []).slice().reverse();
const latestMode = msgs.length ? msgs[msgs.length - 1].decoded?.mode : null;
const preset = chartPresets[latestMode];
if (!preset) {
section.style.display = 'none';
return;
}
section.style.display = '';
subtitle.textContent = preset.subtitle;
const labels = msgs.map(m => fmtShortTime(m.ts)); const labels = msgs.map(m => fmtShortTime(m.ts));
const temps = msgs.map(m => m.decoded?.temperature_c ?? null); const datasets = preset.datasets(msgs);
const humi = msgs.map(m => m.decoded?.humidity_pct ?? null); const hasSecondAxis = datasets.some(ds => ds.yAxisID === 'y1');
const ctx = document.getElementById('chart').getContext('2d'); const ctx = document.getElementById('chart').getContext('2d');
if (state.chart) state.chart.destroy(); if (state.chart) state.chart.destroy();
state.chart = new Chart(ctx, { state.chart = new Chart(ctx, {
type: 'line', type: 'line',
data: { data: { labels, datasets },
labels,
datasets: [
{ label: 'Temperature °C', data: temps, borderColor: '#2563eb', backgroundColor: 'rgba(37,99,235,0.08)', yAxisID: 'y', tension: 0.35, borderWidth: 2, fill: true, spanGaps: true, pointRadius: 2, pointHoverRadius: 4 },
{ label: 'Humidity %', data: humi, borderColor: '#059669', backgroundColor: 'rgba(5,150,105,0.08)', yAxisID: 'y1', tension: 0.35, borderWidth: 2, fill: true, spanGaps: true, pointRadius: 2, pointHoverRadius: 4 },
]
},
options: { options: {
responsive: true, responsive: true,
interaction: { mode: 'index', intersect: false }, interaction: { mode: 'index', intersect: false },
@@ -219,8 +280,8 @@ function renderChart() {
}, },
scales: { scales: {
x: { ticks: { color: '#94a3b8', font: { size: 11 } }, grid: { color: '#f1f5f9' } }, x: { ticks: { color: '#94a3b8', font: { size: 11 } }, grid: { color: '#f1f5f9' } },
y: { ticks: { color: '#2563eb', font: { size: 11 } }, grid: { color: '#f1f5f9' }, position: 'left', border: { display: false } }, y: { ticks: { color: datasets[0].borderColor, font: { size: 11 } }, grid: { color: '#f1f5f9' }, position: 'left', border: { display: false } },
y1: { ticks: { color: '#059669', font: { size: 11 } }, grid: { drawOnChartArea: false }, position: 'right', border: { display: false } }, ...(hasSecondAxis ? { y1: { ticks: { color: datasets[1].borderColor, font: { size: 11 } }, grid: { drawOnChartArea: false }, position: 'right', border: { display: false } } } : {}),
} }
} }
}); });