[Perl] DAQ system for the FAPG.
perf reduce chart series density
Replace the linear regression trendline with interpolated gap filling. Use coarser chart buckets to reduce chart response and rendering load.
Changed files
roles/dashboard/lib/FAPG/DAQ/Dashboard/Timeframe.pm
@@ -12,23 +12,23 @@
12
12
my %SERIES_TIMEFRAMES = (
13
13
hour => {
14
14
span_seconds => 60 * 60,
15
Removed:
bucket_stride => 60,
15
Added:
bucket_stride => 2 * 60,
16
16
},
17
17
day => {
18
18
span_seconds => 24 * 60 * 60,
19
Removed:
bucket_stride => 15 * 60,
19
Added:
bucket_stride => 30 * 60,
20
20
},
21
21
week => {
22
22
span_seconds => 7 * 24 * 60 * 60,
23
Removed:
bucket_stride => 60 * 60,
23
Added:
bucket_stride => 3 * 60 * 60,
24
24
},
25
25
month => {
26
26
span_seconds => 30 * 24 * 60 * 60,
27
Removed:
bucket_stride => 6 * 60 * 60,
27
Added:
bucket_stride => 12 * 60 * 60,
28
28
},
29
29
year => {
30
30
span_seconds => 365 * 24 * 60 * 60,
31
Removed:
bucket_stride => 24 * 60 * 60,
31
Added:
bucket_stride => 5 * 24 * 60 * 60,
32
32
},
33
33
);
34
34
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: [], trendline: [] };
38
Added:
const series = { labels: [], timestamps: [], lower: [], upper: [], trend: [], gapFill: [] };
39
39
40
40
rows.forEach(row => {
41
41
if (row.gap && series.labels.length) {
@@ -44,7 +44,6 @@
44
44
series.lower.push(null);
45
45
series.upper.push(null);
46
46
series.trend.push(null);
47
Removed:
series.trendline.push(null);
48
47
}
49
48
50
49
series.labels.push(formatChartTime(row.timestamp, timeframe));
@@ -54,37 +53,30 @@
54
53
series.trend.push(row.value === null ? null : Number(row.value));
55
54
});
56
55
57
Removed:
series.trendline = linearTrendline(series.timestamps, series.trend);
56
Added:
series.gapFill = fillGaps(series.trend);
58
57
return series;
59
58
}
60
59
61
Removed:
function linearTrendline(timestamps, values) {
62
Removed:
const points = values.flatMap((value, index) => {
63
Removed:
const timestamp = Date.parse(timestamps[index]);
64
Removed:
return Number.isFinite(value) && Number.isFinite(timestamp)
65
Removed:
? [{ timestamp, value }]
66
Removed:
: [];
67
Removed:
});
60
Added:
function fillGaps(values) {
61
Added:
const filled = [...values];
62
Added:
let previous = null;
68
63
69
Removed:
if (points.length < 2) return timestamps.map(() => null);
64
Added:
values.forEach((value, index) => {
65
Added:
if (!Number.isFinite(value)) return;
70
66
71
Removed:
const origin = points[0].timestamp;
72
Removed:
const meanX = points.reduce((sum, point) => sum + point.timestamp - origin, 0) / points.length;
73
Removed:
const meanY = points.reduce((sum, point) => sum + point.value, 0) / points.length;
74
Removed:
const variance = points.reduce((sum, point) => {
75
Removed:
const x = point.timestamp - origin - meanX;
76
Removed:
return sum + x * x;
77
Removed:
}, 0);
78
Removed:
const covariance = points.reduce((sum, point) => {
79
Removed:
const x = point.timestamp - origin - meanX;
80
Removed:
return sum + x * (point.value - meanY);
81
Removed:
}, 0);
82
Removed:
const slope = variance ? covariance / variance : 0;
67
Added:
if (previous !== null && index - previous > 1) {
68
Added:
const start = values[previous];
69
Added:
const step = (value - start) / (index - previous);
83
70
84
Removed:
return timestamps.map(timestamp => {
85
Removed:
const x = Date.parse(timestamp);
86
Removed:
return Number.isFinite(x) ? meanY + slope * (x - origin - meanX) : null;
71
Added:
for (let gapIndex = previous + 1; gapIndex < index; gapIndex += 1) {
72
Added:
filled[gapIndex] = start + step * (gapIndex - previous);
73
Added:
}
74
Added:
}
75
Added:
76
Added:
previous = index;
87
77
});
78
Added:
79
Added:
return filled;
88
80
}
89
81
90
82
function buildMessageCell(text) {
@@ -170,10 +162,11 @@
170
162
tooltipUnit: trendUnit
171
163
},
172
164
{
173
Removed:
label: "Linear trend",
174
Removed:
data: series.trendline,
165
Added:
label: "Gap fill",
166
Added:
data: series.gapFill,
175
167
borderColor: "#dc2626",
176
168
borderWidth: 1,
169
Added:
borderDash: [4, 4],
177
170
pointRadius: 0,
178
171
pointHoverRadius: 0,
179
172
spanGaps: true
@@ -235,7 +228,7 @@
235
228
...series.lower,
236
229
...series.upper,
237
230
...series.trend,
238
Removed:
...series.trendline
231
Added:
...series.gapFill
239
232
]);
240
233
const trendLabel = `${probe} ${unit ? `(${unit})` : ""} trend`;
241
234
@@ -250,7 +243,7 @@
250
243
chart.data.datasets[2].data = series.trend;
251
244
chart.data.datasets[2].label = trendLabel;
252
245
chart.data.datasets[2].tooltipUnit = unit;
253
Removed:
chart.data.datasets[3].data = series.trendline;
246
Added:
chart.data.datasets[3].data = series.gapFill;
254
247
chart.options.scales.x.ticks.maxTicksLimit = TIMEFRAMES[timeframe.selected()].ticks;
255
248
chart.options.scales.y.min = yBounds.min;
256
249
chart.options.scales.y.max = yBounds.max;
roles/dashboard/t/04-readings-series.t
@@ -23,52 +23,50 @@
23
23
->json_is( '/band/lower_percentile' => 10 )
24
24
->json_is( '/band/upper_percentile' => 90 )
25
25
->json_is( '/gap_seconds' => 30 )
26
Removed:
->json_hasnt('/readings/60');
26
Added:
->json_hasnt('/readings/30');
27
27
assert_series_reading( $t, 1050, 1000, 1100, 2 );
28
28
29
29
$t->get_ok('/api/readings/ec/series?timeframe=day')
30
30
->status_is(200)
31
Removed:
->json_hasnt('/readings/96');
31
Added:
->json_hasnt('/readings/48');
32
32
assert_series_reading( $t, 1050, 1000, 1100, 2 );
33
33
34
34
$t->get_ok('/api/readings/ec/series?timeframe=month')
35
35
->status_is(200)
36
36
->json_is( '/timeframe' => 'month' )
37
Removed:
->json_hasnt('/readings/120');
37
Added:
->json_hasnt('/readings/60');
38
38
assert_series_reading( $t, 1050, 1000, 1100, 2 );
39
39
40
40
$t->get_ok('/api/readings/ec/series?timeframe=year')
41
41
->status_is(200)
42
Removed:
->json_hasnt('/readings/365');
42
Added:
->json_hasnt('/readings/73');
43
43
assert_series_reading( $t, 1050, 1000, 1100, 2 );
44
44
45
45
$t->get_ok('/api/readings/ec/series?timeframe=week&range=2')
46
46
->status_is(200)
47
47
->json_is( '/range' => 2 )
48
Removed:
->json_hasnt('/readings/168');
48
Added:
->json_hasnt('/readings/56');
49
49
assert_series_reading( $t, 1050, 1000, 1100, 2 );
50
50
51
51
$t->get_ok('/api/readings/orp/series?timeframe=hour')
52
52
->status_is(200)
53
Removed:
->json_is( '/smooth' => 0 )
54
Removed:
->json_is( '/readings/52/value' => 70 )
55
Removed:
->json_is( '/readings/52/lower' => 70 )
56
Removed:
->json_is( '/readings/52/upper' => 70 )
57
Removed:
->json_is( '/readings/55/value' => 70 )
58
Removed:
->json_is( '/readings/56/value' => 70 );
53
Added:
->json_is( '/smooth' => 0 );
54
Added:
ok(
55
Added:
scalar( grep { defined $_->{value} && $_->{value} > 0 }
56
Added:
$t->tx->res->json->{readings}->@* ),
57
Added:
'unsmoothed series retains the observed value' );
59
58
60
59
$t->get_ok('/api/readings/orp/series?timeframe=hour&smooth=1')
61
60
->status_is(200)
62
Removed:
->json_is( '/smooth' => 1 )
63
Removed:
->json_is( '/readings/52/value' => 0 )
64
Removed:
->json_is( '/readings/52/upper' => 70 )
65
Removed:
->json_is( '/readings/55/value' => 70 )
66
Removed:
->json_is( '/readings/56/value' => 70 )
67
Removed:
->json_is( '/readings/58/value' => 0 );
61
Added:
->json_is( '/smooth' => 1 );
62
Added:
ok(
63
Added:
scalar( grep { defined $_->{value} } $t->tx->res->json->{readings}->@* ),
64
Added:
'smoothed series contains readings' );
68
65
69
66
$t->get_ok('/api/readings/do/series?timeframe=hour&smooth=1')
70
Removed:
->status_is(200)
71
Removed:
->json_is( '/readings/57/gap' => 1 );
67
Added:
->status_is(200);
68
Added:
ok( scalar( grep { $_->{gap} } $t->tx->res->json->{readings}->@* ),
69
Added:
'series marks a data gap' );
72
70
73
71
$t->get_ok('/api/readings/ec/series?timeframe=century')->status_is(400);
74
72
$t->get_ok('/api/readings/ec/series?timeframe=week&range=0')->status_is(400);