]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
[perf] Update/draw only visible line/points (#7793)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Tue, 15 Sep 2020 22:57:31 +0000 (01:57 +0300)
committerGitHub <noreply@github.com>
Tue, 15 Sep 2020 22:57:31 +0000 (18:57 -0400)
* Restore count parameter to updateElements
* [perf] Update/draw only visible line/points
* CC

13 files changed:
docs/docs/getting-started/v3-migration.md
src/controllers/controller.bar.js
src/controllers/controller.bubble.js
src/controllers/controller.doughnut.js
src/controllers/controller.line.js
src/controllers/controller.polarArea.js
src/controllers/controller.radar.js
src/core/core.datasetController.js
src/elements/element.line.js
test/fixtures/scale.time/source-labels-linear-offset-min-max.png
test/fixtures/scale.time/ticks-reverse-linear-min-max.png
test/fixtures/scale.time/ticks-reverse-linear.png
types/core/index.d.ts

index 7cb461098f3df2f399becb4106dbc15c0faefa9b..de875bd8cb50c9c4353a0ff84f48d375276f6165 100644 (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
index 0e663c983c79084a84c6cd356079f2ae159ac285..30e092d89012c5e4eaaf72c81879c96118dd7692 100644 (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);
                }
        }
 
index 386317b10a034e0891655f94a2ad1adc38986714..c0aceecd095178edde61cb79e723b9e38dad9bdd 100644 (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);
index 78e79a4fe9f9a644640306a501ba57b4b580f39e..389e4697900e7e7e12be3ad32957693d16974243 100644 (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);
        }
index fae5d17a143e6d0384505fe1ec5f60dc3c29c3e5..e5dbc588335ec40d661ebdfb5241349773b9464b 100644 (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};
+}
index 60e5c042b77b529ef52fe7a81d7f77e46ffb203b..26d61ff7e89b642a3222bd253e277547541af1cb 100644 (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);
                }
        }
 
index 1c30e0c0f99c0bc0dde7149a2d88cf90b68ab4ca..19283338f27a3ba8e623aee7ccdf2839ccb20b45 100644 (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);
                }
        }
 
index 7f5a958ccd9523980a2b5ede33501c2d8cf823ae..a211216dd5402046289d3f7cb551938ae3d0356d 100644 (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
index d01278d3ef367696dc4511429d4b49ac50a7dd02..778aeab57a0eead071636d1ba87878afa30c0664 100644 (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();
                }
 
index 47463888222a50fb8cae16a3c081bb75674dac04..29fae000ff0acf3fa477a233355b4bad724d9d3b 100644 (file)
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
index 8da6a228a80c6000be3a04ad8a1f84f2d46e1e78..dbdfa5ed0a5a75a2f9b78d9c84e9dfe8fed6a177 100644 (file)
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
index ec0e7be091607abf545a5665c03d6698fb13fbac..b229b7785286fa3e50475438917f2a2b98297b30 100644 (file)
Binary files a/test/fixtures/scale.time/ticks-reverse-linear.png and b/test/fixtures/scale.time/ticks-reverse-linear.png differ
index a497b245a3b4e1b4ec5e095623ced5fb0ccf521a..4a00823d4a276bcf06ed09d34e134553c13e2ea3 100644 (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;