From: Jukka Kurkela Date: Fri, 15 Nov 2019 18:51:41 +0000 (+0200) Subject: Clean up element.line and do data decimation when applicable (#6731) X-Git-Tag: v3.0.0-alpha~232 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d610205e2963429d4df98c987389bee6cc1a95a3;p=thirdparty%2FChart.js.git Clean up element.line and do data decimation when applicable (#6731) Decimate line drawing to improve performance and reduce extraneous drawing --- diff --git a/docs/general/performance.md b/docs/general/performance.md index f6e17d008..a1b344a87 100644 --- a/docs/general/performance.md +++ b/docs/general/performance.md @@ -40,6 +40,7 @@ Decimating your data will achieve the best results. When there is a lot of data There are many approaches to data decimation and selection of an algorithm will depend on your data and the results you want to achieve. For instance, [min/max](https://digital.ni.com/public.nsf/allkb/F694FFEEA0ACF282862576020075F784) decimation will preserve peaks in your data but could require up to 4 points for each pixel. This type of decimation would work well for a very noisy signal where you need to see data peaks. +Line charts are able to do [automatic data decimation during draw](#automatic-data-decimation-during-draw), when certain conditions are met. You should still consider decimating data yourself before passing it in for maximum performance since the automatic decimation occurs late in the chart life cycle. ## Line Charts @@ -63,6 +64,28 @@ new Chart(ctx, { }); ``` +### Automatic data decimation during draw + +Line element will automatically decimate data, when the following conditions are met: `tension` is `0`, `steppedLine` is `false` (default), `fill` is `false` and `borderDash` is `[]` (default).` +This improves rendering speed by skipping drawing of invisible line segments. + +```javascript +new Chart(ctx, { + type: 'line', + data: data, + options: { + elements: { + line: { + tension: 0, // disables bezier curves + fill: false, + steppedLine: false, + borderDash: [] + } + } + } +}); +``` + ### Disable Line Drawing If you have a lot of data points, it can be more performant to disable rendering of the line for a dataset and only draw points. Doing this means that there is less to draw on the canvas which will improve render performance. @@ -82,3 +105,32 @@ new Chart(ctx, { } }); ``` + +### Disable Point Drawing + +If you have a lot of data points, it can be more performant to disable rendering of the points for a dataset and only draw line. Doing this means that there is less to draw on the canvas which will improve render performance. + +To disable point drawing: + +```javascript +new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + pointRadius: 0 // disable for a single dataset + }] + }, + options: { + datasets: { + line: { + pointRadius: 0 // disable for all `'line'` datasets + } + }, + elements: { + point: { + radius: 0 // default to disabled in all datasets + } + } + } +}); +``` diff --git a/src/elements/element.line.js b/src/elements/element.line.js index c67e8f567..3aa0c95a5 100644 --- a/src/elements/element.line.js +++ b/src/elements/element.line.js @@ -4,8 +4,6 @@ const defaults = require('../core/core.defaults'); const Element = require('../core/core.element'); const helpers = require('../helpers/index'); -const valueOrDefault = helpers.valueOrDefault; - const defaultColor = defaults.global.defaultColor; defaults._set('global', { @@ -25,6 +23,118 @@ defaults._set('global', { } }); +function startAtGap(points, spanGaps) { + let closePath = true; + let previous = points.length && points[0]._view; + let index, view; + + for (index = 1; index < points.length; ++index) { + // If there is a gap in the (looping) line, start drawing from that gap + view = points[index]._view; + if (!view.skip && previous.skip) { + points = points.slice(index).concat(points.slice(0, index)); + closePath = spanGaps; + break; + } + previous = view; + } + + points.closePath = closePath; + return points; +} + +function setStyle(ctx, vm) { + ctx.lineCap = vm.borderCapStyle; + ctx.setLineDash(vm.borderDash); + ctx.lineDashOffset = vm.borderDashOffset; + ctx.lineJoin = vm.borderJoinStyle; + ctx.lineWidth = vm.borderWidth; + ctx.strokeStyle = vm.borderColor; +} + +function normalPath(ctx, points, spanGaps) { + let move = true; + let index, currentVM, previousVM; + + for (index = 0; index < points.length; ++index) { + currentVM = points[index]._view; + + if (currentVM.skip) { + move = move || !spanGaps; + continue; + } + if (move) { + ctx.moveTo(currentVM.x, currentVM.y); + move = false; + } else { + helpers.canvas.lineTo(ctx, previousVM, currentVM); + } + previousVM = currentVM; + } +} + +/** + * Create path from points, grouping by truncated x-coordinate + * Points need to be in order by x-coordinate for this to work efficiently + * @param {CanvasRenderingContext2D} ctx - Context + * @param {Point[]} points - Points defining the line + * @param {boolean} spanGaps - Are gaps spanned over + */ +function fastPath(ctx, points, spanGaps) { + let move = true; + let count = 0; + let avgX = 0; + let index, vm, truncX, x, y, prevX, minY, maxY, lastY; + + for (index = 0; index < points.length; ++index) { + vm = points[index]._view; + + // If point is skipped, we either move to next (not skipped) point + // or line to it if spanGaps is true. `move` can already be true. + if (vm.skip) { + move = move || !spanGaps; + continue; + } + + x = vm.x; + y = vm.y; + truncX = x | 0; // truncated x-coordinate + + if (move) { + ctx.moveTo(x, y); + move = false; + } else if (truncX === prevX) { + // Determine `minY` / `maxY` and `avgX` while we stay within same x-position + minY = Math.min(y, minY); + maxY = Math.max(y, maxY); + // For first point in group, count is `0`, so average will be `x` / 1. + avgX = (count * avgX + x) / ++count; + } else { + if (minY !== maxY) { + // Draw line to maxY and minY, using the average x-coordinate + ctx.lineTo(avgX, maxY); + ctx.lineTo(avgX, minY); + // Move to y-value of last point in group. So the line continues + // from correct position. + ctx.moveTo(avgX, lastY); + } + // Draw line to next x-position, using the first (or only) + // y-value in that group + ctx.lineTo(x, y); + + prevX = truncX; + count = 0; + minY = maxY = y; + } + // Keep track of the last y-value in group + lastY = y; + } +} + +function useFastPath(vm) { + return vm.tension === 0 && !vm.steppedLine && !vm.fill && !vm.borderDash.length; +} + class Line extends Element { constructor(props) { @@ -32,77 +142,32 @@ class Line extends Element { } draw() { - var me = this; - var vm = me._view; - var ctx = me._ctx; - var spanGaps = vm.spanGaps; - var points = me._children; - var globalDefaults = defaults.global; - var globalOptionLineElements = globalDefaults.elements.line; - var lastDrawnIndex = -1; - var closePath = me._loop; - var index, previous, currentVM; + const me = this; + const vm = me._view; + const ctx = me._ctx; + const spanGaps = vm.spanGaps; + let closePath = me._loop; + let points = me._children; if (!points.length) { return; } - if (me._loop) { - points = points.slice(); // clone array - for (index = 0; index < points.length; ++index) { - previous = points[Math.max(0, index - 1)]; - // If the line has an open path, shift the point array - if (!points[index]._view.skip && previous._view.skip) { - points = points.slice(index).concat(points.slice(0, index)); - closePath = spanGaps; - break; - } - } - // If the line has a close path, add the first point again - if (closePath) { - points.push(points[0]); - } + if (closePath) { + points = startAtGap(points, spanGaps); + closePath = points.closePath; } ctx.save(); - // Stroke Line Options - ctx.lineCap = vm.borderCapStyle || globalOptionLineElements.borderCapStyle; - - // IE 9 and 10 do not support line dash - if (ctx.setLineDash) { - ctx.setLineDash(vm.borderDash || globalOptionLineElements.borderDash); - } - - ctx.lineDashOffset = valueOrDefault(vm.borderDashOffset, globalOptionLineElements.borderDashOffset); - ctx.lineJoin = vm.borderJoinStyle || globalOptionLineElements.borderJoinStyle; - ctx.lineWidth = valueOrDefault(vm.borderWidth, globalOptionLineElements.borderWidth); - ctx.strokeStyle = vm.borderColor || globalDefaults.defaultColor; + setStyle(ctx, vm); - // Stroke Line ctx.beginPath(); - // First point moves to its starting position no matter what - currentVM = points[0]._view; - if (!currentVM.skip) { - ctx.moveTo(currentVM.x, currentVM.y); - lastDrawnIndex = 0; - } - - for (index = 1; index < points.length; ++index) { - currentVM = points[index]._view; - previous = lastDrawnIndex === -1 ? points[index - 1] : points[lastDrawnIndex]; - - if (!currentVM.skip) { - if ((lastDrawnIndex !== (index - 1) && !spanGaps) || lastDrawnIndex === -1) { - // There was a gap and this is the first point after the gap - ctx.moveTo(currentVM.x, currentVM.y); - } else { - // Line to next point - helpers.canvas.lineTo(ctx, previous._view, currentVM); - } - lastDrawnIndex = index; - } + if (useFastPath(vm)) { + fastPath(ctx, points, spanGaps); + } else { + normalPath(ctx, points, spanGaps); } if (closePath) {