### 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
});
```
+### 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.
// Update Line
if (showLine && mode !== 'resize') {
-
const properties = {
- _children: points,
+ points,
options: me._resolveDatasetElementOptions()
};
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()
};
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);
}
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;
}
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: {
}
});
-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
}
}
-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 {
}
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;
}
ctx.beginPath();
- if (me.drawPath(ctx, area)) {
+ if (me.path(ctx)) {
ctx.closePath();
}
* @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);
* @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,
import {almostEquals, sign} from './helpers.math';
+import {_isPointInArea} from './helpers.canvas';
const EPSILON = Number.EPSILON || 1e-14;
}
}
}
+
+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);
+ }
+}
--- /dev/null
+'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);
+}
import {isFinite as isFiniteNumber} from './helpers.core';
+const PI = Math.PI;
+const TAU = 2 * PI;
+const PITAU = TAU + PI;
+
/**
* @alias Chart.helpers.math
* @namespace
return isPowerOf10 ? powerOf10 : exponent;
};
-
export function isNumber(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
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);
+}
--- /dev/null
+'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);
+}
'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: {
}
});
-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) {
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;
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;
}
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;
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
};
}
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);
}
},
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);
}
}
}
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
};
}
}
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());
}
/**
--- /dev/null
+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
+ }
+ }
+};
--- /dev/null
+{
+ "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
+ }
+ }
+}
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]);
});
});
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();
+ });
});