Use interpolation in fill: 'stack' (and fix interpolation) (#7711)

* Add tests and fix _boundSegment
* Use interpolate for finding points below
* Remove _refPoints logic (getTarget in draw)
This commit is contained in:
Jukka Kurkela 2020-08-16 18:18:46 +03:00 committed by GitHub
parent 689befa807
commit dd261b22f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 93 additions and 48 deletions

View File

@ -67,7 +67,7 @@ module.exports = function(karma) {
{pattern: 'test/BasicChartWebWorker.js', included: false}, {pattern: 'test/BasicChartWebWorker.js', included: false},
{pattern: 'src/index.js', watched: false}, {pattern: 'src/index.js', watched: false},
'node_modules/chartjs-adapter-moment/dist/chartjs-adapter-moment.js', 'node_modules/chartjs-adapter-moment/dist/chartjs-adapter-moment.js',
{pattern: specPattern, watched: false} {pattern: specPattern}
], ],
preprocessors: { preprocessors: {

View File

@ -78,12 +78,18 @@ export function _boundSegment(segment, points, bounds) {
const count = points.length; const count = points.length;
const {compare, between, normalize} = propertyFn(property); const {compare, between, normalize} = propertyFn(property);
const {start, end, loop} = getSegment(segment, points, bounds); const {start, end, loop} = getSegment(segment, points, bounds);
const result = []; const result = [];
let inside = false; let inside = false;
let subStart = null; let subStart = null;
let i, value, point, prev; let value, point, prevValue;
for (i = start; i <= end; ++i) { const startIsBefore = () => between(startBound, prevValue, value) && compare(startBound, prevValue) !== 0;
const endIsBefore = () => compare(endBound, value) === 0 || between(endBound, prevValue, value);
const shouldStart = () => inside || startIsBefore();
const shouldStop = () => !inside || endIsBefore();
for (let i = start, prev = start; i <= end; ++i) {
point = points[i % count]; point = points[i % count];
if (point.skip) { if (point.skip) {
@ -93,15 +99,16 @@ export function _boundSegment(segment, points, bounds) {
value = normalize(point[property]); value = normalize(point[property]);
inside = between(value, startBound, endBound); inside = between(value, startBound, endBound);
if (subStart === null && inside) { if (subStart === null && shouldStart()) {
subStart = i > start && compare(value, startBound) > 0 ? prev : i; subStart = compare(value, startBound) === 0 ? i : prev;
} }
if (subStart !== null && (!inside || compare(value, endBound) === 0)) { if (subStart !== null && shouldStop()) {
result.push(makeSubSegment(subStart, i, loop, count)); result.push(makeSubSegment(subStart, i, loop, count));
subStart = null; subStart = null;
} }
prev = i; prev = i;
prevValue = value;
} }
if (subStart !== null) { if (subStart !== null) {
@ -111,6 +118,7 @@ export function _boundSegment(segment, points, bounds) {
return result; return result;
} }
/** /**
* Returns the segments of the line that are inside given bounds * Returns the segments of the line that are inside given bounds
* @param {Line} line * @param {Line} line

View File

@ -10,6 +10,7 @@ import * as interpolation from './helpers.interpolation';
import * as options from './helpers.options'; import * as options from './helpers.options';
import * as math from './helpers.math'; import * as math from './helpers.math';
import * as rtl from './helpers.rtl'; import * as rtl from './helpers.rtl';
import * as segment from './helpers.segment';
import {color, getHoverColor} from './helpers.color'; import {color, getHoverColor} from './helpers.color';
import {requestAnimFrame, fontString} from './helpers.extras'; import {requestAnimFrame, fontString} from './helpers.extras';
@ -25,6 +26,7 @@ export default {
options, options,
math, math,
rtl, rtl,
segment,
requestAnimFrame, requestAnimFrame,
// -- Canvas methods // -- Canvas methods

View File

@ -170,11 +170,11 @@ function pointsFromSegments(boundary, line) {
const first = linePoints[segment.start]; const first = linePoints[segment.start];
const last = linePoints[segment.end]; const last = linePoints[segment.end];
if (y !== null) { if (y !== null) {
points.push({x: first.x, y, _prop: 'x', _ref: first}); points.push({x: first.x, y});
points.push({x: last.x, y, _prop: 'x', _ref: last}); points.push({x: last.x, y});
} else if (x !== null) { } else if (x !== null) {
points.push({x, y: first.y, _prop: 'y', _ref: first}); points.push({x, y: first.y});
points.push({x, y: last.y, _prop: 'y', _ref: last}); points.push({x, y: last.y});
} }
}); });
return points; return points;
@ -186,13 +186,11 @@ function pointsFromSegments(boundary, line) {
*/ */
function buildStackLine(source) { function buildStackLine(source) {
const {chart, scale, index, line} = source; const {chart, scale, index, line} = source;
const linesBelow = getLinesBelow(chart, index);
const points = []; const points = [];
const segments = line.segments; const segments = line.segments;
const sourcePoints = line.points; const sourcePoints = line.points;
const startPoints = []; const linesBelow = getLinesBelow(chart, index);
sourcePoints.forEach(point => startPoints.push({x: point.x, y: scale.bottom, _prop: 'x', _ref: point})); linesBelow.push(createBoundaryLine({x: null, y: scale.bottom}, line));
linesBelow.push(new Line({points: startPoints, options: {}}));
for (let i = 0; i < segments.length; i++) { for (let i = 0; i < segments.length; i++) {
const segment = segments[i]; const segment = segments[i];
@ -200,9 +198,11 @@ function buildStackLine(source) {
addPointsBelow(points, sourcePoints[j], linesBelow); addPointsBelow(points, sourcePoints[j], linesBelow);
} }
} }
return new Line({points, options: {}, _refPoints: true}); return new Line({points, options: {}});
} }
const isLineAndNotInHideAnimation = (meta) => meta.type === 'line' && !meta.hidden;
/** /**
* @param {Chart} chart * @param {Chart} chart
* @param {number} index * @param {number} index
@ -211,12 +211,13 @@ function buildStackLine(source) {
function getLinesBelow(chart, index) { function getLinesBelow(chart, index) {
const below = []; const below = [];
const metas = chart.getSortedVisibleDatasetMetas(); const metas = chart.getSortedVisibleDatasetMetas();
for (let i = 0; i < metas.length; i++) { for (let i = 0; i < metas.length; i++) {
const meta = metas[i]; const meta = metas[i];
if (meta.index === index) { if (meta.index === index) {
break; break;
} }
if (meta.type === 'line') { if (isLineAndNotInHideAnimation(meta)) {
below.unshift(meta.dataset); below.unshift(meta.dataset);
} }
} }
@ -259,22 +260,27 @@ function addPointsBelow(points, sourcePoint, linesBelow) {
* @returns {{point?: Point, first?: boolean, last?: boolean}} * @returns {{point?: Point, first?: boolean, last?: boolean}}
*/ */
function findPoint(line, sourcePoint, property) { function findPoint(line, sourcePoint, property) {
const point = line.interpolate(sourcePoint, property);
if (!point) {
return {};
}
const pointValue = point[property];
const segments = line.segments; const segments = line.segments;
const linePoints = line.points; const linePoints = line.points;
let first = false;
let last = false;
for (let i = 0; i < segments.length; i++) { for (let i = 0; i < segments.length; i++) {
const segment = segments[i]; const segment = segments[i];
for (let j = segment.start; j <= segment.end; j++) { const firstValue = linePoints[segment.start][property];
const point = linePoints[j]; const lastValue = linePoints[segment.end][property];
if (sourcePoint[property] === point[property]) { if (pointValue >= firstValue && pointValue <= lastValue) {
return { first = pointValue === firstValue;
first: j === segment.start, last = pointValue === lastValue;
last: j === segment.end, break;
point
};
}
} }
} }
return {}; return {first, last, point};
} }
function getTarget(source) { function getTarget(source) {
@ -305,7 +311,6 @@ function getTarget(source) {
function createBoundaryLine(boundary, line) { function createBoundaryLine(boundary, line) {
let points = []; let points = [];
let _loop = false; let _loop = false;
let _refPoints = false;
if (isArray(boundary)) { if (isArray(boundary)) {
_loop = true; _loop = true;
@ -313,15 +318,13 @@ function createBoundaryLine(boundary, line) {
points = boundary; points = boundary;
} else { } else {
points = pointsFromSegments(boundary, line); points = pointsFromSegments(boundary, line);
_refPoints = true;
} }
return points.length ? new Line({ return points.length ? new Line({
points, points,
options: {tension: 0}, options: {tension: 0},
_loop, _loop,
_fullLoop: _loop, _fullLoop: _loop
_refPoints
}) : null; }) : null;
} }
@ -392,17 +395,6 @@ function _segments(line, target, property) {
const tpoints = target.points; const tpoints = target.points;
const parts = []; const parts = [];
if (target._refPoints) {
// Update properties from reference points. (In case those points are animating)
for (let i = 0, ilen = tpoints.length; i < ilen; ++i) {
const point = tpoints[i];
const prop = point._prop;
if (prop) {
point[prop] = point._ref[prop];
}
}
}
for (let i = 0; i < segments.length; i++) { for (let i = 0; i < segments.length; i++) {
const segment = segments[i]; const segment = segments[i];
const bounds = getBounds(property, points[segment.start], points[segment.end], segment.loop); const bounds = getBounds(property, points[segment.start], points[segment.end], segment.loop);
@ -464,7 +456,7 @@ function interpolatedLineTo(ctx, target, point, property) {
function _fill(ctx, cfg) { function _fill(ctx, cfg) {
const {line, target, property, color, scale} = cfg; const {line, target, property, color, scale} = cfg;
const segments = _segments(cfg.line, cfg.target, property); const segments = _segments(line, target, property);
ctx.fillStyle = color; ctx.fillStyle = color;
for (let i = 0, ilen = segments.length; i < ilen; ++i) { for (let i = 0, ilen = segments.length; i < ilen; ++i) {
@ -535,8 +527,7 @@ export default {
fill: decodeFill(line, i, count), fill: decodeFill(line, i, count),
chart, chart,
scale: meta.vScale, scale: meta.vScale,
line, line
target: undefined
}; };
} }
@ -551,7 +542,6 @@ export default {
} }
source.fill = resolveTarget(sources, i, propagate); source.fill = resolveTarget(sources, i, propagate);
source.target = source.fill !== false && getTarget(source);
} }
}, },
@ -572,13 +562,14 @@ export default {
beforeDatasetDraw(chart, args) { beforeDatasetDraw(chart, args) {
const area = chart.chartArea; const area = chart.chartArea;
const ctx = chart.ctx; const ctx = chart.ctx;
const meta = args.meta.$filler; const source = args.meta.$filler;
if (!meta || meta.fill === false) { if (!source || source.fill === false) {
return; return;
} }
const {line, target, scale} = meta; const target = getTarget(source);
const {line, scale} = source;
const lineOpts = line.options; const lineOpts = line.options;
const fillOption = lineOpts.fill; const fillOption = lineOpts.fill;
const color = lineOpts.backgroundColor; const color = lineOpts.backgroundColor;

View File

@ -0,0 +1,44 @@
const {_boundSegment} = Chart.helpers.segment;
describe('helpers.segments', function() {
describe('_boundSegment', function() {
const points = [{x: 10, y: 1}, {x: 20, y: 2}, {x: 30, y: 3}];
const segment = {start: 0, end: 2, loop: false};
it('should not find segment from before the line', function() {
expect(_boundSegment(segment, points, {property: 'x', start: 5, end: 9.99999})).toEqual([]);
});
it('should not find segment from after the line', function() {
expect(_boundSegment(segment, points, {property: 'x', start: 30.00001, end: 800})).toEqual([]);
});
it('should find segment when starting before line', function() {
expect(_boundSegment(segment, points, {property: 'x', start: 5, end: 15})).toEqual([{start: 0, end: 1, loop: false}]);
});
it('should find segment directly on point', function() {
expect(_boundSegment(segment, points, {property: 'x', start: 10, end: 10})).toEqual([{start: 0, end: 0, loop: false}]);
});
it('should find segment from range between points', function() {
expect(_boundSegment(segment, points, {property: 'x', start: 11, end: 14})).toEqual([{start: 0, end: 1, loop: false}]);
});
it('should find segment from point between points', function() {
expect(_boundSegment(segment, points, {property: 'x', start: 22, end: 22})).toEqual([{start: 1, end: 2, loop: false}]);
});
it('should find whole segment', function() {
expect(_boundSegment(segment, points, {property: 'x', start: 0, end: 50})).toEqual([{start: 0, end: 2, loop: false}]);
});
it('should find correct segment from near points', function() {
expect(_boundSegment(segment, points, {property: 'x', start: 10.001, end: 29.999})).toEqual([{start: 0, end: 2, loop: false}]);
});
it('should find segment from after the line', function() {
expect(_boundSegment(segment, points, {property: 'x', start: 25, end: 35})).toEqual([{start: 1, end: 2, loop: false}]);
});
});
});