[Perl] DAQ system for the FAPG.
insights: normalise rate-of-change to relative y-axis
Each probe's derivative is divided by its optimal range span (goodMax - goodMin) and expressed as a percentage per hour. This makes magnitudes comparable across probes with very different absolute scales (e.g. pH 0.8 range vs ORP 150 mV range).
Changed files
roles/dashboard/public/js/dashboard/insights.js
@@ -32,6 +32,14 @@
32
32
caretSize: 0
33
33
};
34
34
35
Added:
// Optimal ranges used to normalise derivatives to % per hour
36
Added:
const OPTIMAL_SPANS = {
37
Added:
ph: READING_RANGES.ph.goodMax - READING_RANGES.ph.goodMin,
38
Added:
do: READING_RANGES.do.goodMax - READING_RANGES.do.goodMin,
39
Added:
orp: READING_RANGES.orp.goodMax - READING_RANGES.orp.goodMin,
40
Added:
ec: READING_RANGES.ec.goodMax - READING_RANGES.ec.goodMin,
41
Added:
};
42
Added:
35
43
let charts = {};
36
44
37
45
async function fetchJson(url, signal) {
@@ -108,17 +116,23 @@
108
116
const refProbe = probeKeys.find(p => data.probes[p].length > 0);
109
117
const labels = refProbe ? data.probes[refProbe].map(p => formatDateTime(p.timestamp)) : [];
110
118
111
Removed:
const datasets = probeKeys.map(probe => ({
112
Removed:
label: probe.toUpperCase(),
113
Removed:
data: data.probes[probe].map(p => p.value),
114
Removed:
borderColor: PROBE_COLORS[probe]?.border || "#64748b",
115
Removed:
backgroundColor: PROBE_COLORS[probe]?.background || "rgba(100,116,139,0.1)",
116
Removed:
tension: 0.3,
117
Removed:
pointRadius: 0,
118
Removed:
pointHoverRadius: 3,
119
Removed:
fill: false
120
Removed:
}));
119
Added:
const datasets = probeKeys.map(probe => {
120
Added:
const span = OPTIMAL_SPANS[probe] || 1;
121
Added:
const points = data.probes[probe] || [];
121
122
123
Added:
return {
124
Added:
label: probe.toUpperCase(),
125
Added:
// Normalise: (Δ per hour) / optimal_span * 100 = % of range per hour
126
Added:
data: points.map(p => (p.value / span) * 100),
127
Added:
borderColor: PROBE_COLORS[probe]?.border || "#64748b",
128
Added:
backgroundColor: PROBE_COLORS[probe]?.background || "rgba(100,116,139,0.1)",
129
Added:
tension: 0.3,
130
Added:
pointRadius: 0,
131
Added:
pointHoverRadius: 3,
132
Added:
fill: false
133
Added:
};
134
Added:
});
135
Added:
122
136
if (!charts.derivatives) {
123
137
charts.derivatives = new Chart(canvas, {
124
138
type: "line",
@@ -130,13 +144,22 @@
130
144
scales: {
131
145
x: { display: true, ticks: { maxTicksLimit: 8, font: { size: 10 } } },
132
146
y: {
133
Removed:
title: { display: true, text: "Δ per hour" },
134
Removed:
grid: { color: ctx => ctx.tick.value === 0 ? "#94a3b8" : "rgba(0,0,0,0.05)" }
147
Added:
title: { display: true, text: "% of optimal range / hour" },
148
Added:
grid: { color: ctx => ctx.tick.value === 0 ? "#94a3b8" : "rgba(0,0,0,0.05)" },
149
Added:
ticks: {
150
Added:
callback: value => `${value > 0 ? "+" : ""}${value.toFixed(1)}%`
151
Added:
}
135
152
}
136
153
},
137
154
plugins: {
138
155
legend: { display: true },
139
Removed:
tooltip: { ...TOOLTIP_DEFAULTS, callbacks: { title: () => "" } }
156
Added:
tooltip: {
157
Added:
...TOOLTIP_DEFAULTS,
158
Added:
callbacks: {
159
Added:
title: () => "",
160
Added:
label: ctx => `${ctx.dataset.label}: ${ctx.parsed.y > 0 ? "+" : ""}${ctx.parsed.y.toFixed(2)}%/h`
161
Added:
}
162
Added:
}
140
163
}
141
164
}
142
165
});
roles/dashboard/templates/dashboard/insights.html.ep
@@ -53,12 +53,13 @@
53
53
<div class="card-header">
54
54
<h3 class="h6 mb-0">Rate of Change</h3>
55
55
<p class="text-body-secondary small mb-0 mt-1">
56
Removed:
How fast each parameter is moving
56
Added:
Normalised rate of change per probe (% of optimal range per hour).
57
Added:
Values near zero mean the parameter is steady.
57
58
</p>
58
59
</div>
59
60
<div class="card-body">
60
61
<div class="chart-wrap">
61
Removed:
<canvas id="chart-derivatives" role="img" aria-label="Rate of change per probe"></canvas>
62
Added:
<canvas id="chart-derivatives" role="img" aria-label="Normalised rate of change per probe"></canvas>
62
63
</div>
63
64
</div>
64
65
</div>