[Perl] DAQ system for the FAPG.
Add graph data smoothing control.
Changed files
roles/dashboard/public/css/app.css
@@ -241,6 +241,23 @@
241
241
color: white;
242
242
}
243
243
244
Added:
.smoothing-control {
245
Added:
display: inline-flex;
246
Added:
align-items: center;
247
Added:
gap: 0.45rem;
248
Added:
min-height: 2.05rem;
249
Added:
color: var(--text);
250
Added:
cursor: pointer;
251
Added:
font-size: 0.88rem;
252
Added:
font-weight: 700;
253
Added:
}
254
Added:
255
Added:
.smoothing-control input {
256
Added:
width: 1rem;
257
Added:
height: 1rem;
258
Added:
accent-color: var(--accent);
259
Added:
}
260
Added:
244
261
.chart-wrap {
245
262
min-height: 420px;
246
263
}
@@ -330,6 +347,10 @@
330
347
331
348
.timeframe-button:nth-child(odd) {
332
349
border-left: 0;
350
Added:
}
351
Added:
352
Added:
.smoothing-control {
353
Added:
width: 100%;
333
354
}
334
355
335
356
.section-heading {
roles/dashboard/public/js/dashboard.js
@@ -25,7 +25,9 @@
25
25
orp: 200,
26
26
ec: 500
27
27
};
28
Added:
const SMOOTHING_WINDOW = 7;
28
29
let activeTimeframe = DEFAULT_TIMEFRAME;
30
Added:
let smoothingEnabled = true;
29
31
30
32
function probeRows() {
31
33
return Array.from(document.querySelectorAll("[data-latest-probe]"));
@@ -47,6 +49,10 @@
47
49
return document.querySelector(`[data-reading-messages="${probe}"]`);
48
50
}
49
51
52
Added:
function smoothingToggles() {
53
Added:
return Array.from(document.querySelectorAll("[data-smoothing-toggle]"));
54
Added:
}
55
Added:
50
56
function labelForRow(row) {
51
57
return formatChartTime(row.timestamp);
52
58
}
@@ -157,6 +163,34 @@
157
163
};
158
164
}
159
165
166
Added:
function movingAverage(values, windowSize = SMOOTHING_WINDOW) {
167
Added:
if (values.length < 3 || windowSize < 3) {
168
Added:
return values;
169
Added:
}
170
Added:
171
Added:
const radius = Math.floor(windowSize / 2);
172
Added:
173
Added:
return values.map((value, index) => {
174
Added:
if (!Number.isFinite(value)) {
175
Added:
return value;
176
Added:
}
177
Added:
178
Added:
const start = Math.max(0, index - radius);
179
Added:
const end = Math.min(values.length, index + radius + 1);
180
Added:
const neighbors = values.slice(start, end).filter(Number.isFinite);
181
Added:
182
Added:
if (!neighbors.length) {
183
Added:
return value;
184
Added:
}
185
Added:
186
Added:
return neighbors.reduce((sum, neighbor) => sum + neighbor, 0) / neighbors.length;
187
Added:
});
188
Added:
}
189
Added:
190
Added:
function chartValues(values) {
191
Added:
return smoothingEnabled ? movingAverage(values) : values;
192
Added:
}
193
Added:
160
194
function statusLabel(state) {
161
195
return {
162
196
online: "Online",
@@ -313,7 +347,8 @@
313
347
const rows = payload.readings || [];
314
348
315
349
const labels = rows.map(labelForRow);
316
Removed:
const values = rows.map(row => Number(row.value));
350
Added:
const rawValues = rows.map(row => Number(row.value));
351
Added:
const values = chartValues(rawValues);
317
352
const unit = rows.length ? rows[rows.length - 1].unit : "";
318
353
const yBounds = yAxisBounds(probe, values);
319
354
@@ -325,7 +360,7 @@
325
360
data: {
326
361
labels,
327
362
datasets: [{
328
Removed:
label: `${probe} ${unit ? `(${unit})` : ""}`,
363
Added:
label: `${probe} ${unit ? `(${unit})` : ""}${smoothingEnabled ? " smoothed" : ""}`,
329
364
data: values,
330
365
tension: 0.2,
331
366
pointRadius: 0
@@ -364,7 +399,7 @@
364
399
365
400
chart.data.labels = labels;
366
401
chart.data.datasets[0].data = values;
367
Removed:
chart.data.datasets[0].label = `${probe} ${unit ? `(${unit})` : ""}`;
402
Added:
chart.data.datasets[0].label = `${probe} ${unit ? `(${unit})` : ""}${smoothingEnabled ? " smoothed" : ""}`;
368
403
chart.options.scales.x.ticks.maxTicksLimit = TIMEFRAMES[selectedTimeframe()].ticks;
369
404
chart.options.scales.y.min = yBounds.min;
370
405
chart.options.scales.y.max = yBounds.max;
@@ -413,6 +448,16 @@
413
448
loadProbe(activeProbe());
414
449
}
415
450
451
Added:
function setSmoothing(enabled) {
452
Added:
smoothingEnabled = enabled;
453
Added:
454
Added:
smoothingToggles().forEach(toggle => {
455
Added:
toggle.checked = smoothingEnabled;
456
Added:
});
457
Added:
458
Added:
loadProbe(activeProbe());
459
Added:
}
460
Added:
416
461
document.querySelectorAll(".tab-button").forEach(button => {
417
462
button.addEventListener("click", () => {
418
463
activateTab(button.dataset.probe);
@@ -422,6 +467,12 @@
422
467
document.querySelectorAll(".timeframe-button").forEach(button => {
423
468
button.addEventListener("click", () => {
424
469
setTimeframe(button.dataset.timeframe);
470
Added:
});
471
Added:
});
472
Added:
473
Added:
smoothingToggles().forEach(toggle => {
474
Added:
toggle.addEventListener("change", () => {
475
Added:
setSmoothing(toggle.checked);
425
476
});
426
477
});
427
478
roles/dashboard/t/basic.t
@@ -120,6 +120,8 @@
120
120
->content_like(qr/Daily/)
121
121
->content_like(qr/Weekly/)
122
122
->content_like(qr/Yearly/)
123
Added:
->content_like(qr/data-smoothing-toggle checked/)
124
Added:
->content_like(qr/smoothing/)
123
125
->content_like(qr/Last 10 pH Reading Messages/)
124
126
->content_like(qr/Last 10 Dissolved Oxygen Reading Messages/)
125
127
->content_like(qr/status-pill-list/)
roles/dashboard/templates/dashboard/index.html.ep
@@ -59,6 +59,10 @@
59
59
<button class="timeframe-button" type="button" data-timeframe="week">Weekly</button>
60
60
<button class="timeframe-button" type="button" data-timeframe="year">Yearly</button>
61
61
</div>
62
Added:
<label class="smoothing-control">
63
Added:
<input type="checkbox" data-smoothing-toggle checked>
64
Added:
<span>smoothing</span>
65
Added:
</label>
62
66
</div>
63
67
</div>
64
68