mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-07 00:36:50 +01:00
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
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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}'`);
|
||||
|
||||
144
test/specs/plugin.decimation.tests.js
Normal file
144
test/specs/plugin.decimation.tests.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user