]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Refactor filler plugin for easier maintenance (#10040)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Sat, 12 Feb 2022 15:22:38 +0000 (17:22 +0200)
committerGitHub <noreply@github.com>
Sat, 12 Feb 2022 15:22:38 +0000 (17:22 +0200)
src/plugins/plugin.filler.js [deleted file]
src/plugins/plugin.filler/filler.drawing.js [new file with mode: 0644]
src/plugins/plugin.filler/filler.options.js [new file with mode: 0644]
src/plugins/plugin.filler/filler.segment.js [new file with mode: 0644]
src/plugins/plugin.filler/filler.target.js [new file with mode: 0644]
src/plugins/plugin.filler/filler.target.stack.js [new file with mode: 0644]
src/plugins/plugin.filler/index.js [new file with mode: 0644]
src/plugins/plugin.filler/simpleArc.js [new file with mode: 0644]

diff --git a/src/plugins/plugin.filler.js b/src/plugins/plugin.filler.js
deleted file mode 100644 (file)
index 75fea2f..0000000
+++ /dev/null
@@ -1,655 +0,0 @@
-/**
- * Plugin based on discussion from the following Chart.js issues:
- * @see https://github.com/chartjs/Chart.js/issues/2380#issuecomment-279961569
- * @see https://github.com/chartjs/Chart.js/issues/2440#issuecomment-256461897
- */
-
-import LineElement from '../elements/element.line';
-import {_boundSegment, _boundSegments} from '../helpers/helpers.segment';
-import {clipArea, unclipArea} from '../helpers/helpers.canvas';
-import {isArray, isFinite, isObject, valueOrDefault} from '../helpers/helpers.core';
-import {TAU, _isBetween, _normalizeAngle} from '../helpers/helpers.math';
-
-/**
- * @typedef { import('../core/core.controller').default } Chart
- * @typedef { import('../core/core.scale').default } Scale
- * @typedef { import("../elements/element.point").default } PointElement
- */
-
-/**
- * @param {Chart} chart
- * @param {number} index
- */
-function getLineByIndex(chart, index) {
-  const meta = chart.getDatasetMeta(index);
-  const visible = meta && chart.isDatasetVisible(index);
-  return visible ? meta.dataset : null;
-}
-
-/**
- * @param {LineElement} line
- */
-function parseFillOption(line) {
-  const options = line.options;
-  const fillOption = options.fill;
-  let fill = valueOrDefault(fillOption && fillOption.target, fillOption);
-
-  if (fill === undefined) {
-    fill = !!options.backgroundColor;
-  }
-
-  if (fill === false || fill === null) {
-    return false;
-  }
-
-  if (fill === true) {
-    return 'origin';
-  }
-  return fill;
-}
-
-/**
- * @param {LineElement} line
- * @param {number} index
- * @param {number} count
- */
-function decodeFill(line, index, count) {
-  const fill = parseFillOption(line);
-
-  if (isObject(fill)) {
-    return isNaN(fill.value) ? false : fill;
-  }
-
-  let target = parseFloat(fill);
-
-  if (isFinite(target) && Math.floor(target) === target) {
-    if (fill[0] === '-' || fill[0] === '+') {
-      target = index + target;
-    }
-
-    if (target === index || target < 0 || target >= count) {
-      return false;
-    }
-
-    return target;
-  }
-
-  return ['origin', 'start', 'end', 'stack', 'shape'].indexOf(fill) >= 0 && fill;
-}
-
-function computeLinearBoundary(source) {
-  const {scale = {}, fill} = source;
-  let target = null;
-  let horizontal;
-
-  if (fill === 'start') {
-    target = scale.bottom;
-  } else if (fill === 'end') {
-    target = scale.top;
-  } else if (isObject(fill)) {
-    target = scale.getPixelForValue(fill.value);
-  } else if (scale.getBasePixel) {
-    target = scale.getBasePixel();
-  }
-
-  if (isFinite(target)) {
-    horizontal = scale.isHorizontal();
-    return {
-      x: horizontal ? target : null,
-      y: horizontal ? null : target
-    };
-  }
-
-  return null;
-}
-
-// TODO: use elements.ArcElement instead
-class simpleArc {
-  constructor(opts) {
-    this.x = opts.x;
-    this.y = opts.y;
-    this.radius = opts.radius;
-  }
-
-  pathSegment(ctx, bounds, opts) {
-    const {x, y, radius} = this;
-    bounds = bounds || {start: 0, end: TAU};
-    ctx.arc(x, y, radius, bounds.end, bounds.start, true);
-    return !opts.bounds;
-  }
-
-  interpolate(point) {
-    const {x, y, radius} = this;
-    const angle = point.angle;
-    return {
-      x: x + Math.cos(angle) * radius,
-      y: y + Math.sin(angle) * radius,
-      angle
-    };
-  }
-}
-
-function computeCircularBoundary(source) {
-  const {scale, fill} = source;
-  const options = scale.options;
-  const length = scale.getLabels().length;
-  const target = [];
-  const start = options.reverse ? scale.max : scale.min;
-  const end = options.reverse ? scale.min : scale.max;
-  let i, center, value;
-
-  if (fill === 'start') {
-    value = start;
-  } else if (fill === 'end') {
-    value = end;
-  } else if (isObject(fill)) {
-    value = fill.value;
-  } else {
-    value = scale.getBaseValue();
-  }
-
-  if (options.grid.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) {
-    target.push(scale.getPointPositionForValue(i, value));
-  }
-  return target;
-}
-
-function computeBoundary(source) {
-  const scale = source.scale || {};
-
-  if (scale.getPointPositionForValue) {
-    return computeCircularBoundary(source);
-  }
-  return computeLinearBoundary(source);
-}
-
-function findSegmentEnd(start, end, points) {
-  for (;end > start; end--) {
-    const point = points[end];
-    if (!isNaN(point.x) && !isNaN(point.y)) {
-      break;
-    }
-  }
-  return end;
-}
-
-function pointsFromSegments(boundary, line) {
-  const {x = null, y = null} = boundary || {};
-  const linePoints = line.points;
-  const points = [];
-  line.segments.forEach(({start, end}) => {
-    end = findSegmentEnd(start, end, linePoints);
-    const first = linePoints[start];
-    const last = linePoints[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;
-}
-
-/**
- * @param {{ chart: Chart; scale: Scale; index: number; line: LineElement; }} source
- * @return {LineElement}
- */
-function buildStackLine(source) {
-  const {scale, index, line} = source;
-  const points = [];
-  const segments = line.segments;
-  const sourcePoints = line.points;
-  const linesBelow = getLinesBelow(scale, index);
-  linesBelow.push(createBoundaryLine({x: null, y: scale.bottom}, line));
-
-  for (let i = 0; i < segments.length; i++) {
-    const segment = segments[i];
-    for (let j = segment.start; j <= segment.end; j++) {
-      addPointsBelow(points, sourcePoints[j], linesBelow);
-    }
-  }
-  return new LineElement({points, options: {}});
-}
-
-/**
- * @param {Scale} scale
- * @param {number} index
- * @return {LineElement[]}
- */
-function getLinesBelow(scale, index) {
-  const below = [];
-  const metas = scale.getMatchingVisibleMetas('line');
-
-  for (let i = 0; i < metas.length; i++) {
-    const meta = metas[i];
-    if (meta.index === index) {
-      break;
-    }
-    if (!meta.hidden) {
-      below.unshift(meta.dataset);
-    }
-  }
-  return below;
-}
-
-/**
- * @param {PointElement[]} points
- * @param {PointElement} sourcePoint
- * @param {LineElement[]} linesBelow
- */
-function addPointsBelow(points, sourcePoint, linesBelow) {
-  const postponed = [];
-  for (let j = 0; j < linesBelow.length; j++) {
-    const line = linesBelow[j];
-    const {first, last, point} = findPoint(line, sourcePoint, 'x');
-
-    if (!point || (first && last)) {
-      continue;
-    }
-    if (first) {
-      // First point of an segment -> need to add another point before this,
-      // from next line below.
-      postponed.unshift(point);
-    } else {
-      points.push(point);
-      if (!last) {
-        // In the middle of an segment, no need to add more points.
-        break;
-      }
-    }
-  }
-  points.push(...postponed);
-}
-
-/**
- * @param {LineElement} line
- * @param {PointElement} sourcePoint
- * @param {string} property
- * @returns {{point?: PointElement, first?: boolean, last?: boolean}}
- */
-function findPoint(line, sourcePoint, property) {
-  const point = line.interpolate(sourcePoint, property);
-  if (!point) {
-    return {};
-  }
-
-  const pointValue = point[property];
-  const segments = line.segments;
-  const linePoints = line.points;
-  let first = false;
-  let last = false;
-  for (let i = 0; i < segments.length; i++) {
-    const segment = segments[i];
-    const firstValue = linePoints[segment.start][property];
-    const lastValue = linePoints[segment.end][property];
-    if (_isBetween(pointValue, firstValue, lastValue)) {
-      first = pointValue === firstValue;
-      last = pointValue === lastValue;
-      break;
-    }
-  }
-  return {first, last, point};
-}
-
-function getTarget(source) {
-  const {chart, fill, line} = source;
-
-  if (isFinite(fill)) {
-    return getLineByIndex(chart, fill);
-  }
-
-  if (fill === 'stack') {
-    return buildStackLine(source);
-  }
-
-  if (fill === 'shape') {
-    return true;
-  }
-
-  const boundary = computeBoundary(source);
-
-  if (boundary instanceof simpleArc) {
-    return boundary;
-  }
-
-  return createBoundaryLine(boundary, line);
-}
-
-/**
- * @param {PointElement[] | { x: number; y: number; }} boundary
- * @param {LineElement} line
- * @return {LineElement?}
- */
-function createBoundaryLine(boundary, line) {
-  let points = [];
-  let _loop = false;
-
-  if (isArray(boundary)) {
-    _loop = true;
-    // @ts-ignore
-    points = boundary;
-  } else {
-    points = pointsFromSegments(boundary, line);
-  }
-
-  return points.length ? new LineElement({
-    points,
-    options: {tension: 0},
-    _loop,
-    _fullLoop: _loop
-  }) : null;
-}
-
-function resolveTarget(sources, index, propagate) {
-  const source = sources[index];
-  let fill = source.fill;
-  const visited = [index];
-  let target;
-
-  if (!propagate) {
-    return fill;
-  }
-
-  while (fill !== false && visited.indexOf(fill) === -1) {
-    if (!isFinite(fill)) {
-      return fill;
-    }
-
-    target = sources[fill];
-    if (!target) {
-      return false;
-    }
-
-    if (target.visible) {
-      return fill;
-    }
-
-    visited.push(fill);
-    fill = target.fill;
-  }
-
-  return false;
-}
-
-function _clip(ctx, target, clipY) {
-  const {segments, points} = target;
-  let first = true;
-  let lineLoop = false;
-
-  ctx.beginPath();
-  for (const segment of segments) {
-    const {start, end} = segment;
-    const firstPoint = points[start];
-    const lastPoint = points[findSegmentEnd(start, end, points)];
-    if (first) {
-      ctx.moveTo(firstPoint.x, firstPoint.y);
-      first = false;
-    } else {
-      ctx.lineTo(firstPoint.x, clipY);
-      ctx.lineTo(firstPoint.x, firstPoint.y);
-    }
-    lineLoop = !!target.pathSegment(ctx, segment, {move: lineLoop});
-    if (lineLoop) {
-      ctx.closePath();
-    } else {
-      ctx.lineTo(lastPoint.x, clipY);
-    }
-  }
-
-  ctx.lineTo(target.first().x, clipY);
-  ctx.closePath();
-  ctx.clip();
-}
-
-function getBounds(property, first, last, loop) {
-  if (loop) {
-    return;
-  }
-  let start = first[property];
-  let end = last[property];
-
-  if (property === 'angle') {
-    start = _normalizeAngle(start);
-    end = _normalizeAngle(end);
-  }
-  return {property, start, end};
-}
-
-function _getEdge(a, b, prop, fn) {
-  if (a && b) {
-    return fn(a[prop], b[prop]);
-  }
-  return a ? a[prop] : b ? b[prop] : 0;
-}
-
-function _segments(line, target, property) {
-  const segments = line.segments;
-  const points = line.points;
-  const tpoints = target.points;
-  const parts = [];
-
-  for (const segment of segments) {
-    let {start, end} = segment;
-    end = findSegmentEnd(start, end, points);
-
-    const bounds = getBounds(property, points[start], points[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[start],
-        end: points[end]
-      });
-      continue;
-    }
-
-    // Get all segments from `target` that intersect the bounds of current segment of `line`
-    const targetSegments = _boundSegments(target, bounds);
-
-    for (const tgt of targetSegments) {
-      const subBounds = getBounds(property, tpoints[tgt.start], tpoints[tgt.end], tgt.loop);
-      const fillSources = _boundSegment(segment, points, subBounds);
-
-      for (const fillSource of fillSources) {
-        parts.push({
-          source: fillSource,
-          target: tgt,
-          start: {
-            [property]: _getEdge(bounds, subBounds, 'start', Math.max)
-          },
-          end: {
-            [property]: _getEdge(bounds, subBounds, 'end', Math.min)
-          }
-        });
-      }
-    }
-  }
-  return parts;
-}
-
-function clipBounds(ctx, scale, bounds) {
-  const {top, bottom} = scale.chart.chartArea;
-  const {property, start, end} = bounds || {};
-  if (property === 'x') {
-    ctx.beginPath();
-    ctx.rect(start, top, end - start, bottom - top);
-    ctx.clip();
-  }
-}
-
-function interpolatedLineTo(ctx, target, point, property) {
-  const interpolatedPoint = target.interpolate(point, property);
-  if (interpolatedPoint) {
-    ctx.lineTo(interpolatedPoint.x, interpolatedPoint.y);
-  }
-}
-
-function _fill(ctx, cfg) {
-  const {line, target, property, color, scale} = cfg;
-  const segments = _segments(line, target, property);
-
-  for (const {source: src, target: tgt, start, end} of segments) {
-    const {style: {backgroundColor = color} = {}} = src;
-    const notShape = target !== true;
-
-    ctx.save();
-    ctx.fillStyle = backgroundColor;
-
-    clipBounds(ctx, scale, notShape && getBounds(property, start, end));
-
-    ctx.beginPath();
-
-    const lineLoop = !!line.pathSegment(ctx, src);
-
-    let loop;
-    if (notShape) {
-      if (lineLoop) {
-        ctx.closePath();
-      } else {
-        interpolatedLineTo(ctx, target, end, property);
-      }
-
-      const targetLoop = !!target.pathSegment(ctx, tgt, {move: lineLoop, reverse: true});
-      loop = lineLoop && targetLoop;
-      if (!loop) {
-        interpolatedLineTo(ctx, target, start, property);
-      }
-    }
-
-    ctx.closePath();
-    ctx.fill(loop ? 'evenodd' : 'nonzero');
-
-    ctx.restore();
-  }
-}
-
-function doFill(ctx, cfg) {
-  const {line, target, above, below, area, scale} = cfg;
-  const property = line._loop ? 'angle' : cfg.axis;
-
-  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();
-}
-
-function drawfill(ctx, source, area) {
-  const target = getTarget(source);
-  const {line, scale, axis} = source;
-  const lineOpts = line.options;
-  const fillOption = lineOpts.fill;
-  const color = lineOpts.backgroundColor;
-  const {above = color, below = color} = fillOption || {};
-  if (target && line.points.length) {
-    clipArea(ctx, area);
-    doFill(ctx, {line, target, above, below, area, scale, axis});
-    unclipArea(ctx);
-  }
-}
-
-export default {
-  id: 'filler',
-
-  afterDatasetsUpdate(chart, _args, options) {
-    const count = (chart.data.datasets || []).length;
-    const sources = [];
-    let meta, i, line, source;
-
-    for (i = 0; i < count; ++i) {
-      meta = chart.getDatasetMeta(i);
-      line = meta.dataset;
-      source = null;
-
-      if (line && line.options && line instanceof LineElement) {
-        source = {
-          visible: chart.isDatasetVisible(i),
-          index: i,
-          fill: decodeFill(line, i, count),
-          chart,
-          axis: meta.controller.options.indexAxis,
-          scale: meta.vScale,
-          line,
-        };
-      }
-
-      meta.$filler = source;
-      sources.push(source);
-    }
-
-    for (i = 0; i < count; ++i) {
-      source = sources[i];
-      if (!source || source.fill === false) {
-        continue;
-      }
-
-      source.fill = resolveTarget(sources, i, options.propagate);
-    }
-  },
-
-  beforeDraw(chart, _args, options) {
-    const draw = options.drawTime === 'beforeDraw';
-    const metasets = chart.getSortedVisibleDatasetMetas();
-    const area = chart.chartArea;
-    for (let i = metasets.length - 1; i >= 0; --i) {
-      const source = metasets[i].$filler;
-      if (!source) {
-        continue;
-      }
-
-      source.line.updateControlPoints(area, source.axis);
-      if (draw) {
-        drawfill(chart.ctx, source, area);
-      }
-    }
-  },
-
-  beforeDatasetsDraw(chart, _args, options) {
-    if (options.drawTime !== 'beforeDatasetsDraw') {
-      return;
-    }
-    const metasets = chart.getSortedVisibleDatasetMetas();
-    for (let i = metasets.length - 1; i >= 0; --i) {
-      const source = metasets[i].$filler;
-      if (source) {
-        drawfill(chart.ctx, source, chart.chartArea);
-      }
-    }
-  },
-
-  beforeDatasetDraw(chart, args, options) {
-    const source = args.meta.$filler;
-
-    if (!source || source.fill === false || options.drawTime !== 'beforeDatasetDraw') {
-      return;
-    }
-
-    drawfill(chart.ctx, source, chart.chartArea);
-  },
-
-  defaults: {
-    propagate: true,
-    drawTime: 'beforeDatasetDraw'
-  }
-};
diff --git a/src/plugins/plugin.filler/filler.drawing.js b/src/plugins/plugin.filler/filler.drawing.js
new file mode 100644 (file)
index 0000000..3fe4c36
--- /dev/null
@@ -0,0 +1,122 @@
+import {clipArea, unclipArea} from '../../helpers';
+import {_findSegmentEnd, _getBounds, _segments} from './filler.segment';
+import {_getTarget} from './filler.target';
+
+export function _drawfill(ctx, source, area) {
+  const target = _getTarget(source);
+  const {line, scale, axis} = source;
+  const lineOpts = line.options;
+  const fillOption = lineOpts.fill;
+  const color = lineOpts.backgroundColor;
+  const {above = color, below = color} = fillOption || {};
+  if (target && line.points.length) {
+    clipArea(ctx, area);
+    doFill(ctx, {line, target, above, below, area, scale, axis});
+    unclipArea(ctx);
+  }
+}
+
+function doFill(ctx, cfg) {
+  const {line, target, above, below, area, scale} = cfg;
+  const property = line._loop ? 'angle' : cfg.axis;
+
+  ctx.save();
+
+  if (property === 'x' && below !== above) {
+    clipVertical(ctx, target, area.top);
+    fill(ctx, {line, target, color: above, scale, property});
+    ctx.restore();
+    ctx.save();
+    clipVertical(ctx, target, area.bottom);
+  }
+  fill(ctx, {line, target, color: below, scale, property});
+
+  ctx.restore();
+}
+
+function clipVertical(ctx, target, clipY) {
+  const {segments, points} = target;
+  let first = true;
+  let lineLoop = false;
+
+  ctx.beginPath();
+  for (const segment of segments) {
+    const {start, end} = segment;
+    const firstPoint = points[start];
+    const lastPoint = points[_findSegmentEnd(start, end, points)];
+    if (first) {
+      ctx.moveTo(firstPoint.x, firstPoint.y);
+      first = false;
+    } else {
+      ctx.lineTo(firstPoint.x, clipY);
+      ctx.lineTo(firstPoint.x, firstPoint.y);
+    }
+    lineLoop = !!target.pathSegment(ctx, segment, {move: lineLoop});
+    if (lineLoop) {
+      ctx.closePath();
+    } else {
+      ctx.lineTo(lastPoint.x, clipY);
+    }
+  }
+
+  ctx.lineTo(target.first().x, clipY);
+  ctx.closePath();
+  ctx.clip();
+}
+
+function fill(ctx, cfg) {
+  const {line, target, property, color, scale} = cfg;
+  const segments = _segments(line, target, property);
+
+  for (const {source: src, target: tgt, start, end} of segments) {
+    const {style: {backgroundColor = color} = {}} = src;
+    const notShape = target !== true;
+
+    ctx.save();
+    ctx.fillStyle = backgroundColor;
+
+    clipBounds(ctx, scale, notShape && _getBounds(property, start, end));
+
+    ctx.beginPath();
+
+    const lineLoop = !!line.pathSegment(ctx, src);
+
+    let loop;
+    if (notShape) {
+      if (lineLoop) {
+        ctx.closePath();
+      } else {
+        interpolatedLineTo(ctx, target, end, property);
+      }
+
+      const targetLoop = !!target.pathSegment(ctx, tgt, {move: lineLoop, reverse: true});
+      loop = lineLoop && targetLoop;
+      if (!loop) {
+        interpolatedLineTo(ctx, target, start, property);
+      }
+    }
+
+    ctx.closePath();
+    ctx.fill(loop ? 'evenodd' : 'nonzero');
+
+    ctx.restore();
+  }
+}
+
+function clipBounds(ctx, scale, bounds) {
+  const {top, bottom} = scale.chart.chartArea;
+  const {property, start, end} = bounds || {};
+  if (property === 'x') {
+    ctx.beginPath();
+    ctx.rect(start, top, end - start, bottom - top);
+    ctx.clip();
+  }
+}
+
+function interpolatedLineTo(ctx, target, point, property) {
+  const interpolatedPoint = target.interpolate(point, property);
+  if (interpolatedPoint) {
+    ctx.lineTo(interpolatedPoint.x, interpolatedPoint.y);
+  }
+}
+
diff --git a/src/plugins/plugin.filler/filler.options.js b/src/plugins/plugin.filler/filler.options.js
new file mode 100644 (file)
index 0000000..bc22cec
--- /dev/null
@@ -0,0 +1,136 @@
+import {isObject, isFinite, valueOrDefault} from '../../helpers/helpers.core';
+
+/**
+ * @typedef { import('../../core/core.scale').default } Scale
+ * @typedef { import('../../elements/element.line').default } LineElement
+ * @typedef { import('../../../types/index.esm').FillTarget } FillTarget
+ * @typedef { import('../../../types/index.esm').ComplexFillTarget } ComplexFillTarget
+ */
+
+export function _resolveTarget(sources, index, propagate) {
+  const source = sources[index];
+  let fill = source.fill;
+  const visited = [index];
+  let target;
+
+  if (!propagate) {
+    return fill;
+  }
+
+  while (fill !== false && visited.indexOf(fill) === -1) {
+    if (!isFinite(fill)) {
+      return fill;
+    }
+
+    target = sources[fill];
+    if (!target) {
+      return false;
+    }
+
+    if (target.visible) {
+      return fill;
+    }
+
+    visited.push(fill);
+    fill = target.fill;
+  }
+
+  return false;
+}
+
+/**
+ * @param {LineElement} line
+ * @param {number} index
+ * @param {number} count
+ */
+export function _decodeFill(line, index, count) {
+  const fill = parseFillOption(line);
+
+  if (isObject(fill)) {
+    return isNaN(fill.value) ? false : fill;
+  }
+
+  let target = parseFloat(fill);
+
+  if (isFinite(target) && Math.floor(target) === target) {
+    return decodeTargetIndex(fill[0], index, target, count);
+  }
+
+  return ['origin', 'start', 'end', 'stack', 'shape'].indexOf(fill) >= 0 && fill;
+}
+
+function decodeTargetIndex(firstCh, index, target, count) {
+  if (firstCh === '-' || firstCh === '+') {
+    target = index + target;
+  }
+
+  if (target === index || target < 0 || target >= count) {
+    return false;
+  }
+
+  return target;
+}
+
+/**
+ * @param {FillTarget | ComplexFillTarget} fill
+ * @param {Scale} scale
+ * @returns {number | null}
+ */
+export function _getTargetPixel(fill, scale) {
+  let pixel = null;
+  if (fill === 'start') {
+    pixel = scale.bottom;
+  } else if (fill === 'end') {
+    pixel = scale.top;
+  } else if (isObject(fill)) {
+    // @ts-ignore
+    pixel = scale.getPixelForValue(fill.value);
+  } else if (scale.getBasePixel) {
+    pixel = scale.getBasePixel();
+  }
+  return pixel;
+}
+
+/**
+ * @param {FillTarget | ComplexFillTarget} fill
+ * @param {Scale} scale
+ * @param {number} startValue
+ * @returns {number | undefined}
+ */
+export function _getTargetValue(fill, scale, startValue) {
+  let value;
+
+  if (fill === 'start') {
+    value = startValue;
+  } else if (fill === 'end') {
+    value = scale.options.reverse ? scale.min : scale.max;
+  } else if (isObject(fill)) {
+    // @ts-ignore
+    value = fill.value;
+  } else {
+    value = scale.getBaseValue();
+  }
+  return value;
+}
+
+/**
+ * @param {LineElement} line
+ */
+function parseFillOption(line) {
+  const options = line.options;
+  const fillOption = options.fill;
+  let fill = valueOrDefault(fillOption && fillOption.target, fillOption);
+
+  if (fill === undefined) {
+    fill = !!options.backgroundColor;
+  }
+
+  if (fill === false || fill === null) {
+    return false;
+  }
+
+  if (fill === true) {
+    return 'origin';
+  }
+  return fill;
+}
diff --git a/src/plugins/plugin.filler/filler.segment.js b/src/plugins/plugin.filler/filler.segment.js
new file mode 100644 (file)
index 0000000..8d78f69
--- /dev/null
@@ -0,0 +1,99 @@
+import {_boundSegment, _boundSegments, _normalizeAngle} from '../../helpers';
+
+export function _segments(line, target, property) {
+  const segments = line.segments;
+  const points = line.points;
+  const tpoints = target.points;
+  const parts = [];
+
+  for (const segment of segments) {
+    let {start, end} = segment;
+    end = _findSegmentEnd(start, end, points);
+
+    const bounds = _getBounds(property, points[start], points[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[start],
+        end: points[end]
+      });
+      continue;
+    }
+
+    // Get all segments from `target` that intersect the bounds of current segment of `line`
+    const targetSegments = _boundSegments(target, bounds);
+
+    for (const tgt of targetSegments) {
+      const subBounds = _getBounds(property, tpoints[tgt.start], tpoints[tgt.end], tgt.loop);
+      const fillSources = _boundSegment(segment, points, subBounds);
+
+      for (const fillSource of fillSources) {
+        parts.push({
+          source: fillSource,
+          target: tgt,
+          start: {
+            [property]: _getEdge(bounds, subBounds, 'start', Math.max)
+          },
+          end: {
+            [property]: _getEdge(bounds, subBounds, 'end', Math.min)
+          }
+        });
+      }
+    }
+  }
+  return parts;
+}
+
+export function _getBounds(property, first, last, loop) {
+  if (loop) {
+    return;
+  }
+  let start = first[property];
+  let end = last[property];
+
+  if (property === 'angle') {
+    start = _normalizeAngle(start);
+    end = _normalizeAngle(end);
+  }
+  return {property, start, end};
+}
+
+export function _pointsFromSegments(boundary, line) {
+  const {x = null, y = null} = boundary || {};
+  const linePoints = line.points;
+  const points = [];
+  line.segments.forEach(({start, end}) => {
+    end = _findSegmentEnd(start, end, linePoints);
+    const first = linePoints[start];
+    const last = linePoints[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;
+}
+
+export function _findSegmentEnd(start, end, points) {
+  for (;end > start; end--) {
+    const point = points[end];
+    if (!isNaN(point.x) && !isNaN(point.y)) {
+      break;
+    }
+  }
+  return end;
+}
+
+function _getEdge(a, b, prop, fn) {
+  if (a && b) {
+    return fn(a[prop], b[prop]);
+  }
+  return a ? a[prop] : b ? b[prop] : 0;
+}
diff --git a/src/plugins/plugin.filler/filler.target.js b/src/plugins/plugin.filler/filler.target.js
new file mode 100644 (file)
index 0000000..e7dbf3d
--- /dev/null
@@ -0,0 +1,121 @@
+import {LineElement} from '../../elements';
+import {isArray, isFinite} from '../../helpers';
+import {_getTargetPixel, _getTargetValue} from './filler.options';
+import {_pointsFromSegments} from './filler.segment';
+import {_buildStackLine} from './filler.target.stack';
+import {simpleArc} from './simpleArc';
+
+/**
+ * @typedef { import('../../core/core.controller').default } Chart
+ * @typedef { import('../../core/core.scale').default } Scale
+ * @typedef { import('../../elements/element.point').default } PointElement
+ */
+
+export function _getTarget(source) {
+  const {chart, fill, line} = source;
+
+  if (isFinite(fill)) {
+    return getLineByIndex(chart, fill);
+  }
+
+  if (fill === 'stack') {
+    return _buildStackLine(source);
+  }
+
+  if (fill === 'shape') {
+    return true;
+  }
+
+  const boundary = computeBoundary(source);
+
+  if (boundary instanceof simpleArc) {
+    return boundary;
+  }
+
+  return _createBoundaryLine(boundary, line);
+}
+
+/**
+ * @param {PointElement[] | { x: number; y: number; }} boundary
+ * @param {LineElement} line
+ * @return {LineElement?}
+ */
+export function _createBoundaryLine(boundary, line) {
+  let points = [];
+  let _loop = false;
+
+  if (isArray(boundary)) {
+    _loop = true;
+    // @ts-ignore
+    points = boundary;
+  } else {
+    points = _pointsFromSegments(boundary, line);
+  }
+
+  return points.length ? new LineElement({
+    points,
+    options: {tension: 0},
+    _loop,
+    _fullLoop: _loop
+  }) : null;
+}
+
+/**
+ * @param {Chart} chart
+ * @param {number} index
+ */
+function getLineByIndex(chart, index) {
+  const meta = chart.getDatasetMeta(index);
+  const visible = meta && chart.isDatasetVisible(index);
+  return visible ? meta.dataset : null;
+}
+
+function computeBoundary(source) {
+  const scale = source.scale || {};
+
+  if (scale.getPointPositionForValue) {
+    return computeCircularBoundary(source);
+  }
+  return computeLinearBoundary(source);
+}
+
+
+function computeLinearBoundary(source) {
+  const {scale = {}, fill} = source;
+  const pixel = _getTargetPixel(fill, scale);
+
+  if (isFinite(pixel)) {
+    const horizontal = scale.isHorizontal();
+
+    return {
+      x: horizontal ? pixel : null,
+      y: horizontal ? null : pixel
+    };
+  }
+
+  return null;
+}
+
+function computeCircularBoundary(source) {
+  const {scale, fill} = source;
+  const options = scale.options;
+  const length = scale.getLabels().length;
+  const start = options.reverse ? scale.max : scale.min;
+  const value = _getTargetValue(fill, scale, start);
+  const target = [];
+
+  if (options.grid.circular) {
+    const center = scale.getPointPositionForValue(0, start);
+    return new simpleArc({
+      x: center.x,
+      y: center.y,
+      radius: scale.getDistanceFromCenterForValue(value)
+    });
+  }
+
+  for (let i = 0; i < length; ++i) {
+    target.push(scale.getPointPositionForValue(i, value));
+  }
+  return target;
+}
+
diff --git a/src/plugins/plugin.filler/filler.target.stack.js b/src/plugins/plugin.filler/filler.target.stack.js
new file mode 100644 (file)
index 0000000..dcb974b
--- /dev/null
@@ -0,0 +1,110 @@
+/**
+ * @typedef { import('../../core/core.controller').default } Chart
+ * @typedef { import('../../core/core.scale').default } Scale
+ * @typedef { import('../../elements/element.point').default } PointElement
+ */
+
+import {LineElement} from '../../elements';
+import {_isBetween} from '../../helpers';
+import {_createBoundaryLine} from './filler.target';
+
+/**
+ * @param {{ chart: Chart; scale: Scale; index: number; line: LineElement; }} source
+ * @return {LineElement}
+ */
+export function _buildStackLine(source) {
+  const {scale, index, line} = source;
+  const points = [];
+  const segments = line.segments;
+  const sourcePoints = line.points;
+  const linesBelow = getLinesBelow(scale, index);
+  linesBelow.push(_createBoundaryLine({x: null, y: scale.bottom}, line));
+
+  for (let i = 0; i < segments.length; i++) {
+    const segment = segments[i];
+    for (let j = segment.start; j <= segment.end; j++) {
+      addPointsBelow(points, sourcePoints[j], linesBelow);
+    }
+  }
+  return new LineElement({points, options: {}});
+}
+
+/**
+ * @param {Scale} scale
+ * @param {number} index
+ * @return {LineElement[]}
+ */
+function getLinesBelow(scale, index) {
+  const below = [];
+  const metas = scale.getMatchingVisibleMetas('line');
+
+  for (let i = 0; i < metas.length; i++) {
+    const meta = metas[i];
+    if (meta.index === index) {
+      break;
+    }
+    if (!meta.hidden) {
+      below.unshift(meta.dataset);
+    }
+  }
+  return below;
+}
+
+/**
+ * @param {PointElement[]} points
+ * @param {PointElement} sourcePoint
+ * @param {LineElement[]} linesBelow
+ */
+function addPointsBelow(points, sourcePoint, linesBelow) {
+  const postponed = [];
+  for (let j = 0; j < linesBelow.length; j++) {
+    const line = linesBelow[j];
+    const {first, last, point} = findPoint(line, sourcePoint, 'x');
+
+    if (!point || (first && last)) {
+      continue;
+    }
+    if (first) {
+      // First point of an segment -> need to add another point before this,
+      // from next line below.
+      postponed.unshift(point);
+    } else {
+      points.push(point);
+      if (!last) {
+        // In the middle of an segment, no need to add more points.
+        break;
+      }
+    }
+  }
+  points.push(...postponed);
+}
+
+/**
+ * @param {LineElement} line
+ * @param {PointElement} sourcePoint
+ * @param {string} property
+ * @returns {{point?: PointElement, first?: boolean, last?: boolean}}
+ */
+function findPoint(line, sourcePoint, property) {
+  const point = line.interpolate(sourcePoint, property);
+  if (!point) {
+    return {};
+  }
+
+  const pointValue = point[property];
+  const segments = line.segments;
+  const linePoints = line.points;
+  let first = false;
+  let last = false;
+  for (let i = 0; i < segments.length; i++) {
+    const segment = segments[i];
+    const firstValue = linePoints[segment.start][property];
+    const lastValue = linePoints[segment.end][property];
+    if (_isBetween(pointValue, firstValue, lastValue)) {
+      first = pointValue === firstValue;
+      last = pointValue === lastValue;
+      break;
+    }
+  }
+  return {first, last, point};
+}
diff --git a/src/plugins/plugin.filler/index.js b/src/plugins/plugin.filler/index.js
new file mode 100644 (file)
index 0000000..7cbf593
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * Plugin based on discussion from the following Chart.js issues:
+ * @see https://github.com/chartjs/Chart.js/issues/2380#issuecomment-279961569
+ * @see https://github.com/chartjs/Chart.js/issues/2440#issuecomment-256461897
+ */
+
+import LineElement from '../../elements/element.line';
+import {_drawfill} from './filler.drawing';
+import {_decodeFill, _resolveTarget} from './filler.options';
+
+export default {
+  id: 'filler',
+
+  afterDatasetsUpdate(chart, _args, options) {
+    const count = (chart.data.datasets || []).length;
+    const sources = [];
+    let meta, i, line, source;
+
+    for (i = 0; i < count; ++i) {
+      meta = chart.getDatasetMeta(i);
+      line = meta.dataset;
+      source = null;
+
+      if (line && line.options && line instanceof LineElement) {
+        source = {
+          visible: chart.isDatasetVisible(i),
+          index: i,
+          fill: _decodeFill(line, i, count),
+          chart,
+          axis: meta.controller.options.indexAxis,
+          scale: meta.vScale,
+          line,
+        };
+      }
+
+      meta.$filler = source;
+      sources.push(source);
+    }
+
+    for (i = 0; i < count; ++i) {
+      source = sources[i];
+      if (!source || source.fill === false) {
+        continue;
+      }
+
+      source.fill = _resolveTarget(sources, i, options.propagate);
+    }
+  },
+
+  beforeDraw(chart, _args, options) {
+    const draw = options.drawTime === 'beforeDraw';
+    const metasets = chart.getSortedVisibleDatasetMetas();
+    const area = chart.chartArea;
+    for (let i = metasets.length - 1; i >= 0; --i) {
+      const source = metasets[i].$filler;
+      if (!source) {
+        continue;
+      }
+
+      source.line.updateControlPoints(area, source.axis);
+      if (draw) {
+        _drawfill(chart.ctx, source, area);
+      }
+    }
+  },
+
+  beforeDatasetsDraw(chart, _args, options) {
+    if (options.drawTime !== 'beforeDatasetsDraw') {
+      return;
+    }
+    const metasets = chart.getSortedVisibleDatasetMetas();
+    for (let i = metasets.length - 1; i >= 0; --i) {
+      const source = metasets[i].$filler;
+      if (source) {
+        _drawfill(chart.ctx, source, chart.chartArea);
+      }
+    }
+  },
+
+  beforeDatasetDraw(chart, args, options) {
+    const source = args.meta.$filler;
+
+    if (!source || source.fill === false || options.drawTime !== 'beforeDatasetDraw') {
+      return;
+    }
+
+    _drawfill(chart.ctx, source, chart.chartArea);
+  },
+
+  defaults: {
+    propagate: true,
+    drawTime: 'beforeDatasetDraw'
+  }
+};
diff --git a/src/plugins/plugin.filler/simpleArc.js b/src/plugins/plugin.filler/simpleArc.js
new file mode 100644 (file)
index 0000000..0606a52
--- /dev/null
@@ -0,0 +1,27 @@
+import {TAU} from '../../helpers';
+
+// TODO: use elements.ArcElement instead
+export class simpleArc {
+  constructor(opts) {
+    this.x = opts.x;
+    this.y = opts.y;
+    this.radius = opts.radius;
+  }
+
+  pathSegment(ctx, bounds, opts) {
+    const {x, y, radius} = this;
+    bounds = bounds || {start: 0, end: TAU};
+    ctx.arc(x, y, radius, bounds.end, bounds.start, true);
+    return !opts.bounds;
+  }
+
+  interpolate(point) {
+    const {x, y, radius} = this;
+    const angle = point.angle;
+    return {
+      x: x + Math.cos(angle) * radius,
+      y: y + Math.sin(angle) * radius,
+      angle
+    };
+  }
+}