[perf] Update/draw only visible line/points (#7793)

* Restore count parameter to updateElements
* [perf] Update/draw only visible line/points
* CC
This commit is contained in:
Jukka Kurkela 2020-09-16 01:57:31 +03:00 committed by GitHub
parent 17e27e16cc
commit 8cdc60ccd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 119 additions and 77 deletions

View File

@ -461,7 +461,7 @@ The APIs listed in this section have changed in signature or behaviour from vers
##### Dataset Controllers
* `updateElement` was replaced with `updateElements` now taking the elements to update, the `start` index, and `mode`
* `updateElement` was replaced with `updateElements` now taking the elements to update, the `start` index, `count`, and `mode`
* `setHoverStyle` and `removeHoverStyle` now additionally take the `datasetIndex` and `index`
#### Interactions

View File

@ -231,10 +231,10 @@ export default class BarController extends DatasetController {
const me = this;
const meta = me._cachedMeta;
me.updateElements(meta.data, 0, mode);
me.updateElements(meta.data, 0, meta.data.length, mode);
}
updateElements(rectangles, start, mode) {
updateElements(rectangles, start, count, mode) {
const me = this;
const reset = mode === 'reset';
const vscale = me._cachedMeta.vScale;
@ -247,11 +247,10 @@ export default class BarController extends DatasetController {
me.updateSharedOptions(sharedOptions, mode, firstOpts);
for (let i = 0; i < rectangles.length; i++) {
const index = start + i;
const options = sharedOptions || me.resolveDataElementOptions(index, mode);
const vpixels = me._calculateBarValuePixels(index, options);
const ipixels = me._calculateBarIndexPixels(index, ruler, options);
for (let i = start; i < start + count; i++) {
const options = sharedOptions || me.resolveDataElementOptions(i, mode);
const vpixels = me._calculateBarValuePixels(i, options);
const ipixels = me._calculateBarIndexPixels(i, ruler, options);
const properties = {
horizontal,
@ -265,7 +264,7 @@ export default class BarController extends DatasetController {
if (includeOptions) {
properties.options = options;
}
me.updateElement(rectangles[i], index, properties, mode);
me.updateElement(rectangles[i], i, properties, mode);
}
}

View File

@ -65,10 +65,10 @@ export default class BubbleController extends DatasetController {
const points = me._cachedMeta.data;
// Update Points
me.updateElements(points, 0, mode);
me.updateElements(points, 0, points.length, mode);
}
updateElements(points, start, mode) {
updateElements(points, start, count, mode) {
const me = this;
const reset = mode === 'reset';
const {xScale, yScale} = me._cachedMeta;
@ -76,10 +76,9 @@ export default class BubbleController extends DatasetController {
const sharedOptions = me.getSharedOptions(firstOpts);
const includeOptions = me.includeOptions(mode, sharedOptions);
for (let i = 0; i < points.length; i++) {
for (let i = start; i < start + count; i++) {
const point = points[i];
const index = start + i;
const parsed = !reset && me.getParsed(index);
const parsed = !reset && me.getParsed(i);
const x = reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(parsed.x);
const y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(parsed.y);
const properties = {
@ -89,14 +88,14 @@ export default class BubbleController extends DatasetController {
};
if (includeOptions) {
properties.options = me.resolveDataElementOptions(index, mode);
properties.options = me.resolveDataElementOptions(i, mode);
if (reset) {
properties.options.radius = 0;
}
}
me.updateElement(point, index, properties, mode);
me.updateElement(point, i, properties, mode);
}
me.updateSharedOptions(sharedOptions, mode, firstOpts);

View File

@ -104,7 +104,7 @@ export default class DoughnutController extends DatasetController {
me.outerRadius = outerRadius - radiusLength * me._getRingWeightOffset(me.index);
me.innerRadius = Math.max(me.outerRadius - radiusLength * chartWeight, 0);
me.updateElements(arcs, 0, mode);
me.updateElements(arcs, 0, arcs.length, mode);
}
/**
@ -117,7 +117,7 @@ export default class DoughnutController extends DatasetController {
return reset && opts.animation.animateRotate ? 0 : this.chart.getDataVisibility(i) ? me.calculateCircumference(meta._parsed[i] * opts.circumference / DOUBLE_PI) : 0;
}
updateElements(arcs, start, mode) {
updateElements(arcs, start, count, mode) {
const me = this;
const reset = mode === 'reset';
const chart = me.chart;
@ -139,9 +139,8 @@ export default class DoughnutController extends DatasetController {
startAngle += me._circumference(i, reset);
}
for (i = 0; i < arcs.length; ++i) {
const index = start + i;
const circumference = me._circumference(index, reset);
for (i = start; i < start + count; ++i) {
const circumference = me._circumference(i, reset);
const arc = arcs[i];
const properties = {
x: centerX + me.offsetX,
@ -153,11 +152,11 @@ export default class DoughnutController extends DatasetController {
innerRadius
};
if (includeOptions) {
properties.options = sharedOptions || me.resolveDataElementOptions(index, mode);
properties.options = sharedOptions || me.resolveDataElementOptions(i, mode);
}
startAngle += circumference;
me.updateElement(arc, index, properties, mode);
me.updateElement(arc, i, properties, mode);
}
me.updateSharedOptions(sharedOptions, mode, firstOpts);
}

View File

@ -2,6 +2,7 @@ import DatasetController from '../core/core.datasetController';
import {valueOrDefault} from '../helpers/helpers.core';
import {isNumber} from '../helpers/helpers.math';
import {resolve} from '../helpers/helpers.options';
import {_lookupByKey} from '../helpers/helpers.collection';
export default class LineController extends DatasetController {
@ -13,8 +14,11 @@ export default class LineController extends DatasetController {
update(mode) {
const me = this;
const meta = me._cachedMeta;
const line = meta.dataset;
const points = meta.data || [];
const {dataset: line, data: points = []} = meta;
const {start, count} = getStartAndCountOfVisiblePoints(meta, points);
me._drawStart = start;
me._drawCount = count;
// Update Line
// In resize mode only point locations change, so no need to set the points or options.
@ -28,10 +32,10 @@ export default class LineController extends DatasetController {
}
// Update Points
me.updateElements(points, 0, mode);
me.updateElements(points, start, count, mode);
}
updateElements(points, start, mode) {
updateElements(points, start, count, mode) {
const me = this;
const reset = mode === 'reset';
const {xScale, yScale, _stacked} = me._cachedMeta;
@ -40,14 +44,13 @@ export default class LineController extends DatasetController {
const includeOptions = me.includeOptions(mode, sharedOptions);
const spanGaps = valueOrDefault(me._config.spanGaps, me.chart.options.spanGaps);
const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY;
let prevParsed;
let prevParsed = start > 0 && me.getParsed(start - 1);
for (let i = 0; i < points.length; ++i) {
const index = start + i;
for (let i = start; i < start + count; ++i) {
const point = points[i];
const parsed = me.getParsed(index);
const x = xScale.getPixelForValue(parsed.x, index);
const y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(_stacked ? me.applyStack(yScale, parsed) : parsed.y, index);
const parsed = me.getParsed(i);
const x = xScale.getPixelForValue(parsed.x, i);
const y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(_stacked ? me.applyStack(yScale, parsed) : parsed.y, i);
const properties = {
x,
y,
@ -56,10 +59,10 @@ export default class LineController extends DatasetController {
};
if (includeOptions) {
properties.options = sharedOptions || me.resolveDataElementOptions(index, mode);
properties.options = sharedOptions || me.resolveDataElementOptions(i, mode);
}
me.updateElement(point, index, properties, mode);
me.updateElement(point, i, properties, mode);
prevParsed = parsed;
}
@ -167,3 +170,19 @@ LineController.defaults = {
},
}
};
function getStartAndCountOfVisiblePoints(meta, points) {
const pointCount = points.length;
let start = 0;
let count = pointCount;
if (meta._sorted) {
const {iScale, _parsed} = meta;
const {min, max, minDefined, maxDefined} = iScale.getUserBounds();
start = minDefined ? Math.max(0, _lookupByKey(_parsed, iScale.axis, min).lo) : 0;
count = (maxDefined ? Math.min(pointCount, _lookupByKey(_parsed, iScale.axis, max).hi + 1) : pointCount) - start;
}
return {start, count};
}

View File

@ -21,7 +21,7 @@ export default class PolarAreaController extends DatasetController {
const arcs = this._cachedMeta.data;
this._updateRadius();
this.updateElements(arcs, 0, mode);
this.updateElements(arcs, 0, arcs.length, mode);
}
/**
@ -42,7 +42,7 @@ export default class PolarAreaController extends DatasetController {
me.innerRadius = me.outerRadius - radiusLength;
}
updateElements(arcs, start, mode) {
updateElements(arcs, start, count, mode) {
const me = this;
const reset = mode === 'reset';
const chart = me.chart;
@ -61,12 +61,11 @@ export default class PolarAreaController extends DatasetController {
for (i = 0; i < start; ++i) {
angle += me._computeAngle(i);
}
for (i = 0; i < arcs.length; i++) {
for (i = start; i < start + count; i++) {
const arc = arcs[i];
const index = start + i;
let startAngle = angle;
let endAngle = angle + me._computeAngle(index);
let outerRadius = this.chart.getDataVisibility(index) ? scale.getDistanceFromCenterForValue(dataset.data[index]) : 0;
let endAngle = angle + me._computeAngle(i);
let outerRadius = this.chart.getDataVisibility(i) ? scale.getDistanceFromCenterForValue(dataset.data[i]) : 0;
angle = endAngle;
if (reset) {
@ -86,10 +85,10 @@ export default class PolarAreaController extends DatasetController {
outerRadius,
startAngle,
endAngle,
options: me.resolveDataElementOptions(index, mode)
options: me.resolveDataElementOptions(i, mode)
};
me.updateElement(arc, index, properties, mode);
me.updateElement(arc, i, properties, mode);
}
}

View File

@ -38,21 +38,19 @@ export default class RadarController extends DatasetController {
}
// Update Points
me.updateElements(points, 0, mode);
me.updateElements(points, 0, points.length, mode);
}
updateElements(points, start, mode) {
updateElements(points, start, count, mode) {
const me = this;
const dataset = me.getDataset();
const scale = me._cachedMeta.rScale;
const reset = mode === 'reset';
let i;
for (i = 0; i < points.length; i++) {
for (let i = start; i < start + count; i++) {
const point = points[i];
const index = start + i;
const options = me.resolveDataElementOptions(index, mode);
const pointPosition = scale.getPointPositionForValue(index, dataset.data[index]);
const options = me.resolveDataElementOptions(i, mode);
const pointPosition = scale.getPointPositionForValue(i, dataset.data[i]);
const x = reset ? scale.xCenter : pointPosition.x;
const y = reset ? scale.yCenter : pointPosition.y;
@ -65,7 +63,7 @@ export default class RadarController extends DatasetController {
options
};
me.updateElement(point, index, properties, mode);
me.updateElement(point, i, properties, mode);
}
}

View File

@ -179,6 +179,8 @@ export default class DatasetController {
this._data = undefined;
this._objectData = undefined;
this._sharedOptions = undefined;
this._drawStart = undefined;
this._drawCount = undefined;
this.enableOptionSharing = false;
this.initialize();
@ -380,11 +382,11 @@ export default class DatasetController {
parsed = me.parsePrimitiveData(meta, data, start, count);
}
const isNotInOrderComparedToPrev = () => isNaN(cur[iAxis]) || (prev && cur[iAxis] < prev[iAxis]);
for (i = 0; i < count; ++i) {
meta._parsed[i + start] = cur = parsed[i];
if (sorted) {
if (prev && cur[iAxis] < prev[iAxis]) {
if (isNotInOrderComparedToPrev()) {
sorted = false;
}
prev = cur;
@ -544,7 +546,7 @@ export default class DatasetController {
parsed = _parsed[i];
value = parsed[scale.axis];
otherValue = parsed[otherScale.axis];
return (isNaN(value) || otherMin > otherValue || otherMax < otherValue);
return (isNaN(value) || isNaN(otherValue) || otherMin > otherValue || otherMax < otherValue);
}
for (i = 0; i < ilen; ++i) {
@ -633,13 +635,15 @@ export default class DatasetController {
const elements = meta.data || [];
const area = chart.chartArea;
const active = [];
let i, ilen;
const start = me._drawStart || 0;
const count = me._drawCount || (elements.length - start);
let i;
if (meta.dataset) {
meta.dataset.draw(ctx, area);
meta.dataset.draw(ctx, area, start, count);
}
for (i = 0, ilen = elements.length; i < ilen; ++i) {
for (i = start; i < start + count; ++i) {
const element = elements[i];
if (element.active) {
active.push(element);
@ -648,7 +652,7 @@ export default class DatasetController {
}
}
for (i = 0, ilen = active.length; i < ilen; ++i) {
for (i = 0; i < active.length; ++i) {
active[i].draw(ctx, area);
}
}
@ -936,10 +940,10 @@ export default class DatasetController {
}
me.parse(start, count);
me.updateElements(elements, start, 'reset');
me.updateElements(data, start, count, 'reset');
}
updateElements(element, start, mode) {} // eslint-disable-line no-unused-vars
updateElements(element, start, count, mode) {} // eslint-disable-line no-unused-vars
/**
* @private

View File

@ -33,6 +33,20 @@ function getLineMethod(options) {
return lineTo;
}
function pathVars(points, segment, params) {
params = params || {};
const count = points.length;
const start = Math.max(params.start || 0, segment.start);
const end = Math.min(params.end || count - 1, segment.end);
return {
count,
start,
loop: segment.loop,
ilen: end < start ? count + end - start : end - start
};
}
/**
* Create path from points, grouping by truncated x-coordinate
* Points need to be in order by x-coordinate for this to work efficiently
@ -43,17 +57,17 @@ function getLineMethod(options) {
* @param {number} segment.end - end index of the segment, referring the points array
* @param {boolean} segment.loop - indicates that the segment is a loop
* @param {object} params
* @param {object} params.move - move to starting point (vs line to it)
* @param {object} params.reverse - path the segment from end to start
* @param {boolean} params.move - move to starting point (vs line to it)
* @param {boolean} params.reverse - path the segment from end to start
* @param {number} params.start - limit segment to points starting from `start` index
* @param {number} params.end - limit segment to points ending at `start` + `count` index
*/
function pathSegment(ctx, line, segment, params) {
const {start, end, loop} = segment;
const {points, options} = line;
const {count, start, loop, ilen} = pathVars(points, segment, params);
const lineMethod = getLineMethod(options);
const count = points.length;
// eslint-disable-next-line prefer-const
let {move = true, reverse} = params || {};
const ilen = end < start ? count + end - start : end - start;
let i, point, prev;
for (i = 0; i <= ilen; ++i) {
@ -90,15 +104,15 @@ function pathSegment(ctx, line, segment, params) {
* @param {number} segment.end - end index of the segment, referring the points array
* @param {boolean} segment.loop - indicates that the segment is a loop
* @param {object} params
* @param {object} params.move - move to starting point (vs line to it)
* @param {object} params.reverse - path the segment from end to start
* @param {boolean} params.move - move to starting point (vs line to it)
* @param {boolean} params.reverse - path the segment from end to start
* @param {number} params.start - limit segment to points starting from `start` index
* @param {number} params.end - limit segment to points ending at `start` + `count` index
*/
function fastPathSegment(ctx, line, segment, params) {
const points = line.points;
const count = points.length;
const {start, end} = segment;
const {count, start, ilen} = pathVars(points, segment, params);
const {move = true, reverse} = params || {};
const ilen = end < start ? count + end - start : end - start;
let avgX = 0;
let countX = 0;
let i, point, prevX, minY, maxY, lastY;
@ -290,8 +304,10 @@ export default class Line extends Element {
* @param {number} segment.end - end index of the segment, referring the points array
* @param {boolean} segment.loop - indicates that the segment is a loop
* @param {object} params
* @param {object} params.move - move to starting point (vs line to it)
* @param {object} params.reverse - path the segment from end to start
* @param {boolean} params.move - move to starting point (vs line to it)
* @param {boolean} params.reverse - path the segment from end to start
* @param {number} params.start - limit segment to points starting from `start` index
* @param {number} params.end - limit segment to points ending at `start` + `count` index
* @returns {undefined|boolean} - true if the segment is a full loop (path should be closed)
*/
pathSegment(ctx, segment, params) {
@ -302,16 +318,22 @@ export default class Line extends Element {
/**
* Append all segments of this line to current path.
* @param {CanvasRenderingContext2D} ctx
* @param {number} [start]
* @param {number} [count]
* @returns {undefined|boolean} - true if line is a full loop (path should be closed)
*/
path(ctx) {
path(ctx, start, count) {
const me = this;
const segments = me.segments;
const ilen = segments.length;
const segmentMethod = _getSegmentMethod(me);
let loop = me._loop;
start = start || 0;
count = count || (me.points.length - start);
for (let i = 0; i < ilen; ++i) {
loop &= segmentMethod(ctx, me, segments[i]);
loop &= segmentMethod(ctx, me, segments[i], {start, end: start + count - 1});
}
return !!loop;
}
@ -319,8 +341,11 @@ export default class Line extends Element {
/**
* Draw
* @param {CanvasRenderingContext2D} ctx
* @param {object} chartArea
* @param {number} [start]
* @param {number} [count]
*/
draw(ctx) {
draw(ctx, chartArea, start, count) {
const options = this.options || {};
const points = this.points || [];
@ -334,7 +359,7 @@ export default class Line extends Element {
ctx.beginPath();
if (this.path(ctx)) {
if (this.path(ctx, start, count)) {
ctx.closePath();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -338,7 +338,7 @@ export class DatasetController<E extends Element = Element, DSE extends Element
linkScales(): void;
getAllParsedValues(scale: Scale): number[];
protected getLabelAndValue(index: number): { label: string; value: string };
updateElements(elements: E[], start: number, mode: UpdateMode): void;
updateElements(elements: E[], start: number, count: number, mode: UpdateMode): void;
update(mode: UpdateMode): void;
updateIndex(datasetIndex: number): void;
protected getMaxOverflow(): boolean | number;