-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.
*
* 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
}
// 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;
});
}
+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',
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;
}
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}'`);
--- /dev/null
+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);
+ });
+ });
+});