]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
LTTB Decimation (#8468)
authorEvert Timberg <evert.timberg+github@gmail.com>
Sun, 21 Feb 2021 14:15:45 +0000 (09:15 -0500)
committerGitHub <noreply@github.com>
Sun, 21 Feb 2021 14:15:45 +0000 (09:15 -0500)
* LTTB Decimation
* Lint fixes

docs/docs/configuration/decimation.md
src/plugins/plugin.decimation.js
types/index.esm.d.ts

index 3f741b75df166de865eb4fc1359b4e9a4c769a43..a9bfb4fb52e637eb3316015bd0113d8ba3b270b1 100644 (file)
@@ -12,13 +12,19 @@ Namespace: `options.plugins.decimation`, the global options for the plugin are d
 | ---- | ---- | ------- | -----------
 | `enabled` | `boolean` | `true` | Is decimation enabled?
 | `algorithm` | `string` | `'min-max'` | Decimation algorithm to use. See the [more...](#decimation-algorithms)
+| `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.
 
 ## Decimation Algorithms
 
 Decimation algorithm to use for data. Options are:
 
+* `'lttb'`
 * `'min-max'`
 
+### Largest Triangle Three Bucket (LTTB) Decimation
+
+[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.
+
 ### Min/Max Decimation
 
 [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.
index abe5e13efc13c30c8ccb38eb82fb4c79892b831f..d0204dc817d75c021bf9b1257cedfc6417fbd79e 100644 (file)
@@ -1,5 +1,73 @@
 import {isNullOrUndef, resolve} from '../helpers';
 
+function lttbDecimation(data, availableWidth, options) {
+  /**
+   * Implementation of the Largest Triangle Three Buckets algorithm.
+   *
+   * This implementation is based on the original implementation by Sveinn Steinarsson
+   * in https://github.com/sveinn-steinarsson/flot-downsample/blob/master/jquery.flot.downsample.js
+   *
+   * The original implementation is MIT licensed.
+   */
+  const samples = options.samples || availableWidth;
+  const decimated = [];
+
+  const bucketWidth = (data.length - 2) / (samples - 2);
+  let sampledIndex = 0;
+  let a = 0;
+  let i, maxAreaPoint, maxArea, area, nextA;
+  decimated[sampledIndex++] = data[a];
+
+  for (i = 0; i < samples - 2; i++) {
+    let avgX = 0;
+    let avgY = 0;
+    let j;
+    const avgRangeStart = Math.floor((i + 1) * bucketWidth) + 1;
+    const avgRangeEnd = Math.min(Math.floor((i + 2) * bucketWidth) + 1, data.length);
+    const avgRangeLength = avgRangeEnd - avgRangeStart;
+
+    for (j = avgRangeStart; j < avgRangeEnd; j++) {
+      avgX = data[j].x;
+      avgY = data[j].y;
+    }
+
+    avgX /= avgRangeLength;
+    avgY /= avgRangeLength;
+
+    const rangeOffs = Math.floor(i * bucketWidth) + 1;
+    const rangeTo = Math.floor((i + 1) * bucketWidth) + 1;
+    const {x: pointAx, y: pointAy} = data[a];
+
+    // Note that this is changed from the original algorithm which initializes these
+    // values to 1. The reason for this change is that if the area is small, nextA
+    // would never be set and thus a crash would occur in the next loop as `a` would become
+    // `undefined`. Since the area is always positive, but could be 0 in the case of a flat trace,
+    // initializing with a negative number is the correct solution.
+    maxArea = area = -1;
+
+    for (j = rangeOffs; j < rangeTo; j++) {
+      area = 0.5 * Math.abs(
+        (pointAx - avgX) * (data[j].y - pointAy) -
+        (pointAx - data[j].x) * (avgY - pointAy)
+      );
+
+      if (area > maxArea) {
+        maxArea = area;
+        maxAreaPoint = data[j];
+        nextA = j;
+      }
+    }
+
+    decimated[sampledIndex++] = maxAreaPoint;
+    a = nextA;
+  }
+
+  // Include the last point
+  decimated[sampledIndex++] = data[data.length - 1];
+
+  return decimated;
+}
+
 function minMaxDecimation(data, availableWidth) {
   let avgX = 0;
   let countX = 0;
@@ -141,6 +209,9 @@ export default {
       // Point the chart to the decimated data
       let decimated;
       switch (options.algorithm) {
+      case 'lttb':
+        decimated = lttbDecimation(data, availableWidth, options);
+        break;
       case 'min-max':
         decimated = minMaxDecimation(data, availableWidth);
         break;
index 5a7da61e6629f9bc363ec59d3371a6a7bcf5f0e7..9cc8e562421c353324cab0677d2e925a7cc872ea 100644 (file)
@@ -1920,14 +1920,24 @@ export class BasicPlatform extends BasePlatform {}
 export class DomPlatform extends BasePlatform {}
 
 export declare enum DecimationAlgorithm {
+       lttb = 'lttb',
        minmax = 'min-max',
 }
-
-export interface DecimationOptions {
+interface BaseDecimationOptions {
        enabled: boolean;
-       algorithm: DecimationAlgorithm;
 }
 
+interface LttbDecimationOptions extends BaseDecimationOptions {
+       algorithm: DecimationAlgorithm.lttb;
+       samples?: number;
+}
+
+interface MinMaxDecimationOptions extends BaseDecimationOptions {
+       algorithm: DecimationAlgorithm.minmax;
+}
+
+export type DecimationOptions = LttbDecimationOptions | MinMaxDecimationOptions;
+
 export const Filler: Plugin;
 export interface FillerOptions {
        propagate: boolean;