]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Clean up element.line and do data decimation when applicable (#6731)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Fri, 15 Nov 2019 18:51:41 +0000 (20:51 +0200)
committerEvert Timberg <evert.timberg+github@gmail.com>
Fri, 15 Nov 2019 18:51:41 +0000 (13:51 -0500)
Decimate line drawing to improve performance and reduce extraneous drawing

docs/general/performance.md
src/elements/element.line.js

index f6e17d0086fab22f7422b95bce26c9183b0d705a..a1b344a8766c87726900ed77d948d494ae5a62ac 100644 (file)
@@ -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
+            }
+        }
+    }
+});
+```
index c67e8f5677545326d28881a2221554ca7bd6d21e..3aa0c95a58499bd73861418027a67547c26168ff 100644 (file)
@@ -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) {