Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filtering data before decimation #8843

Merged
merged 4 commits into from Apr 7, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion 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 {
Expand Down Expand Up @@ -140,6 +140,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,
Expand Down
102 changes: 78 additions & 24 deletions 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.
*
Expand All @@ -10,32 +10,47 @@ 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;
let i, maxAreaPoint, maxArea, area, nextA;
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;
kurkle marked this conversation as resolved.
Show resolved Hide resolved
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
Expand All @@ -48,7 +63,7 @@ function lttbDecimation(data, availableWidth, options) {
for (j = rangeOffs; j < rangeTo; j++) {
area = 0.5 * Math.abs(
(pointAx - avgX) * (data[j].y - pointAy) -
(pointAx - data[j].x) * (avgY - pointAy)
(pointAx - data[j].x) * (avgY - pointAy),
kurkle marked this conversation as resolved.
Show resolved Hide resolved
);

if (area > maxArea) {
Expand All @@ -63,22 +78,32 @@ 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;
let i,
kurkle marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand Down Expand Up @@ -117,7 +142,7 @@ function minMaxDecimation(data, availableWidth) {
if (intermediateIndex2 !== startIndex && intermediateIndex2 !== lastIndex) {
decimated.push({
...data[intermediateIndex2],
x: avgX
x: avgX,
});
}
}
Expand Down Expand Up @@ -152,6 +177,32 @@ function cleanDecimatedData(chart) {
});
}

function getStartAndCountOfVisiblePointsSimplified(meta, points, animationsDisabled) {
const pointCount = points.length;

let start = 0;
let count;

const {iScale} = meta;
const axis = iScale.axis;
const {min, max, minDefined, maxDefined} = iScale.getUserBounds();

if (minDefined) {
start = _limitValue(Math.min(
_lookupByKey(points, iScale.axis, min).lo,
animationsDisabled ? pointCount : _lookupByKey(points, axis, iScale.getPixelForValue(min)).lo), 0, pointCount - 1);
kurkle marked this conversation as resolved.
Show resolved Hide resolved
}
if (maxDefined) {
count = _limitValue(Math.max(
_lookupByKey(points, iScale.axis, max).hi + 1,
animationsDisabled ? 0 : _lookupByKey(points, axis, iScale.getPixelForValue(max)).hi + 1), start, pointCount) - start;
} else {
count = pointCount - start;
}

return {start, count};
}

export default {
id: 'decimation',

Expand Down Expand Up @@ -196,7 +247,10 @@ export default {
return;
}

if (data.length <= 4 * availableWidth) {
// We know that the diagram is a line type
kurkle marked this conversation as resolved.
Show resolved Hide resolved
const animationsDisabled = chart._animationsDisabled;
let {start, count} = getStartAndCountOfVisiblePointsSimplified(meta, data, animationsDisabled);
if (count <= 4 * availableWidth) {
// No decimation is required until we are above this threshold
return;
}
Expand All @@ -215,18 +269,18 @@ export default {
},
set: function(d) {
this._data = d;
}
},
});
}

// Point the chart to the decimated data
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}'`);
Expand All @@ -238,5 +292,5 @@ export default {

destroy(chart) {
cleanDecimatedData(chart);
}
},
};
144 changes: 144 additions & 0 deletions 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);
});
});
});