]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Rewrite filler (#6795)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Tue, 31 Dec 2019 17:56:15 +0000 (19:56 +0200)
committerEvert Timberg <evert.timberg+github@gmail.com>
Tue, 31 Dec 2019 17:56:15 +0000 (12:56 -0500)
Filler plugin is rewritten and test coverage increased

26 files changed:
docs/general/performance.md
src/controllers/controller.line.js
src/controllers/controller.radar.js
src/core/core.scale.js
src/elements/element.line.js
src/helpers/helpers.canvas.js
src/helpers/helpers.curve.js
src/helpers/helpers.interpolation.js [new file with mode: 0644]
src/helpers/helpers.math.js
src/helpers/helpers.segment.js [new file with mode: 0644]
src/plugins/plugin.filler.js
src/scales/scale.radialLinear.js
test/fixtures/plugin.filler/fill-line-dataset-interpolated.js [new file with mode: 0644]
test/fixtures/plugin.filler/fill-line-dataset-interpolated.png [new file with mode: 0644]
test/fixtures/plugin.filler/fill-line-dataset-span-dual.png
test/fixtures/plugin.filler/fill-line-dataset-span.png
test/fixtures/plugin.filler/fill-line-dataset-spline-span-below.png
test/fixtures/plugin.filler/fill-line-dataset-spline-span.png
test/fixtures/plugin.filler/fill-line-dataset-stepped.json [new file with mode: 0644]
test/fixtures/plugin.filler/fill-line-dataset-stepped.png [new file with mode: 0644]
test/fixtures/plugin.filler/fill-radar-dataset-border.png
test/fixtures/plugin.filler/fill-radar-dataset-span.png
test/fixtures/plugin.filler/fill-radar-dataset-spline.png
test/fixtures/plugin.filler/fill-radar-dataset.png
test/specs/element.line.tests.js
test/specs/helpers.math.tests.js

index 1d970a6f9bbb0ff864246bb5980daa28ceb423f6..3e5669b121adcdaf365852b2d8d0b9b8936edf63 100644 (file)
@@ -89,7 +89,7 @@ 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).`
+Line element will automatically decimate data, when the following conditions are met: `tension` is `0`, `steppedLine` is `false` (default) and `borderDash` is `[]` (default).`
 This improves rendering speed by skipping drawing of invisible line segments.
 
 ```javascript
@@ -109,6 +109,26 @@ new Chart(ctx, {
 });
 ```
 
+### Enable spanGaps
+
+If you have a lot of data points, it can be more performant to enable `spanGaps`. This disables segmentation of the line, which can be an unneeded step.
+
+To enable `spanGaps`:
+
+```javascript
+new Chart(ctx, {
+    type: 'line',
+    data: {
+        datasets: [{
+            spanGaps: true // enable for a single dataset
+        }]
+    },
+    options: {
+        spanGaps: true // enable for all datasets
+    }
+});
+```
+
 ### 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.
