From: Nico-DF Date: Wed, 7 Apr 2021 20:40:45 +0000 (+0200) Subject: Filtering data before decimation (#8843) X-Git-Tag: v3.1.0~18 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ba3320ef1962b6f16ae08fca95ae86ecd7d8afc8;p=thirdparty%2FChart.js.git Filtering data before decimation (#8843) * Filtering data before decimation Using only points between the currently displayed x-axis for the decimation algorithm. Allows better resolution, especially if using a zoom If data are outside range, they will not be displayed, hence the line graph will not show the trend at extremities * Fix LTTB algorithm * Adding test file * Simplifying count algorithm for decimation plugin --- diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 53233baf7..142c11500 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -1,5 +1,5 @@ import DatasetController from '../core/core.datasetController'; -import {isNumber, _limitValue} from '../helpers/helpers.math'; +import {_limitValue, isNumber} from '../helpers/helpers.math'; import {_lookupByKey} from '../helpers/helpers.collection'; export default class LineController extends DatasetController { @@ -137,6 +137,7 @@ function getStartAndCountOfVisiblePoints(meta, points, animationsDisabled) { const {iScale, _parsed} = meta; const axis = iScale.axis; const {min, max, minDefined, maxDefined} = iScale.getUserBounds(); + if (minDefined) { start = _limitValue(Math.min( _lookupByKey(_parsed, iScale.axis, min).lo, diff --git a/src/plugins/plugin.decimation.js b/src/plugins/plugin.decimation.js index 988526413..f9bada37d 100644 --- a/src/plugins/plugin.decimation.js +++ b/src/plugins/plugin.decimation.js @@ -1,6 +1,6 @@ -import {isNullOrUndef, resolve} from '../helpers'; +import {_limitValue, _lookupByKey, isNullOrUndef, resolve} from '../helpers'; -function lttbDecimation(data, availableWidth, options) { +function lttbDecimation(data, start, count, availableWidth, options) { /** * Implementation of the Largest Triangle Three Buckets algorithm. * @@ -10,32 +10,43 @@ function lttbDecimation(data, availableWidth, options) { * The original implementation is MIT licensed. */ const samples = options.samples || availableWidth; + // There is less points than the threshold, returning the whole array + if (samples >= count) { + return data.slice(start, start + count); + } + const decimated = []; - const bucketWidth = (data.length - 2) / (samples - 2); + const bucketWidth = (count - 2) / (samples - 2); let sampledIndex = 0; - let a = 0; + const endIndex = start + count - 1; + // Starting from offset + let a = start; 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); + + // Adding offset + const avgRangeStart = Math.floor((i + 1) * bucketWidth) + 1 + start; + const avgRangeEnd = Math.min(Math.floor((i + 2) * bucketWidth) + 1, count) + start; const avgRangeLength = avgRangeEnd - avgRangeStart; for (j = avgRangeStart; j < avgRangeEnd; j++) { - avgX = data[j].x; - avgY = data[j].y; + 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; + // Adding offset + const rangeOffs = Math.floor(i * bucketWidth) + 1 + start; + const rangeTo = Math.floor((i + 1) * bucketWidth) + 1 + start; const {x: pointAx, y: pointAy} = data[a]; // Note that this is changed from the original algorithm which initializes these @@ -63,22 +74,23 @@ function lttbDecimation(data, availableWidth, options) { } // Include the last point - decimated[sampledIndex++] = data[data.length - 1]; + decimated[sampledIndex++] = data[endIndex]; return decimated; } -function minMaxDecimation(data, availableWidth) { +function minMaxDecimation(data, start, count, availableWidth) { let avgX = 0; let countX = 0; let i, point, x, y, prevX, minIndex, maxIndex, startIndex, minY, maxY; const decimated = []; + const endIndex = start + count - 1; - const xMin = data[0].x; - const xMax = data[data.length - 1].x; + const xMin = data[start].x; + const xMax = data[endIndex].x; const dx = xMax - xMin; - for (i = 0; i < data.length; ++i) { + for (i = start; i < start + count; ++i) { point = data[i]; x = (point.x - xMin) / dx * availableWidth; y = point.y; @@ -152,6 +164,27 @@ function cleanDecimatedData(chart) { }); } +function getStartAndCountOfVisiblePointsSimplified(meta, points) { + const pointCount = points.length; + + let start = 0; + let count; + + const {iScale} = meta; + const {min, max, minDefined, maxDefined} = iScale.getUserBounds(); + + if (minDefined) { + start = _limitValue(_lookupByKey(points, iScale.axis, min).lo, 0, pointCount - 1); + } + if (maxDefined) { + count = _limitValue(_lookupByKey(points, iScale.axis, max).hi + 1, start, pointCount) - start; + } else { + count = pointCount - start; + } + + return {start, count}; +} + export default { id: 'decimation', @@ -196,7 +229,8 @@ export default { return; } - if (data.length <= 4 * availableWidth) { + let {start, count} = getStartAndCountOfVisiblePointsSimplified(meta, data); + if (count <= 4 * availableWidth) { // No decimation is required until we are above this threshold return; } @@ -223,10 +257,10 @@ export default { let decimated; switch (options.algorithm) { case 'lttb': - decimated = lttbDecimation(data, availableWidth, options); + decimated = lttbDecimation(data, start, count, availableWidth, options); break; case 'min-max': - decimated = minMaxDecimation(data, availableWidth); + decimated = minMaxDecimation(data, start, count, availableWidth); break; default: throw new Error(`Unsupported decimation algorithm '${options.algorithm}'`); diff --git a/test/specs/plugin.decimation.tests.js b/test/specs/plugin.decimation.tests.js new file mode 100644 index 000000000..11b7d6a4d --- /dev/null +++ b/test/specs/plugin.decimation.tests.js @@ -0,0 +1,144 @@ +describe('Plugin.decimation', function() { + + describe('auto', jasmine.fixture.specs('plugin.decimation')); + + describe('lttb', function() { + const originalData = [ + {x: 0, y: 0}, + {x: 1, y: 1}, + {x: 2, y: 2}, + {x: 3, y: 3}, + {x: 4, y: 4}, + {x: 5, y: 5}, + {x: 6, y: 6}, + {x: 7, y: 7}, + {x: 8, y: 8}, + {x: 9, y: 9}]; + + it('should draw all element if sample is greater than data', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: originalData, + label: 'dataset1' + }] + }, + scales: { + x: { + type: 'linear', + min: 0, + max: 9 + } + }, + options: { + plugins: { + decimation: { + enabled: true, + algorithm: 'lttb', + samples: 100 + } + } + } + }, { + canvas: { + height: 1, + width: 1 + }, + wrapper: { + height: 1, + width: 1 + } + }); + + expect(chart.data.datasets[0].data.length).toBe(10); + }); + + it('should draw the specified number of elements', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: originalData, + label: 'dataset1' + }] + }, + options: { + parsing: false, + scales: { + x: { + type: 'linear', + min: 0, + max: 9 + } + }, + plugins: { + decimation: { + enabled: true, + algorithm: 'lttb', + samples: 7 + } + } + } + }, { + canvas: { + height: 1, + width: 1 + }, + wrapper: { + height: 1, + width: 1 + } + }); + + expect(chart.data.datasets[0].data.length).toBe(7); + }); + + it('should draw all element only in range', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: originalData, + label: 'dataset1' + }] + }, + options: { + parsing: false, + scales: { + x: { + type: 'linear', + min: 3, + max: 6 + } + }, + plugins: { + decimation: { + enabled: true, + algorithm: 'lttb', + samples: 7 + } + } + } + }, { + canvas: { + height: 1, + width: 1 + }, + wrapper: { + height: 1, + width: 1 + } + }); + + // Data range is 4 (3->6) and the first point is added + const expectedPoints = 5; + expect(chart.data.datasets[0].data.length).toBe(expectedPoints); + expect(chart.data.datasets[0].data[0].x).toBe(originalData[2].x); + expect(chart.data.datasets[0].data[1].x).toBe(originalData[3].x); + expect(chart.data.datasets[0].data[2].x).toBe(originalData[4].x); + expect(chart.data.datasets[0].data[3].x).toBe(originalData[5].x); + expect(chart.data.datasets[0].data[4].x).toBe(originalData[6].x); + }); + }); +});