Skip to content

Commit 5c9e1d5

Browse files
authored
LTTB Decimation (#8468)
* LTTB Decimation * Lint fixes
1 parent def8d25 commit 5c9e1d5

File tree

3 files changed

+90
-3
lines changed

3 files changed

+90
-3
lines changed

docs/docs/configuration/decimation.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@ Namespace: `options.plugins.decimation`, the global options for the plugin are d
1212
| ---- | ---- | ------- | -----------
1313
| `enabled` | `boolean` | `true` | Is decimation enabled?
1414
| `algorithm` | `string` | `'min-max'` | Decimation algorithm to use. See the [more...](#decimation-algorithms)
15+
| `samples` | `number` | | If the `'lttb'` algorithm is used, this is the number of samples in the output dataset. Defaults to the canvas width to pick 1 sample per pixel.
1516

1617
## Decimation Algorithms
1718

1819
Decimation algorithm to use for data. Options are:
1920

21+
* `'lttb'`
2022
* `'min-max'`
2123

24+
### Largest Triangle Three Bucket (LTTB) Decimation
25+
26+
[LTTB](https://github.com/sveinn-steinarsson/flot-downsample) decimation reduces the number of data points significantly. This is most useful for showing trends in data using only a few data points.
27+
2228
### Min/Max Decimation
2329

2430
[Min/max](https://digital.ni.com/public.nsf/allkb/F694FFEEA0ACF282862576020075F784) decimation will preserve peaks in your data but could require up to 4 points for each pixel. This type of decimation would work well for a very noisy signal where you need to see data peaks.

src/plugins/plugin.decimation.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,73 @@
11
import {isNullOrUndef, resolve} from '../helpers';
22

3+
function lttbDecimation(data, availableWidth, options) {
4+
/**
5+
* Implementation of the Largest Triangle Three Buckets algorithm.
6+
*
7+
* This implementation is based on the original implementation by Sveinn Steinarsson
8+
* in https://github.com/sveinn-steinarsson/flot-downsample/blob/master/jquery.flot.downsample.js
9+
*
10+
* The original implementation is MIT licensed.
11+
*/
12+
const samples = options.samples || availableWidth;
13+
const decimated = [];
14+
15+
const bucketWidth = (data.length - 2) / (samples - 2);
16+
let sampledIndex = 0;
17+
let a = 0;
18+
let i, maxAreaPoint, maxArea, area, nextA;
19+
decimated[sampledIndex++] = data[a];
20+
21+
for (i = 0; i < samples - 2; i++) {
22+
let avgX = 0;
23+
let avgY = 0;
24+
let j;
25+
const avgRangeStart = Math.floor((i + 1) * bucketWidth) + 1;
26+
const avgRangeEnd = Math.min(Math.floor((i + 2) * bucketWidth) + 1, data.length);
27+
const avgRangeLength = avgRangeEnd - avgRangeStart;
28+
29+
for (j = avgRangeStart; j < avgRangeEnd; j++) {
30+
avgX = data[j].x;
31+
avgY = data[j].y;
32+
}
33+
34+
avgX /= avgRangeLength;
35+
avgY /= avgRangeLength;
36+
37+
const rangeOffs = Math.floor(i * bucketWidth) + 1;
38+
const rangeTo = Math.floor((i + 1) * bucketWidth) + 1;
39+
const {x: pointAx, y: pointAy} = data[a];
40+
41+
// Note that this is changed from the original algorithm which initializes these
42+
// values to 1. The reason for this change is that if the area is small, nextA
43+
// would never be set and thus a crash would occur in the next loop as `a` would become
44+
// `undefined`. Since the area is always positive, but could be 0 in the case of a flat trace,
45+
// initializing with a negative number is the correct solution.
46+
maxArea = area = -1;
47+
48+
for (j = rangeOffs; j < rangeTo; j++) {
49+
area = 0.5 * Math.abs(
50+
(pointAx - avgX) * (data[j].y - pointAy) -
51+
(pointAx - data[j].x) * (avgY - pointAy)
52+
);
53+
54+
if (area > maxArea) {
55+
maxArea = area;
56+
maxAreaPoint = data[j];
57+
nextA = j;
58+
}
59+
}
60+
61+
decimated[sampledIndex++] = maxAreaPoint;
62+
a = nextA;
63+
}
64+
65+
// Include the last point
66+
decimated[sampledIndex++] = data[data.length - 1];
67+
68+
return decimated;
69+
}
70+
371
function minMaxDecimation(data, availableWidth) {
472
let avgX = 0;
573
let countX = 0;
@@ -141,6 +209,9 @@ export default {
141209
// Point the chart to the decimated data
142210
let decimated;
143211
switch (options.algorithm) {
212+
case 'lttb':
213+
decimated = lttbDecimation(data, availableWidth, options);
214+
break;
144215
case 'min-max':
145216
decimated = minMaxDecimation(data, availableWidth);
146217
break;

types/index.esm.d.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1920,14 +1920,24 @@ export class BasicPlatform extends BasePlatform {}
19201920
export class DomPlatform extends BasePlatform {}
19211921

19221922
export declare enum DecimationAlgorithm {
1923+
lttb = 'lttb',
19231924
minmax = 'min-max',
19241925
}
1925-
1926-
export interface DecimationOptions {
1926+
interface BaseDecimationOptions {
19271927
enabled: boolean;
1928-
algorithm: DecimationAlgorithm;
19291928
}
19301929

1930+
interface LttbDecimationOptions extends BaseDecimationOptions {
1931+
algorithm: DecimationAlgorithm.lttb;
1932+
samples?: number;
1933+
}
1934+
1935+
interface MinMaxDecimationOptions extends BaseDecimationOptions {
1936+
algorithm: DecimationAlgorithm.minmax;
1937+
}
1938+
1939+
export type DecimationOptions = LttbDecimationOptions | MinMaxDecimationOptions;
1940+
19311941
export const Filler: Plugin;
19321942
export interface FillerOptions {
19331943
propagate: boolean;

0 commit comments

Comments
 (0)