From: Jukka Kurkela Date: Tue, 15 Sep 2020 22:57:31 +0000 (+0300) Subject: [perf] Update/draw only visible line/points (#7793) X-Git-Tag: v3.0.0-beta.2~1^2~6 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8cdc60ccd13c11d007ae7f09d226e043dc803e0a;p=thirdparty%2FChart.js.git [perf] Update/draw only visible line/points (#7793) * Restore count parameter to updateElements * [perf] Update/draw only visible line/points * CC --- diff --git a/docs/docs/getting-started/v3-migration.md b/docs/docs/getting-started/v3-migration.md index 7cb461098..de875bd8c 100644 --- a/docs/docs/getting-started/v3-migration.md +++ b/docs/docs/getting-started/v3-migration.md @@ -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 diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 0e663c983..30e092d89 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -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); } } diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index 386317b10..c0aceecd0 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -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); diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 78e79a4fe..389e46979 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -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); } diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index fae5d17a1..e5dbc5883 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -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}; +} diff --git a/src/controllers/controller.polarArea.js b/src/controllers/controller.polarArea.js index 60e5c042b..26d61ff7e 100644 --- a/src/controllers/controller.polarArea.js +++ b/src/controllers/controller.polarArea.js @@ -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); } } diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index 1c30e0c0f..19283338f 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -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); } } diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 7f5a958cc..a211216dd 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -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 diff --git a/src/elements/element.line.js b/src/elements/element.line.js index d01278d3e..778aeab57 100644 --- a/src/elements/element.line.js +++ b/src/elements/element.line.js @@ -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(); } diff --git a/test/fixtures/scale.time/source-labels-linear-offset-min-max.png b/test/fixtures/scale.time/source-labels-linear-offset-min-max.png index 474638882..29fae000f 100644 Binary files a/test/fixtures/scale.time/source-labels-linear-offset-min-max.png and b/test/fixtures/scale.time/source-labels-linear-offset-min-max.png differ diff --git a/test/fixtures/scale.time/ticks-reverse-linear-min-max.png b/test/fixtures/scale.time/ticks-reverse-linear-min-max.png index 8da6a228a..dbdfa5ed0 100644 Binary files a/test/fixtures/scale.time/ticks-reverse-linear-min-max.png and b/test/fixtures/scale.time/ticks-reverse-linear-min-max.png differ diff --git a/test/fixtures/scale.time/ticks-reverse-linear.png b/test/fixtures/scale.time/ticks-reverse-linear.png index ec0e7be09..b229b7785 100644 Binary files a/test/fixtures/scale.time/ticks-reverse-linear.png and b/test/fixtures/scale.time/ticks-reverse-linear.png differ diff --git a/types/core/index.d.ts b/types/core/index.d.ts index a497b245a..4a00823d4 100644 --- a/types/core/index.d.ts +++ b/types/core/index.d.ts @@ -338,7 +338,7 @@ export class DatasetController