index 9713d3b5d38d8ef43061af52efdfe4e44891485f..dd8eded5bf0eb5a59fbf0e1c7c65e8f344de47e5 100644 (file)
@@ -77,9 +77,8 @@ module.exports = DatasetController.extend({
 
                // Update Line
                if (showLine && mode !== 'resize') {
-
                        const properties = {
-                               _children: points,
+                               points,
                                options: me._resolveDatasetElementOptions()
                        };
 
index 617677ce2f84228101c55a8a4373de81f1f9b4ba..ab1f18c1cd1426a95caf5e74cb4f4c3d9d8b6f24 100644 (file)
@@ -90,10 +90,11 @@ module.exports = DatasetController.extend({
                const meta = me._cachedMeta;
                const line = meta.dataset;
                const points = meta.data || [];
-
+               const labels = meta.iScale._getLabels();
                const properties = {
-                       _children: points,
+                       points,
                        _loop: true,
+                       _fullLoop: labels.length === points.length,
                        options: me._resolveDatasetElementOptions()
                };
 
@@ -122,10 +123,11 @@ module.exports = DatasetController.extend({
                        const y = reset ? scale.yCenter : pointPosition.y;
 
                        const properties = {
-                               x: x,
-                               y: y,
+                               x,
+                               y,
+                               angle: pointPosition.angle,
                                skip: isNaN(x) || isNaN(y),
-                               options,
+                               options
                        };
 
                        me._updateElement(point, index, properties, mode);
index 164ee564a6def99e1b6d8aae280097f31b6afd5a..db7c10aed81b15ca6102fa48638e28ce5a79d047 100644 (file)
@@ -876,12 +876,9 @@ class Scale extends Element {
        }
 
        getBaseValue() {
-               var me = this;
-               var min = me.min;
-               var max = me.max;
+               const {min, max} = this;
 
-               return me.beginAtZero ? 0 :
-                       min < 0 && max < 0 ? max :
+               return min < 0 && max < 0 ? max :
                        min > 0 && max > 0 ? min :
                        0;
        }
index 67bd08e1e3439b06220164d4d3f7a94857360b6f..18b730ee3e8c297fa327ccc26683479faa083376 100644 (file)
@@ -2,10 +2,12 @@
 
 import defaults from '../core/core.defaults';
 import Element from '../core/core.element';
-import helpers from '../helpers';
+import {_bezierInterpolation, _pointInLine, _steppedInterpolation} from '../helpers/helpers.interpolation';
+import {_computeSegments, _boundSegments} from '../helpers/helpers.segment';
+import {_steppedLineTo, _bezierCurveTo} from '../helpers/helpers.canvas';
+import {_updateBezierControlPoints} from '../helpers/helpers.curve';
 
 const defaultColor = defaults.global.defaultColor;
-const isPointInArea = helpers.canvas._isPointInArea;
 
 defaults._set('global', {
        elements: {
@@ -24,134 +26,141 @@ defaults._set('global', {
        }
 });
 
-function startAtGap(points, spanGaps) {
-       let closePath = true;
-       let previous = points.length && points[0];
-       let index, point;
-
-       for (index = 1; index < points.length; ++index) {
-               // If there is a gap in the (looping) line, start drawing from that gap
-               point = points[index];
-               if (!point.skip && previous.skip) {
-                       points = points.slice(index).concat(points.slice(0, index));
-                       closePath = spanGaps;
-                       break;
-               }
-               previous = point;
-       }
-
-       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 setStyle(ctx, options) {
-       ctx.lineCap = options.borderCapStyle;
-       ctx.setLineDash(options.borderDash);
-       ctx.lineDashOffset = options.borderDashOffset;
-       ctx.lineJoin = options.borderJoinStyle;
-       ctx.lineWidth = options.borderWidth;
-       ctx.strokeStyle = options.borderColor;
-}
-
-function bezierCurveTo(ctx, previous, target, flip) {
-       ctx.bezierCurveTo(
-               flip ? previous.controlPointPreviousX : previous.controlPointNextX,
-               flip ? previous.controlPointPreviousY : previous.controlPointNextY,
-               flip ? target.controlPointNextX : target.controlPointPreviousX,
-               flip ? target.controlPointNextY : target.controlPointPreviousY,
-               target.x,
-               target.y);
+function lineTo(ctx, previous, target) {
+       ctx.lineTo(target.x, target.y);
 }
 
-function steppedLineTo(ctx, previous, target, flip, mode) {
-       if (mode === 'middle') {
-               const midpoint = (previous.x + target.x) / 2.0;
-               ctx.lineTo(midpoint, flip ? target.y : previous.y);
-               ctx.lineTo(midpoint, flip ? previous.y : target.y);
-       } else if ((mode === 'after' && !flip) || (mode !== 'after' && flip)) {
-               ctx.lineTo(previous.x, target.y);
-       } else {
-               ctx.lineTo(target.x, previous.y);
+function getLineMethod(options) {
+       if (options.steppedLine) {
+               return _steppedLineTo;
        }
-       ctx.lineTo(target.x, target.y);
-}
 
-function normalPath(ctx, points, spanGaps, options) {
-       const steppedLine = options.steppedLine;
-       const lineMethod = steppedLine ? steppedLineTo : bezierCurveTo;
-       let move = true;
-       let index, currentVM, previousVM;
+       if (options.tension) {
+               return _bezierCurveTo;
+       }
 
-       for (index = 0; index < points.length; ++index) {
-               currentVM = points[index];
+       return lineTo;
+}
 
-               if (currentVM.skip) {
-                       move = move || !spanGaps;
+/**
+ * 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 {Line} line
+ * @param {object} segment
+ * @param {number} segment.start - start index of the segment, referring the points array
+ * @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
+ */
+function pathSegment(ctx, line, segment, params) {
+       const {start, end, loop} = segment;
+       const {points, options} = line;
+       const lineMethod = getLineMethod(options);
+       const count = points.length;
+       let {move = true, reverse} = params || {};
+       let ilen = end < start ? count + end - start : end - start;
+       let i, point, prev;
+
+       for (i = 0; i <= ilen; ++i) {
+               point = points[(start + (reverse ? ilen - i : i)) % count];
+
+               if (point.skip) {
+                       // If there is a skipped point inside a segment, spanGaps must be true
                        continue;
-               }
-               if (move) {
-                       ctx.moveTo(currentVM.x, currentVM.y);
+               } else if (move) {
+                       ctx.moveTo(point.x, point.y);
                        move = false;
-               } else if (options.tension || steppedLine) {
-                       lineMethod(ctx, previousVM, currentVM, false, steppedLine);
                } else {
-                       ctx.lineTo(currentVM.x, currentVM.y);
+                       lineMethod(ctx, prev, point, reverse, options.steppedLine);
                }
-               previousVM = currentVM;
+
+               prev = point;
        }
+
+       if (loop) {
+               point = points[(start + (reverse ? ilen : 0)) % count];
+               lineMethod(ctx, prev, point, reverse, options.steppedLine);
+       }
+
+       return !!loop;
 }
 
 /**
  * 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
+ * @param {Line} line
+ * @param {object} segment
+ * @param {number} segment.start - start index of the segment, referring the points array
+ * @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
  */
-function fastPath(ctx, points, spanGaps) {
-       let move = true;
-       let count = 0;
+function fastPathSegment(ctx, line, segment, params) {
+       const points = line.points;
+       const count = points.length;
+       const {start, end} = segment;
+       let {move = true, reverse} = params || {};
+       let ilen = end < start ? count + end - start : end - start;
        let avgX = 0;
-       let index, vm, truncX, x, y, prevX, minY, maxY, lastY;
+       let countX = 0;
+       let i, point, prevX, minY, maxY, lastY;
 
-       for (index = 0; index < points.length; ++index) {
-               vm = points[index];
+       if (move) {
+               point = points[(start + (reverse ? ilen : 0)) % count];
+               ctx.moveTo(point.x, point.y);
+       }
+
+       for (i = 0; i <= ilen; ++i) {
+               point = points[(start + (reverse ? ilen - i : i)) % count];
 
-               // 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;
+               if (point.skip) {
+                       // If there is a skipped point inside a segment, spanGaps must be true
                        continue;
                }
 
-               x = vm.x;
-               y = vm.y;
-               truncX = x | 0; // truncated x-coordinate
+               const x = point.x;
+               const y = point.y;
+               const truncX = x | 0; // truncated x-coordinate
 
-               if (move) {
-                       ctx.moveTo(x, y);
-                       move = false;
-               } else if (truncX === prevX) {
+               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;
+                       if (y < minY) {
+                               minY = y;
+                       } else if (y > maxY) {
+                               maxY = y;
+                       }
+                       // For first point in group, countX is `0`, so average will be `x` / 1.
+                       avgX = (countX * avgX + x) / ++countX;
                } 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);
+                               // Line to y-value of last point in group. So the line continues
+                               // from correct position. Not using move, to have solid path.
+                               ctx.lineTo(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;
+                       countX = 0;
                        minY = maxY = y;
                }
                // Keep track of the last y-value in group
@@ -159,64 +168,23 @@ function fastPath(ctx, points, spanGaps) {
        }
 }
 
-function useFastPath(options) {
-       return options.tension === 0 && !options.steppedLine && !options.fill && !options.borderDash.length;
-}
-
-function capControlPoint(pt, min, max) {
-       return Math.max(Math.min(pt, max), min);
+function _getSegmentMethod(line) {
+       const opts = line.options;
+       const borderDash = opts.borderDash && opts.borderDash.length;
+       const useFastPath = !line._loop && !opts.tension && !opts.steppedLine && !borderDash;
+       return useFastPath ? fastPathSegment : pathSegment;
 }
 
-function capBezierPoints(points, area) {
-       var i, ilen, model;
-       for (i = 0, ilen = points.length; i < ilen; ++i) {
-               model = points[i];
-               if (isPointInArea(model, area)) {
-                       if (i > 0 && isPointInArea(points[i - 1], area)) {
-                               model.controlPointPreviousX = capControlPoint(model.controlPointPreviousX, area.left, area.right);
-                               model.controlPointPreviousY = capControlPoint(model.controlPointPreviousY, area.top, area.bottom);
-                       }
-                       if (i < points.length - 1 && isPointInArea(points[i + 1], area)) {
-                               model.controlPointNextX = capControlPoint(model.controlPointNextX, area.left, area.right);
-                               model.controlPointNextY = capControlPoint(model.controlPointNextY, area.top, area.bottom);
-                       }
-               }
+function _getInterpolationMethod(options) {
+       if (options.steppedLine) {
+               return _steppedInterpolation;
        }
-}
 
-function updateBezierControlPoints(points, options, area, loop) {
-       var i, ilen, point, controlPoints;
-
-       // Only consider points that are drawn in case the spanGaps option is used
-       if (options.spanGaps) {
-               points = points.filter(function(pt) {
-                       return !pt.skip;
-               });
+       if (options.tension) {
+               return _bezierInterpolation;
        }
 
-       if (options.cubicInterpolationMode === 'monotone') {
-               helpers.curve.splineCurveMonotone(points);
-       } else {
-               let prev = loop ? points[points.length - 1] : points[0];
-               for (i = 0, ilen = points.length; i < ilen; ++i) {
-                       point = points[i];
-                       controlPoints = helpers.curve.splineCurve(
-                               prev,
-                               point,
-                               points[Math.min(i + 1, ilen - (loop ? 0 : 1)) % ilen],
-                               options.tension
-                       );
-                       point.controlPointPreviousX = controlPoints.previous.x;
-                       point.controlPointPreviousY = controlPoints.previous.y;
-                       point.controlPointNextX = controlPoints.next.x;
-                       point.controlPointNextY = controlPoints.next.y;
-                       prev = point;
-               }
-       }
-
-       if (options.capBezierPoints) {
-               capBezierPoints(points, area);
-       }
+       return _pointInLine;
 }
 
 class Line extends Element {
@@ -232,40 +200,127 @@ class Line extends Element {
                }
                const options = me.options;
                if (options.tension && !options.steppedLine) {
-                       updateBezierControlPoints(me._children, options, chartArea, me._loop);
+                       const loop = options.spanGaps ? me._loop : me._fullLoop;
+                       _updateBezierControlPoints(me._points, options, chartArea, loop);
                }
        }
 
-       drawPath(ctx, area) {
+       set points(points) {
+               this._points = points;
+               delete this._segments;
+       }
+
+       get points() {
+               return this._points;
+       }
+
+       get segments() {
+               return this._segments || (this._segments = _computeSegments(this));
+       }
+
+       /**
+        * First non-skipped point on this line
+        * @returns {Point|undefined}
+        */
+       first() {
+               const segments = this.segments;
+               const points = this.points;
+               return segments.length && points[segments[0].start];
+       }
+
+       /**
+        * Last non-skipped point on this line
+        * @returns {Point|undefined}
+        */
+       last() {
+               const segments = this.segments;
+               const points = this.points;
+               const count = segments.length;
+               return count && points[segments[count - 1].end];
+       }
+
+       /**
+        * Interpolate a point in this line at the same value on `property` as
+        * the reference `point` provided
+        * @param {Point} point - the reference point
+        * @param {string} property - the property to match on
+        * @returns {Point|undefined}
+        */
+       interpolate(point, property) {
                const me = this;
                const options = me.options;
-               const spanGaps = options.spanGaps;
-               let closePath = me._loop;
-               let points = me._children;
+               const value = point[property];
+               const points = me.points;
+               const segments = _boundSegments(me, {property, start: value, end: value});
 
-               if (!points.length) {
+               if (!segments.length) {
                        return;
                }
 
-               if (closePath) {
-                       points = startAtGap(points, spanGaps);
-                       closePath = points.closePath;
+               const result = [];
+               const _interpolate = _getInterpolationMethod(options);
+               let i, ilen;
+               for (i = 0, ilen = segments.length; i < ilen; ++i) {
+                       const {start, end} = segments[i];
+                       const p1 = points[start];
+                       const p2 = points[end];
+                       if (p1 === p2) {
+                               result.push(p1);
+                               continue;
+                       }
+                       const t = Math.abs((value - p1[property]) / (p2[property] - p1[property]));
+                       let interpolated = _interpolate(p1, p2, t, options.steppedLine);
+                       interpolated[property] = point[property];
+                       result.push(interpolated);
                }
+               return result.lenght === 1 ? result[0] : result;
+       }
 
-               if (useFastPath(options)) {
-                       fastPath(ctx, points, spanGaps);
-               } else {
-                       me.updateControlPoints(area);
-                       normalPath(ctx, points, spanGaps, options);
-               }
+       /**
+        * Append a segment of this line to current path.
+        * @param {CanvasRenderingContext2D} ctx
+        * @param {object} segment
+        * @param {number} segment.start - start index of the segment, referring the points array
+        * @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
+        * @returns {undefined|boolean} - true if the segment is a full loop (path should be closed)
+        */
+       pathSegment(ctx, segment, params) {
+               const segmentMethod = _getSegmentMethod(this);
+               return segmentMethod(ctx, this, segment, params);
+       }
 
-               return closePath;
+       /**
+        * Append all segments of this line to current path.
+        * @param {CanvasRenderingContext2D} ctx
+        * @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
+        * @returns {undefined|boolean} - true if line is a full loop (path should be closed)
+        */
+       path(ctx, params) {
+               const me = this;
+               const segments = me.segments;
+               const ilen = segments.length;
+               const segmentMethod = _getSegmentMethod(me);
+               let loop = me._loop;
+               for (let i = 0; i < ilen; ++i) {
+                       loop &= segmentMethod(ctx, me, segments[i], params);
+               }
+               return !!loop;
        }
 
-       draw(ctx, area) {
+       /**
+        * Draw
+        * @param {CanvasRenderingContext2D} ctx
+        */
+       draw(ctx) {
                const me = this;
 
-               if (!me._children.length) {
+               if (!me.points.length) {
                        return;
                }
 
@@ -275,7 +330,7 @@ class Line extends Element {
 
                ctx.beginPath();
 
-               if (me.drawPath(ctx, area)) {
+               if (me.path(ctx)) {
                        ctx.closePath();
                }
 
index 8c3b92e09b59d6e22ff55f52b2473ddb9ca4c59d..481eb6c0122b7858879c3fc5398764549a658119 100644 (file)
@@ -174,11 +174,14 @@ export function unclipArea(ctx) {
  * @private
  */
 export function _steppedLineTo(ctx, previous, target, flip, mode) {
+       if (!previous) {
+               return ctx.lineTo(target.x, target.y);
+       }
        if (mode === 'middle') {
                const midpoint = (previous.x + target.x) / 2.0;
-               ctx.lineTo(midpoint, flip ? target.y : previous.y);
-               ctx.lineTo(midpoint, flip ? previous.y : target.y);
-       } else if ((mode === 'after' && !flip) || (mode !== 'after' && flip)) {
+               ctx.lineTo(midpoint, previous.y);
+               ctx.lineTo(midpoint, target.y);
+       } else if (mode === 'after' ^ flip) {
                ctx.lineTo(previous.x, target.y);
        } else {
                ctx.lineTo(target.x, previous.y);
@@ -190,6 +193,9 @@ export function _steppedLineTo(ctx, previous, target, flip, mode) {
  * @private
  */
 export function _bezierCurveTo(ctx, previous, target, flip) {
+       if (!previous) {
+               return ctx.lineTo(target.x, target.y);
+       }
        ctx.bezierCurveTo(
                flip ? previous.controlPointPreviousX : previous.controlPointNextX,
                flip ? previous.controlPointPreviousY : previous.controlPointNextY,
index d4bf75823bc6d811592b5edd11d889104ce150fa..58e0952b993b5993a0cc85c5a73c1763e3154042 100644 (file)
@@ -1,4 +1,5 @@
 import {almostEquals, sign} from './helpers.math';
+import {_isPointInArea} from './helpers.canvas';
 
 const EPSILON = Number.EPSILON || 1e-14;
 
@@ -128,3 +129,60 @@ export function splineCurveMonotone(points) {
                }
        }
 }
+
+function capControlPoint(pt, min, max) {
+       return Math.max(Math.min(pt, max), min);
+}
+
+function capBezierPoints(points, area) {
+       var i, ilen, point;
+       for (i = 0, ilen = points.length; i < ilen; ++i) {
+               point = points[i];
+               if (!_isPointInArea(point, area)) {
+                       continue;
+               }
+               if (i > 0 && _isPointInArea(points[i - 1], area)) {
+                       point.controlPointPreviousX = capControlPoint(point.controlPointPreviousX, area.left, area.right);
+                       point.controlPointPreviousY = capControlPoint(point.controlPointPreviousY, area.top, area.bottom);
+               }
+               if (i < points.length - 1 && _isPointInArea(points[i + 1], area)) {
+                       point.controlPointNextX = capControlPoint(point.controlPointNextX, area.left, area.right);
+                       point.controlPointNextY = capControlPoint(point.controlPointNextY, area.top, area.bottom);
+               }
+       }
+}
+
+export function _updateBezierControlPoints(points, options, area, loop) {
+       var i, ilen, point, controlPoints;
+
+       // Only consider points that are drawn in case the spanGaps option is used
+       if (options.spanGaps) {
+               points = points.filter(function(pt) {
+                       return !pt.skip;
+               });
+       }
+
+       if (options.cubicInterpolationMode === 'monotone') {
+               splineCurveMonotone(points);
+       } else {
+               let prev = loop ? points[points.length - 1] : points[0];
+               for (i = 0, ilen = points.length; i < ilen; ++i) {
+                       point = points[i];
+                       controlPoints = splineCurve(
+                               prev,
+                               point,
+                               points[Math.min(i + 1, ilen - (loop ? 0 : 1)) % ilen],
+                               options.tension
+                       );
+                       point.controlPointPreviousX = controlPoints.previous.x;
+                       point.controlPointPreviousY = controlPoints.previous.y;
+                       point.controlPointNextX = controlPoints.next.x;
+                       point.controlPointNextY = controlPoints.next.y;
+                       prev = point;
+               }
+       }
+
+       if (options.capBezierPoints) {
+               capBezierPoints(points, area);
+       }
+}
diff --git a/src/helpers/helpers.interpolation.js b/src/helpers/helpers.interpolation.js
new file mode 100644 (file)
index 0000000..c8400b4
--- /dev/null
@@ -0,0 +1,37 @@
+'use strict';
+
+/**
+ * @private
+ */
+export function _pointInLine(p1, p2, t) {
+       return {
+               x: p1.x + t * (p2.x - p1.x),
+               y: p1.y + t * (p2.y - p1.y)
+       };
+}
+
+/**
+ * @private
+ */
+export function _steppedInterpolation(p1, p2, t, mode) {
+       return {
+               x: p1.x + t * (p2.x - p1.x),
+               y: mode === 'middle' ? t < 0.5 ? p1.y : p2.y
+               : mode === 'after' ? t < 1 ? p1.y : p2.y
+               : t > 0 ? p2.y : p1.y
+       };
+}
+
+/**
+ * @private
+ */
+export function _bezierInterpolation(p1, p2, t) {
+       const cp1 = {x: p1.controlPointNextX, y: p1.controlPointNextY};
+       const cp2 = {x: p2.controlPointPreviousX, y: p2.controlPointPreviousY};
+       const a = _pointInLine(p1, cp1, t);
+       const b = _pointInLine(cp1, cp2, t);
+       const c = _pointInLine(cp2, p2, t);
+       const d = _pointInLine(a, b, t);
+       const e = _pointInLine(b, c, t);
+       return _pointInLine(d, e, t);
+}
index 1ad309ccd07805e0d3186860e4a614f25e84e27c..d5a5680b8c5dbc50e1c31766c5f538de70a08c1e 100644 (file)
@@ -2,6 +2,10 @@
 
 import {isFinite as isFiniteNumber} from './helpers.core';
 
+const PI = Math.PI;
+const TAU = 2 * PI;
+const PITAU = TAU + PI;
+
 /**
  * @alias Chart.helpers.math
  * @namespace
@@ -41,7 +45,6 @@ export const log10 = Math.log10 || function(x) {
        return isPowerOf10 ? powerOf10 : exponent;
 };
 
-
 export function isNumber(n) {
        return !isNaN(parseFloat(n)) && isFinite(n);
 }
@@ -128,3 +131,33 @@ export function getAngleFromPoint(centrePoint, anglePoint) {
 export function distanceBetweenPoints(pt1, pt2) {
        return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));
 }
+
+/**
+ * Shortest distance between angles, in either direction.
+ * @private
+ */
+export function _angleDiff(a, b) {
+       return (a - b + PITAU) % TAU - PI;
+}
+
+/**
+ * Normalize angle to be between 0 and 2*PI
+ * @private
+ */
+export function _normalizeAngle(a) {
+       return (a % TAU + TAU) % TAU;
+}
+
+/**
+ * @private
+ */
+export function _angleBetween(angle, start, end) {
+       const a = _normalizeAngle(angle);
+       const s = _normalizeAngle(start);
+       const e = _normalizeAngle(end);
+       const angleToStart = _normalizeAngle(s - a);
+       const angleToEnd = _normalizeAngle(e - a);
+       const startToAngle = _normalizeAngle(a - s);
+       const endToAngle = _normalizeAngle(a - e);
+       return a === s || a === e || (angleToStart > angleToEnd && startToAngle < endToAngle);
+}
diff --git a/src/helpers/helpers.segment.js b/src/helpers/helpers.segment.js
new file mode 100644 (file)
index 0000000..bd0099b
--- /dev/null
@@ -0,0 +1,228 @@
+'use strict';
+
+import {_angleBetween, _angleDiff, _normalizeAngle} from './helpers.math';
+
+function propertyFn(property) {
+       if (property === 'angle') {
+               return {
+                       between: _angleBetween,
+                       compare: _angleDiff,
+                       normalize: _normalizeAngle,
+               };
+       }
+       return {
+               between: (n, s, e) => n >= s && n <= e,
+               compare: (a, b) => a - b,
+               normalize: x => x
+       };
+}
+
+function makeSubSegment(start, end, loop, count) {
+       return {
+               start: start % count,
+               end: end % count,
+               loop: loop && (end - start + 1) % count === 0
+       };
+}
+
+function getSegment(segment, points, bounds) {
+       const {property, start: startBound, end: endBound} = bounds;
+       const {between, normalize} = propertyFn(property);
+       const count = points.length;
+       let {start, end, loop} = segment;
+       let i, ilen;
+
+       if (loop) {
+               start += count;
+               end += count;
+               for (i = 0, ilen = count; i < ilen; ++i) {
+                       if (!between(normalize(points[start % count][property]), startBound, endBound)) {
+                               break;
+                       }
+                       start--;
+                       end--;
+               }
+               start %= count;
+               end %= count;
+       }
+
+       if (end < start) {
+               end += count;
+       }
+       return {start, end, loop};
+}
+
+/**
+ * Returns the sub-segment(s) of a line segment that fall in the given bounds
+ * @param {object} segment
+ * @param {number} segment.start - start index of the segment, referring the points array
+ * @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 {Point[]} points - the points that this segment refers to
+ * @param {object} bounds
+ * @param {string} bounds.property - the property of a `Point` we are bounding. `x`, `y` or `angle`.
+ * @param {number} bounds.start - start value of the property
+ * @param {number} bounds.end - end value of the property
+ **/
+export function _boundSegment(segment, points, bounds) {
+       if (!bounds) {
+               return [segment];
+       }
+
+       const {property, start: startBound, end: endBound} = bounds;
+       const count = points.length;
+       const {compare, between, normalize} = propertyFn(property);
+       const {start, end, loop} = getSegment(segment, points, bounds);
+       const result = [];
+       let inside = false;
+       let subStart = null;
+       let i, value, point, prev;
+
+       for (i = start; i <= end; ++i) {
+               point = points[i % count];
+
+               if (point.skip) {
+                       continue;
+               }
+
+               value = normalize(point[property]);
+               inside = between(value, startBound, endBound);
+
+               if (subStart === null && inside) {
+                       subStart = i > start && compare(value, startBound) > 0 ? prev : i;
+               }
+
+               if (subStart !== null && (!inside || compare(value, endBound) === 0)) {
+                       result.push(makeSubSegment(subStart, i, loop, count));
+                       subStart = null;
+               }
+               prev = i;
+       }
+
+       if (subStart !== null) {
+               result.push(makeSubSegment(subStart, end, loop, count));
+       }
+
+       return result;
+}
+
+/**
+ * Returns the segments of the line that are inside given bounds
+ * @param {Line} line
+ * @param {object} bounds
+ * @param {string} bounds.property - the property we are bounding with. `x`, `y` or `angle`.
+ * @param {number} bounds.start - start value of the `property`
+ * @param {number} bounds.end - end value of the `property`
+ */
+export function _boundSegments(line, bounds) {
+       const result = [];
+
+       for (let segment of line.segments) {
+               let sub = _boundSegment(segment, line.points, bounds);
+               if (sub.length) {
+                       result.push(...sub);
+               }
+       }
+       return result;
+}
+
+/**
+ * Find start and end index of a line.
+ */
+function findStartAndEnd(points, count, loop, spanGaps) {
+       let start = 0;
+       let end = count - 1;
+
+       if (loop && !spanGaps) {
+               // loop and not spaning gaps, first find a gap to start from
+               while (start < count && !points[start].skip) {
+                       start++;
+               }
+       }
+
+       // find first non skipped point (after the first gap possibly)
+       while (start < count && points[start].skip) {
+               start++;
+       }
+
+       // if we looped to count, start needs to be 0
+       start %= count;
+
+       if (loop) {
+               // loop will go past count, if start > 0
+               end += start;
+       }
+
+       while (end > start && points[end % count].skip) {
+               end--;
+       }
+
+       // end could be more than count, normalize
+       end %= count;
+
+       return {start, end};
+}
+
+/**
+ * Compute solid segments from Points, when spanGaps === false
+ * @param {Point[]} points - the points
+ * @param {number} start - start index
+ * @param {number} max - max index (can go past count on a loop)
+ * @param {boolean} loop - boolean indicating that this would be a loop if no gaps are found
+ */
+function solidSegments(points, start, max, loop) {
+       const count = points.length;
+       const result = [];
+       let last = start;
+       let prev = points[start];
+       let end;
+
+       for (end = start + 1; end <= max; ++end) {
+               const cur = points[end % count];
+               if (cur.skip) {
+                       if (!prev.skip) {
+                               loop = false;
+                               result.push({start: start % count, end: (end - 1) % count, loop});
+                               start = last = null;
+                       }
+               } else {
+                       last = end;
+                       if (prev.skip) {
+                               start = end;
+                       }
+               }
+               prev = cur;
+       }
+
+       if (last !== null) {
+               result.push({start: start % count, end: last % count, loop});
+       }
+
+       return result;
+}
+
+/**
+ * Compute the continuous segments that define the whole line
+ * There can be skipped points within a segment, if spanGaps is true.
+ * @param {Line} line
+ */
+export function _computeSegments(line) {
+       const points = line.points;
+       const spanGaps = line.options.spanGaps;
+       const count = points.length;
+
+       if (!count) {
+               return [];
+       }
+
+       const loop = !!line._loop;
+       const {start, end} = findStartAndEnd(points, count, loop, spanGaps);
+
+       if (spanGaps) {
+               return [{start, end, loop}];
+       }
+
+       const max = end < start ? end + count : end;
+       const completeLoop = !!line._fullLoop && start === 0 && end === count - 1;
+       return solidSegments(points, start, max, completeLoop);
+}
index 2ddda44de7ba9ed51157f1191886bfea81c3d095..8735f15dbb829d32100524efcc142f12fd1afe54 100644 (file)
@@ -6,9 +6,12 @@
 
 'use strict';
 
-var defaults = require('../core/core.defaults');
-var elements = require('../elements/index');
-var helpers = require('../helpers/index');
+import defaults from '../core/core.defaults';
+import Line from '../elements/element.line';
+import {_boundSegment, _boundSegments} from '../helpers/helpers.segment';
+import {clipArea, unclipArea} from '../helpers/helpers.canvas';
+import {valueOrDefault, isFinite, isArray, extend} from '../helpers/helpers.core';
+import {_normalizeAngle} from '../helpers/helpers.math';
 
 defaults._set('global', {
        plugins: {
@@ -18,50 +21,19 @@ defaults._set('global', {
        }
 });
 
-var mappers = {
-       dataset: function(source) {
-               var index = source.fill;
-               var chart = source.chart;
-               var meta = chart.getDatasetMeta(index);
-               var visible = meta && chart.isDatasetVisible(index);
-               var points = (visible && meta.dataset._children) || [];
-               var length = points.length || 0;
-
-               return !length ? null : function(point, i) {
-                       return (i < length && points[i]) || null;
-               };
-       },
-
-       boundary: function(source) {
-               var boundary = source.boundary;
-               var x = boundary ? boundary.x : null;
-               var y = boundary ? boundary.y : null;
-
-               if (helpers.isArray(boundary)) {
-                       return function(point, i) {
-                               return boundary[i];
-                       };
-               }
-
-               return function(point) {
-                       return {
-                               x: x === null ? point.x : x,
-                               y: y === null ? point.y : y,
-                               boundary: true
-                       };
-               };
-       }
-};
+function getLineByIndex(chart, index) {
+       const meta = chart.getDatasetMeta(index);
+       const visible = meta && chart.isDatasetVisible(index);
+       return visible ? meta.dataset : null;
+}
 
-// @todo if (fill[0] === '#')
-function decodeFill(el, index, count) {
-       var model = el.options || {};
-       var fillOption = model.fill;
-       var fill = fillOption && typeof fillOption.target !== 'undefined' ? fillOption.target : fillOption;
-       var target;
+function parseFillOption(line) {
+       const options = line.options;
+       const fillOption = options.fill;
+       let fill = valueOrDefault(fillOption && fillOption.target, fillOption);
 
        if (fill === undefined) {
-               fill = !!model.backgroundColor;
+               fill = !!options.backgroundColor;
        }
 
        if (fill === false || fill === null) {
@@ -71,8 +43,14 @@ function decodeFill(el, index, count) {
        if (fill === true) {
                return 'origin';
        }
+       return fill;
+}
+
+// @todo if (fill[0] === '#')
+function decodeFill(line, index, count) {
+       const fill = parseFillOption(line);
+       let target = parseFloat(fill, 10);
 
-       target = parseFloat(fill, 10);
        if (isFinite(target) && Math.floor(target) === target) {
                if (fill[0] === '-' || fill[0] === '+') {
                        target = index + target;
@@ -85,94 +63,88 @@ function decodeFill(el, index, count) {
                return target;
        }
 
-       switch (fill) {
-       // compatibility
-       case 'bottom':
-               return 'start';
-       case 'top':
-               return 'end';
-       case 'zero':
-               return 'origin';
-       // supported boundaries
-       case 'origin':
-       case 'start':
-       case 'end':
-               return fill;
-       // invalid fill values
-       default:
-               return false;
-       }
+       return ['origin', 'start', 'end'].indexOf(fill) >= 0 ? fill : false;
 }
 
 function computeLinearBoundary(source) {
-       var model = source.el || {};
-       var scale = source.scale || {};
-       var fill = source.fill;
+       const {scale = {}, fill} = source;
        var target = null;
        var horizontal;
 
-       if (isFinite(fill)) {
-               return null;
-       }
-
-       // Backward compatibility: until v3, we still need to support boundary values set on
-       // the model (scaleTop, scaleBottom and scaleZero) because some external plugins and
-       // controllers might still use it (e.g. the Smith chart).
-
        if (fill === 'start') {
-               target = model.scaleBottom === undefined ? scale.bottom : model.scaleBottom;
+               target = scale.bottom;
        } else if (fill === 'end') {
-               target = model.scaleTop === undefined ? scale.top : model.scaleTop;
-       } else if (model.scaleZero !== undefined) {
-               target = model.scaleZero;
+               target = scale.top;
        } else if (scale.getBasePixel) {
                target = scale.getBasePixel();
        }
 
-       if (target !== undefined && target !== null) {
-               if (target.x !== undefined && target.y !== undefined) {
-                       return target;
+       if (isFinite(target)) {
+               horizontal = scale.isHorizontal();
+               return {
+                       x: horizontal ? target : null,
+                       y: horizontal ? null : target
+               };
+       }
+
+       return null;
+}
+
+// TODO: use elements.Arc instead
+class simpleArc {
+       constructor(opts) {
+               extend(this, opts);
+       }
+
+       pathSegment(ctx, bounds, opts) {
+               const {x, y, radius} = this;
+               bounds = bounds || {start: 0, end: Math.PI * 2};
+               if (opts.reverse) {
+                       ctx.arc(x, y, radius, bounds.end, bounds.start, true);
+               } else {
+                       ctx.arc(x, y, radius, bounds.start, bounds.end);
                }
+               return !opts.bounds;
+       }
 
-               if (helpers.isFinite(target)) {
-                       horizontal = scale.isHorizontal();
+       interpolate(point, property) {
+               const {x, y, radius} = this;
+               const angle = point.angle;
+               if (property === 'angle') {
                        return {
-                               x: horizontal ? target : null,
-                               y: horizontal ? null : target,
-                               boundary: true
+                               x: x + Math.cos(angle) * radius,
+                               y: y + Math.sin(angle) * radius,
+                               angle
                        };
                }
        }
-
-       return null;
 }
 
 function computeCircularBoundary(source) {
-       var scale = source.scale;
-       var options = scale.options;
-       var length = scale.chart.data.labels.length;
-       var fill = source.fill;
-       var target = [];
-       var start, end, center, i, point;
-
-       if (!length) {
-               return null;
-       }
+       const {scale, fill} = source;
+       const options = scale.options;
+       const length = scale._getLabels().length;
+       const target = [];
+       let start, end, value, i, center;
 
        start = options.reverse ? scale.max : scale.min;
        end = options.reverse ? scale.min : scale.max;
-       center = scale.getPointPositionForValue(0, start);
+
+       value = fill === 'start' ? start
+               : fill === 'end' ? end
+               : scale.getBaseValue();
+
+       if (options.gridLines.circular) {
+               center = scale.getPointPositionForValue(0, start);
+               return new simpleArc({
+                       x: center.x,
+                       y: center.y,
+                       radius: scale.getDistanceFromCenterForValue(value)
+               });
+       }
+
        for (i = 0; i < length; ++i) {
-               point = fill === 'start' || fill === 'end'
-                       ? scale.getPointPositionForValue(i, fill === 'start' ? start : end)
-                       : scale.getBasePosition(i);
-               if (options.gridLines.circular) {
-                       point.cx = center.x;
-                       point.cy = center.y;
-                       point.angle = scale.getIndexAngle(i) - Math.PI / 2;
-               }
-               point.boundary = true;
-               target.push(point);
+               target.push(scale.getPointPositionForValue(i, value));
        }
        return target;
 }
@@ -186,6 +158,48 @@ function computeBoundary(source) {
        return computeLinearBoundary(source);
 }
 
+function pointsFromSegments(boundary, line) {
+       const {x = null, y = null} = boundary || {};
+       const linePoints = line.points;
+       const points = [];
+       line.segments.forEach((segment) => {
+               const first = linePoints[segment.start];
+               const last = linePoints[segment.end];
+               if (y !== null) {
+                       points.push({x: first.x, y});
+                       points.push({x: last.x, y});
+               } else if (x !== null) {
+                       points.push({x, y: first.y});
+                       points.push({x, y: last.y});
+               }
+       });
+       return points;
+}
+
+function getTarget(source) {
+       const {chart, fill, line} = source;
+
+       if (isFinite(fill)) {
+               return getLineByIndex(chart, fill);
+       }
+
+       const boundary = computeBoundary(source);
+       let points = [];
+       let _loop = false;
+
+       if (boundary instanceof simpleArc) {
+               return boundary;
+       }
+
+       if (isArray(boundary)) {
+               _loop = true;
+               points = boundary;
+       } else {
+               points = pointsFromSegments(boundary, line);
+       }
+       return points.length ? new Line({points, options: {tension: 0}, _loop, _fullLoop: _loop}) : null;
+}
+
 function resolveTarget(sources, index, propagate) {
        var source = sources[index];
        var fill = source.fill;
@@ -217,219 +231,170 @@ function resolveTarget(sources, index, propagate) {
        return false;
 }
 
-function createMapper(source) {
-       var fill = source.fill;
-       var type = 'dataset';
+function _clip(ctx, target, clipY) {
+       ctx.beginPath();
+       target.path(ctx);
+       ctx.lineTo(target.last().x, clipY);
+       ctx.lineTo(target.first().x, clipY);
+       ctx.closePath();
+       ctx.clip();
+}
 
-       if (fill === false) {
-               return null;
+function getBounds(property, first, last, loop) {
+       if (loop) {
+               return;
        }
+       let start = first[property];
+       let end = last[property];
 
-       if (!isFinite(fill)) {
-               type = 'boundary';
+       if (property === 'angle') {
+               start = _normalizeAngle(start);
+               end = _normalizeAngle(end);
        }
-
-       return mappers[type](source);
+       return {property, start, end};
 }
 
-function isDrawable(point) {
-       return point && !point.skip;
+function _getEdge(a, b, prop, fn) {
+       if (a && b) {
+               return fn(a[prop], b[prop]);
+       }
+       return a ? a[prop] : b ? b[prop] : 0;
 }
 
-function fillPointsSets(ctx, curve0, curve1, len0, len1, area, pointSets) {
-       const fillAreaPointsSet = [];
-       const clipAboveAreaPointsSet = [];
-       const clipBelowAreaPointsSet = [];
-       const radialSet = [];
-       const jointPoint = {};
-       let i, cx, cy, r;
+function _segments(line, target, property) {
+       const points = line.points;
+       const tpoints = target.points;
+       const parts = [];
+
+       for (let segment of line.segments) {
+               const bounds = getBounds(property, points[segment.start], points[segment.end], segment.loop);
+
+               if (!target.segments) {
+                       // Special case for boundary not supporting `segments` (simpleArc)
+                       // Bounds are provided as `target` for partial circle, or undefined for full circle
+                       parts.push({
+                               source: segment,
+                               target: bounds,
+                               start: points[segment.start],
+                               end: points[segment.end]
+                       });
+                       continue;
+               }
 
-       if (!len0 || !len1) {
-               return;
-       }
-       clipAboveAreaPointsSet.push({x: curve1[len1 - 1].x, y: area.top});
-       clipBelowAreaPointsSet.push({x: curve0[0].x, y: area.top});
-       clipBelowAreaPointsSet.push(curve0[0]);
-
-       // building first area curve (normal)
-       fillAreaPointsSet.push(curve0[0]);
-       for (i = 1; i < len0; ++i) {
-               curve0[i].flip = false;
-               fillAreaPointsSet.push(curve0[i]);
-               clipBelowAreaPointsSet.push(curve0[i]);
-       }
+               // Get all segments from `target` that intersect the bounds of current segment of `line`
+               const subs = _boundSegments(target, bounds);
+
+               for (let sub of subs) {
+                       const subBounds = getBounds(property, tpoints[sub.start], tpoints[sub.end], sub.loop);
+                       const fillSources = _boundSegment(segment, points, subBounds);
+
+                       for (let source of fillSources) {
+                               parts.push({
+                                       source,
+                                       target: sub,
+                                       start: {
+                                               [property]: _getEdge(bounds, subBounds, 'start', Math.max)
+                                       },
+                                       end: {
+                                               [property]: _getEdge(bounds, subBounds, 'end', Math.min)
+                                       }
 
-       if (curve1[0].angle !== undefined) {
-               pointSets.fill.push(fillAreaPointsSet);
-               cx = curve1[0].cx;
-               cy = curve1[0].cy;
-               r = Math.sqrt(Math.pow(curve1[0].x - cx, 2) + Math.pow(curve1[0].y - cy, 2));
-               for (i = len1 - 1; i > 0; --i) {
-                       radialSet.push({cx: cx, cy: cy, radius: r, startAngle: curve1[i].angle, endAngle: curve1[i - 1].angle});
-               }
-               if (radialSet.length) {
-                       pointSets.fill.push(radialSet);
-               }
-               return;
-       }
-       // joining the two area curves
-       for (var key in curve1[len1 - 1]) {
-               if (Object.prototype.hasOwnProperty.call(curve1[len1 - 1], key)) {
-                       jointPoint[key] = curve1[len1 - 1][key];
+                               });
+                       }
                }
        }
-       jointPoint.joint = true;
-       fillAreaPointsSet.push(jointPoint);
-
-       // building opposite area curve (reverse)
-       for (i = len1 - 1; i > 0; --i) {
-               curve1[i].flip = true;
-               clipAboveAreaPointsSet.push(curve1[i]);
-               curve1[i - 1].flip = true;
-               fillAreaPointsSet.push(curve1[i - 1]);
-       }
-       clipAboveAreaPointsSet.push(curve1[0]);
-       clipAboveAreaPointsSet.push({x: curve1[0].x, y: area.top});
-       clipBelowAreaPointsSet.push({x: curve0[len0 - 1].x, y: area.top});
-
-       pointSets.clipAbove.push(clipAboveAreaPointsSet);
-       pointSets.clipBelow.push(clipBelowAreaPointsSet);
-       pointSets.fill.push(fillAreaPointsSet);
+       return parts;
 }
 
-function clipAndFill(ctx, clippingPointsSets, fillingPointsSets, color, stepped, tension) {
-       const lineTo = stepped ? helpers.canvas._steppedLineTo : helpers.canvas._bezierCurveTo;
-       let i, ilen, j, jlen, set, target;
-       if (clippingPointsSets) {
-               ctx.save();
+function clipBounds(ctx, scale, bounds) {
+       const {top, bottom} = scale.chart.chartArea;
+       const {property, start, end} = bounds || {};
+       if (property === 'x') {
                ctx.beginPath();
-               for (i = 0, ilen = clippingPointsSets.length; i < ilen; i++) {
-                       set = clippingPointsSets[i];
-                       // Have edge lines straight
-                       ctx.moveTo(set[0].x, set[0].y);
-                       ctx.lineTo(set[1].x, set[1].y);
-                       for (j = 2, jlen = set.length; j < jlen - 1; j++) {
-                               target = set[j];
-                               if (!target.boundary && (tension || stepped)) {
-                                       lineTo(ctx, set[j - 1], target, target.flip, stepped);
-                               } else {
-                                       ctx.lineTo(target.x, target.y);
-                               }
-                       }
-                       ctx.lineTo(set[j].x, set[j].y);
-               }
-               ctx.closePath();
+               ctx.rect(start, top, end - start, bottom - top);
                ctx.clip();
-               ctx.beginPath();
        }
-       for (i = 0, ilen = fillingPointsSets.length; i < ilen; i++) {
-               set = fillingPointsSets[i];
-               if (set[0].startAngle !== undefined) {
-                       for (j = 0, jlen = set.length; j < jlen; j++) {
-                               ctx.arc(set[j].cx, set[j].cy, set[j].radius, set[j].startAngle, set[j].endAngle, true);
-                       }
-               } else {
-                       ctx.moveTo(set[0].x, set[0].y);
-                       for (j = 1, jlen = set.length; j < jlen; j++) {
-                               if (set[j].joint) {
-                                       ctx.lineTo(set[j].x, set[j].y);
-                               } else {
-                                       target = set[j];
-                                       if (!target.boundary && (tension || stepped)) {
-                                               lineTo(ctx, set[j - 1], target, target.flip, stepped);
-                                       } else {
-                                               ctx.lineTo(target.x, target.y);
-                                       }
-                               }
-                       }
-               }
+}
+
+function interpolatedLineTo(ctx, target, point, property) {
+       const interpolatedPoint = target.interpolate(point, property);
+       if (interpolatedPoint) {
+               ctx.lineTo(interpolatedPoint.x, interpolatedPoint.y);
        }
-       ctx.closePath();
-       ctx.fillStyle = color;
-       ctx.fill();
-       ctx.restore();
 }
 
-function doFill(ctx, points, mapper, colors, el, area) {
-       const count = points.length;
-       const options = el.options;
-       const loop = el._loop;
-       const span = options.spanGaps;
-       const stepped = options.steppedLine;
-       const tension = options.tension;
-       let curve0 = [];
-       let curve1 = [];
-       let len0 = 0;
-       let len1 = 0;
-       let pointSets = {clipBelow: [], clipAbove: [], fill: []};
-       let i, ilen, index, p0, p1, d0, d1, loopOffset;
+function _fill(ctx, cfg) {
+       const {line, target, property, color, scale} = cfg;
+       const segments = _segments(cfg.line, cfg.target, property);
 
-       ctx.save();
-       ctx.beginPath();
+       ctx.fillStyle = color;
+       for (let i = 0, ilen = segments.length; i < ilen; ++i) {
+               const {source: src, target: tgt, start, end} = segments[i];
+
+               ctx.save();
 
-       for (i = 0, ilen = count; i < ilen; ++i) {
-               index = i % count;
-               p0 = points[index];
-               p1 = mapper(p0, index);
-               d0 = isDrawable(p0);
-               d1 = isDrawable(p1);
+               clipBounds(ctx, scale, getBounds(property, start, end));
 
-               if (loop && loopOffset === undefined && d0) {
-                       loopOffset = i + 1;
-                       ilen = count + loopOffset;
+               ctx.beginPath();
+
+               let loop = !!line.pathSegment(ctx, src);
+               if (loop) {
+                       ctx.closePath();
+               } else {
+                       interpolatedLineTo(ctx, target, end, property);
                }
 
-               if (d0 && d1) {
-                       len0 = curve0.push(p0);
-                       len1 = curve1.push(p1);
-               } else if (len0 && len1) {
-                       if (!span) {
-                               fillPointsSets(ctx, curve0, curve1, len0, len1, area, pointSets);
-                               len0 = len1 = 0;
-                               curve0 = [];
-                               curve1 = [];
-                       } else {
-                               if (d0) {
-                                       curve0.push(p0);
-                               }
-                               if (d1) {
-                                       curve1.push(p1);
-                               }
-                       }
+               loop &= target.pathSegment(ctx, tgt, {move: loop, reverse: true});
+               if (!loop) {
+                       interpolatedLineTo(ctx, target, start, property);
                }
+
+               ctx.closePath();
+               ctx.fill(loop ? 'evenodd' : 'nonzero');
+
+               ctx.restore();
        }
+}
 
-       fillPointsSets(ctx, curve0, curve1, len0, len1, area, pointSets);
+function doFill(ctx, cfg) {
+       const {line, target, above, below, area, scale} = cfg;
+       const property = line._loop ? 'angle' : 'x';
 
-       if (colors.below !== colors.above) {
-               clipAndFill(ctx, pointSets.clipAbove, pointSets.fill, colors.above, stepped, tension);
-               clipAndFill(ctx, pointSets.clipBelow, pointSets.fill, colors.below, stepped, tension);
-       } else {
-               clipAndFill(ctx, false, pointSets.fill, colors.above, stepped, tension);
+       ctx.save();
+
+       if (property === 'x' && below !== above) {
+               _clip(ctx, target, area.top);
+               _fill(ctx, {line, target, color: above, scale, property});
+               ctx.restore();
+               ctx.save();
+               _clip(ctx, target, area.bottom);
        }
+       _fill(ctx, {line, target, color: below, scale, property});
+
+       ctx.restore();
 }
 
-module.exports = {
+export default {
        id: 'filler',
 
        afterDatasetsUpdate: function(chart, options) {
                var count = (chart.data.datasets || []).length;
                var propagate = options.propagate;
                var sources = [];
-               var meta, i, el, source;
+               var meta, i, line, source;
 
                for (i = 0; i < count; ++i) {
                        meta = chart.getDatasetMeta(i);
-                       el = meta.dataset;
+                       line = meta.dataset;
                        source = null;
 
-                       if (el && el.options && el instanceof elements.Line) {
+                       if (line && line.options && line instanceof Line) {
                                source = {
                                        visible: chart.isDatasetVisible(i),
-                                       fill: decodeFill(el, i, count),
+                                       fill: decodeFill(line, i, count),
                                        chart: chart,
-                                       scale: meta.yScale || meta.rScale,
-                                       el: el
+                                       scale: meta.vScale,
+                                       line
                                };
                        }
 
@@ -439,13 +404,12 @@ module.exports = {
 
                for (i = 0; i < count; ++i) {
                        source = sources[i];
-                       if (!source) {
+                       if (!source || source.fill === false) {
                                continue;
                        }
 
                        source.fill = resolveTarget(sources, i, propagate);
-                       source.boundary = computeBoundary(source);
-                       source.mapper = createMapper(source);
+                       source.target = source.fill !== false && getTarget(source);
                }
        },
 
@@ -453,40 +417,31 @@ module.exports = {
                const metasets = chart._getSortedVisibleDatasetMetas();
                const area = chart.chartArea;
                const ctx = chart.ctx;
-               var meta, i, el, options, points, mapper, color, colors, fillOption;
+               let i, meta;
 
                for (i = metasets.length - 1; i >= 0; --i) {
                        meta = metasets[i].$filler;
 
-                       if (!meta || !meta.visible) {
-                               continue;
+                       if (meta) {
+                               meta.line.updateControlPoints(area);
                        }
-                       meta.el.updateControlPoints(area);
                }
 
                for (i = metasets.length - 1; i >= 0; --i) {
                        meta = metasets[i].$filler;
 
-                       if (!meta || !meta.visible) {
+                       if (!meta || meta.fill === false) {
                                continue;
                        }
-
-                       el = meta.el;
-                       options = el.options;
-                       points = el._children || [];
-                       mapper = meta.mapper;
-                       fillOption = options.fill;
-                       color = options.backgroundColor || defaults.global.defaultColor;
-
-                       colors = {above: color, below: color};
-                       if (fillOption && typeof fillOption === 'object') {
-                               colors.above = fillOption.above || color;
-                               colors.below = fillOption.below || color;
-                       }
-                       if (mapper && points.length) {
-                               helpers.canvas.clipArea(ctx, area);
-                               doFill(ctx, points, mapper, colors, el, area);
-                               helpers.canvas.unclipArea(ctx);
+                       const {line, target, scale} = meta;
+                       const lineOpts = line.options;
+                       const fillOption = lineOpts.fill;
+                       const color = lineOpts.backgroundColor || defaults.global.defaultColor;
+                       const {above = color, below = color} = fillOption || {};
+                       if (target && line.points.length) {
+                               clipArea(ctx, area);
+                               doFill(ctx, {line, target, above, below, area, scale});
+                               unclipArea(ctx);
                        }
                }
        }
index 944826a7022e33cb1c7b62686848e722845f83a6..ddf7fac13170d13dc8eb8b0984016f304360b4a1 100644 (file)
@@ -407,10 +407,11 @@ class RadialLinearScale extends LinearScaleBase {
 
        getPointPosition(index, distanceFromCenter) {
                var me = this;
-               var thisAngle = me.getIndexAngle(index) - (Math.PI / 2);
+               var angle = me.getIndexAngle(index) - (Math.PI / 2);
                return {
-                       x: Math.cos(thisAngle) * distanceFromCenter + me.xCenter,
-                       y: Math.sin(thisAngle) * distanceFromCenter + me.yCenter
+                       x: Math.cos(angle) * distanceFromCenter + me.xCenter,
+                       y: Math.sin(angle) * distanceFromCenter + me.yCenter,
+                       angle
                };
        }
 
@@ -419,15 +420,7 @@ class RadialLinearScale extends LinearScaleBase {
        }
 
        getBasePosition(index) {
-               var me = this;
-               var min = me.min;
-               var max = me.max;
-
-               return me.getPointPositionForValue(index || 0,
-                       me.beginAtZero ? 0 :
-                       min < 0 && max < 0 ? max :
-                       min > 0 && max > 0 ? min :
-                       0);
+               return this.getPointPositionForValue(index || 0, this.getBaseValue());
        }
 
        /**
diff --git a/test/fixtures/plugin.filler/fill-line-dataset-interpolated.js b/test/fixtures/plugin.filler/fill-line-dataset-interpolated.js
new file mode 100644 (file)
index 0000000..ca216e3
--- /dev/null
@@ -0,0 +1,69 @@
+const data1 = [];
+const data2 = [];
+const data3 = [];
+for (let i = 0; i < 200; i++) {
+       const a = i / Math.PI / 10;
+
+       data1.push({x: i, y: i < 86 || i > 104 && i < 178 ? Math.sin(a) : NaN});
+
+       if (i % 10 === 0) {
+               data2.push({x: i, y: Math.cos(a)});
+       }
+
+       if (i % 15 === 0) {
+               data3.push({x: i, y: Math.cos(a + Math.PI / 2)});
+       }
+}
+
+module.exports = {
+       config: {
+               type: 'line',
+               data: {
+                       datasets: [{
+                               borderColor: 'rgba(255, 0, 0, 0.5)',
+                               backgroundColor: 'rgba(255, 0, 0, 0.25)',
+                               data: data1,
+                               fill: false,
+                       }, {
+                               borderColor: 'rgba(0, 0, 255, 0.5)',
+                               backgroundColor: 'rgba(0, 0, 255, 0.25)',
+                               data: data2,
+                               fill: 0,
+                       }, {
+                               borderColor: 'rgba(0, 255, 0, 0.5)',
+                               backgroundColor: 'rgba(0, 255, 0, 0.25)',
+                               data: data3,
+                               fill: 1,
+                       }]
+               },
+               options: {
+                       animation: false,
+                       responsive: false,
+                       legend: false,
+                       title: false,
+                       datasets: {
+                               line: {
+                                       lineTension: 0.4,
+                                       borderWidth: 1,
+                                       pointRadius: 1.5,
+                               }
+                       },
+                       scales: {
+                               x: {
+                                       type: 'linear',
+                                       display: false
+                               },
+                               y: {
+                                       type: 'linear',
+                                       display: false
+                               }
+                       }
+               }
+       },
+       options: {
+               canvas: {
+                       height: 512,
+                       width: 512
+               }
+       }
+};
diff --git a/test/fixtures/plugin.filler/fill-line-dataset-interpolated.png b/test/fixtures/plugin.filler/fill-line-dataset-interpolated.png
new file mode 100644 (file)
index 0000000..fab42a4
Binary files /dev/null and b/test/fixtures/plugin.filler/fill-line-dataset-interpolated.png differ
index d5600b7678842678486e97fedbee23507326219c..a7b705ced47118cb460117e178f250ff045aeccd 100644 (file)
Binary files a/test/fixtures/plugin.filler/fill-line-dataset-span-dual.png and b/test/fixtures/plugin.filler/fill-line-dataset-span-dual.png differ
index 7c8a856c678aece91d3b24e4aaefcfa7fdc88b70..780ce79f33a8f573b528dc894279c8f73bfb71a4 100644 (file)
Binary files a/test/fixtures/plugin.filler/fill-line-dataset-span.png and b/test/fixtures/plugin.filler/fill-line-dataset-span.png differ
index 45e15c5d8ad198ef856fa08baf05889c8eccbb6c..68a20e2eeeae72d6ff5d2447df65eb11d70e7aab 100644 (file)
Binary files a/test/fixtures/plugin.filler/fill-line-dataset-spline-span-below.png and b/test/fixtures/plugin.filler/fill-line-dataset-spline-span-below.png differ
index 5f66359f3b57decaba1c133c1cd2de1be17d08bf..97cbb6d2f508c623e15d027abbcee553a8841ac1 100644 (file)
Binary files a/test/fixtures/plugin.filler/fill-line-dataset-spline-span.png and b/test/fixtures/plugin.filler/fill-line-dataset-spline-span.png differ
diff --git a/test/fixtures/plugin.filler/fill-line-dataset-stepped.json b/test/fixtures/plugin.filler/fill-line-dataset-stepped.json
new file mode 100644 (file)
index 0000000..c6a6815
--- /dev/null
@@ -0,0 +1,62 @@
+{
+    "config": {
+        "type": "line",
+        "data": {
+            "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"],
+            "datasets": [{
+                "backgroundColor": "rgba(255, 0, 0, 0.25)",
+                "steppedLine": true,
+                "data": [null, null, 0, -1, 0, 1, 0, -1, 0],
+                "fill": 1
+            }, {
+                "backgroundColor": "rgba(0, 255, 0, 0.25)",
+                "steppedLine": "after",
+                "data": [1, 0, null, 1, 0, null, -1, 0, 1],
+                "fill": "+1"
+            }, {
+                "backgroundColor": "rgba(0, 0, 255, 0.25)",
+                "steppedLine": "before",
+                "data": [0, 2, 0, -2, 0, 2, 0],
+                "fill": 3
+            }, {
+                "backgroundColor": "rgba(255, 0, 255, 0.25)",
+                "steppedLine": "middle",
+                "data": [2, 0, -2, 0, 2, 0, -2, 0, 2],
+                "fill": "-2"
+            }, {
+                "backgroundColor": "rgba(255, 255, 0, 0.25)",
+                "steppedLine": false,
+                "data": [3, 1, -1, -3, -1, 1, 3, 1, -1],
+                "fill": "-1"
+            }]
+        },
+        "options": {
+            "responsive": false,
+            "spanGaps": false,
+            "legend": false,
+            "title": false,
+            "scales": {
+                "x": {
+                    "display": false
+                },
+                "y": {
+                    "display": false
+                }
+            },
+            "elements": {
+                "point": {
+                    "radius": 0
+                },
+                "line": {
+                    "borderColor": "black"
+                }
+            }
+        }
+    },
+    "options": {
+        "canvas": {
+            "height": 256,
+            "width": 512
+        }
+    }
+}
diff --git a/test/fixtures/plugin.filler/fill-line-dataset-stepped.png b/test/fixtures/plugin.filler/fill-line-dataset-stepped.png
new file mode 100644 (file)
index 0000000..e6dc9e7
Binary files /dev/null and b/test/fixtures/plugin.filler/fill-line-dataset-stepped.png differ
index fbc4e772c11df6b411bb9913164ff437ebec4f84..ee53eccaa892eb7fdb13939e7aca8e2f310750b9 100644 (file)
Binary files a/test/fixtures/plugin.filler/fill-radar-dataset-border.png and b/test/fixtures/plugin.filler/fill-radar-dataset-border.png differ
index 48e7e8930bee1b9e03992a7338cc165d047695cc..c7da86373bcf4e8bf7831302b4d7f3bf3071d02a 100644 (file)
Binary files a/test/fixtures/plugin.filler/fill-radar-dataset-span.png and b/test/fixtures/plugin.filler/fill-radar-dataset-span.png differ
index 3dbc538656f0be7d9164b4627fd4f21780ce3c61..c9e9b0b9f06bd98f4fe6a4fc196abf725f74cc38 100644 (file)
Binary files a/test/fixtures/plugin.filler/fill-radar-dataset-spline.png and b/test/fixtures/plugin.filler/fill-radar-dataset-spline.png differ
index 450ce2e0a7391ae4dff1d88147df78b37d3cc21b..a6cb6593ea6cba2abdbd4a032ed83eaf08e8e218 100644 (file)
Binary files a/test/fixtures/plugin.filler/fill-radar-dataset.png and b/test/fixtures/plugin.filler/fill-radar-dataset.png differ
index 5697c481a231713aee3b3fa61c4a70e50ed48df2..8e8b3c907940a97a7cbb87b90d497c2ce3a63e6c 100644 (file)
@@ -4,12 +4,10 @@ describe('Chart.elements.Line', function() {
 
        it('should be constructed', function() {
                var line = new Chart.elements.Line({
-                       _datasetindex: 2,
-                       _points: [1, 2, 3, 4]
+                       points: [1, 2, 3, 4]
                });
 
                expect(line).not.toBe(undefined);
-               expect(line._datasetindex).toBe(2);
-               expect(line._points).toEqual([1, 2, 3, 4]);
+               expect(line.points).toEqual([1, 2, 3, 4]);
        });
 });
index 87ff63b163a4b6c199de3803c8b2df55e125242f..66c00df6335512914e5b01d038594c20331cda74 100644 (file)
@@ -106,4 +106,36 @@ describe('Chart.helpers.math', function() {
                expect(math.isNumber(undefined)).toBe(false);
                expect(math.isNumber('cbc')).toBe(false);
        });
+
+       it('should compute shortest distance between angles', function() {
+               expect(math._angleDiff(1, 2)).toEqual(-1);
+               expect(math._angleDiff(2, 1)).toEqual(1);
+               expect(math._angleDiff(0, 3.15)).toBeCloseTo(3.13, 2);
+               expect(math._angleDiff(0, 3.13)).toEqual(-3.13);
+               expect(math._angleDiff(6.2, 0)).toBeCloseTo(-0.08, 2);
+               expect(math._angleDiff(6.3, 0)).toBeCloseTo(0.02, 2);
+               expect(math._angleDiff(4 * Math.PI, -4 * Math.PI)).toBeCloseTo(0, 4);
+               expect(math._angleDiff(4 * Math.PI, -3 * Math.PI)).toBeCloseTo(-3.14, 2);
+               expect(math._angleDiff(6.28, 3.1)).toBeCloseTo(-3.1, 2);
+               expect(math._angleDiff(6.28, 3.2)).toBeCloseTo(3.08, 2);
+       });
+
+       it('should normalize angles correctly', function() {
+               expect(math._normalizeAngle(-Math.PI)).toEqual(Math.PI);
+               expect(math._normalizeAngle(Math.PI)).toEqual(Math.PI);
+               expect(math._normalizeAngle(2)).toEqual(2);
+               expect(math._normalizeAngle(5 * Math.PI)).toEqual(Math.PI);
+               expect(math._normalizeAngle(-50 * Math.PI)).toBeCloseTo(6.28, 2);
+       });
+
+       it('should determine if angle is between boundaries', function() {
+               expect(math._angleBetween(2, 1, 3)).toBeTrue();
+               expect(math._angleBetween(2, 3, 1)).toBeFalse();
+               expect(math._angleBetween(-3.14, 2, 4)).toBeTrue();
+               expect(math._angleBetween(-3.14, 4, 2)).toBeFalse();
+               expect(math._angleBetween(0, -1, 1)).toBeTrue();
+               expect(math._angleBetween(-1, 0, 1)).toBeFalse();
+               expect(math._angleBetween(-15 * Math.PI, 3.1, 3.2)).toBeTrue();
+               expect(math._angleBetween(15 * Math.PI, -3.2, -3.1)).toBeTrue();
+       });
 });