[Perl] DAQ system for the FAPG.
feat add graph trendlines
Add thin red least-squares trendlines to probe charts so trends remain visible across missing readings.
roles/dashboard/public/js/dashboard/charts.js
@@ -35,7 +35,7 @@
35
35
}
36
36
37
37
function chartSeries(rows, timeframe) {
38
Removed:
const series = { labels: [], timestamps: [], lower: [], upper: [], trend: [] };
38
Added:
const series = { labels: [], timestamps: [], lower: [], upper: [], trend: [], trendline: [] };
39
39
40
40
rows.forEach(row => {
41
41
if (row.gap && series.labels.length) {
@@ -44,6 +44,7 @@
44
44
series.lower.push(null);
45
45
series.upper.push(null);
46
46
series.trend.push(null);
47
Added:
series.trendline.push(null);
47
48
}
48
49
49
50
series.labels.push(formatChartTime(row.timestamp, timeframe));
@@ -53,9 +54,39 @@
53
54
series.trend.push(row.value === null ? null : Number(row.value));
54
55
});
55
56
57
Added:
series.trendline = linearTrendline(series.timestamps, series.trend);
56
58
return series;
57
59
}
58
60
61
Added:
function linearTrendline(timestamps, values) {
62
Added:
const points = values.flatMap((value, index) => {
63
Added:
const timestamp = Date.parse(timestamps[index]);
64
Added:
return Number.isFinite(value) && Number.isFinite(timestamp)
65
Added:
? [{ timestamp, value }]
66
Added:
: [];
67
Added:
});
68
Added:
69
Added:
if (points.length < 2) return timestamps.map(() => null);
70
Added:
71
Added:
const origin = points[0].timestamp;
72
Added:
const meanX = points.reduce((sum, point) => sum + point.timestamp - origin, 0) / points.length;
73
Added:
const meanY = points.reduce((sum, point) => sum + point.value, 0) / points.length;
74
Added:
const variance = points.reduce((sum, point) => {
75
Added:
const x = point.timestamp - origin - meanX;
76
Added:
return sum + x * x;
77
Added:
}, 0);
78
Added:
const covariance = points.reduce((sum, point) => {
79
Added:
const x = point.timestamp - origin - meanX;
80
Added:
return sum + x * (point.value - meanY);
81
Added:
}, 0);
82
Added:
const slope = variance ? covariance / variance : 0;
83
Added:
84
Added:
return timestamps.map(timestamp => {
85
Added:
const x = Date.parse(timestamp);
86
Added:
return Number.isFinite(x) ? meanY + slope * (x - origin - meanX) : null;
87
Added:
});
88
Added:
}
89
Added:
59
90
function buildMessageCell(text) {
60
91
const cell = document.createElement("td");
61
92
cell.textContent = text;
@@ -137,6 +168,15 @@
137
168
pointRadius: 0,
138
169
pointHoverRadius: 0,
139
170
tooltipUnit: trendUnit
171
Added:
},
172
Added:
{
173
Added:
label: "Linear trend",
174
Added:
data: series.trendline,
175
Added:
borderColor: "#dc2626",
176
Added:
borderWidth: 1,
177
Added:
pointRadius: 0,
178
Added:
pointHoverRadius: 0,
179
Added:
spanGaps: true
140
180
}
141
181
]
142
182
},
@@ -191,7 +231,12 @@
191
231
.reverse()
192
232
.find(row => row.value !== null && Number.isFinite(Number(row.value)));
193
233
const unit = [...data.series].reverse().find(row => row.unit)?.unit || "";
194
Removed:
const yBounds = yAxisBounds(probe, [...series.lower, ...series.upper, ...series.trend]);
234
Added:
const yBounds = yAxisBounds(probe, [
235
Added:
...series.lower,
236
Added:
...series.upper,
237
Added:
...series.trend,
238
Added:
...series.trendline
239
Added:
]);
195
240
const trendLabel = `${probe} ${unit ? `(${unit})` : ""} trend`;
196
241
197
242
if (!charts.has(probe)) {
@@ -205,6 +250,7 @@
205
250
chart.data.datasets[2].data = series.trend;
206
251
chart.data.datasets[2].label = trendLabel;
207
252
chart.data.datasets[2].tooltipUnit = unit;
253
Added:
chart.data.datasets[3].data = series.trendline;
208
254
chart.options.scales.x.ticks.maxTicksLimit = TIMEFRAMES[timeframe.selected()].ticks;
209
255
chart.options.scales.y.min = yBounds.min;
210
256
chart.options.scales.y.max = yBounds.max;