Skip to content

Commit

Permalink
Filler: support segment backgroundColor (#8864)
Browse files Browse the repository at this point in the history
  • Loading branch information
kurkle committed Apr 9, 2021
1 parent 735764a commit ba84cc5
Show file tree
Hide file tree
Showing 10 changed files with 86 additions and 32 deletions.
2 changes: 1 addition & 1 deletion docs/charts/line.md
Expand Up @@ -161,7 +161,7 @@ If left untouched (`undefined`), the global `options.elements.line.cubicInterpol

### Segment

Line segment styles can be overridden by scriptable options in the `segment` object. Currently all of the `border*` options are supported. The segment styles are resolved for each section of the line between each point. `undefined` fallbacks to main line styles.
Line segment styles can be overridden by scriptable options in the `segment` object. Currently all of the `border*` and `backgroundColor` options are supported. The segment styles are resolved for each section of the line between each point. `undefined` fallbacks to main line styles.

Context for the scriptable segment contains the following properties:

Expand Down
17 changes: 10 additions & 7 deletions src/helpers/helpers.segment.js
Expand Up @@ -21,11 +21,12 @@ function propertyFn(property) {
};
}

function makeSubSegment(start, end, loop, count) {
function normalizeSegment({start, end, count, loop, style}) {
return {
start: start % count,
end: end % count,
loop: loop && (end - start + 1) % count === 0
loop: loop && (end - start + 1) % count === 0,
style
};
}

Expand Down Expand Up @@ -54,7 +55,7 @@ function getSegment(segment, points, bounds) {
if (end < start) {
end += count;
}
return {start, end, loop};
return {start, end, loop, style: segment.style};
}

/**
Expand All @@ -63,6 +64,7 @@ function getSegment(segment, points, bounds) {
* @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} [segment.style] - segment style
* @param {PointElement[]} points - the points that this segment refers to
* @param {object} [bounds]
* @param {string} bounds.property - the property of a `PointElement` we are bounding. `x`, `y` or `angle`.
Expand All @@ -78,7 +80,7 @@ export function _boundSegment(segment, points, bounds) {
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 {start, end, loop, style} = getSegment(segment, points, bounds);

const result = [];
let inside = false;
Expand All @@ -105,15 +107,15 @@ export function _boundSegment(segment, points, bounds) {
}

if (subStart !== null && shouldStop()) {
result.push(makeSubSegment(subStart, i, loop, count));
result.push(normalizeSegment({start: subStart, end: i, loop, count, style}));
subStart = null;
}
prev = i;
prevValue = value;
}

if (subStart !== null) {
result.push(makeSubSegment(subStart, end, loop, count));
result.push(normalizeSegment({start: subStart, end, loop, count, style}));
}

return result;
Expand Down Expand Up @@ -296,12 +298,13 @@ function doSplitByStyles(segments, points, segmentOptions) {

function readStyle(options) {
return {
backgroundColor: options.backgroundColor,
borderCapStyle: options.borderCapStyle,
borderDash: options.borderDash,
borderDashOffset: options.borderDashOffset,
borderJoinStyle: options.borderJoinStyle,
borderWidth: options.borderWidth,
borderColor: options.borderColor,
borderColor: options.borderColor
};
}

Expand Down
24 changes: 10 additions & 14 deletions src/plugins/plugin.filler.js
Expand Up @@ -405,8 +405,7 @@ function _segments(line, target, property) {
const tpoints = target.points;
const parts = [];

for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
for (const segment of segments) {
const bounds = getBounds(property, points[segment.start], points[segment.end], segment.loop);

if (!target.segments) {
Expand All @@ -422,24 +421,22 @@ function _segments(line, target, property) {
}

// Get all segments from `target` that intersect the bounds of current segment of `line`
const subs = _boundSegments(target, bounds);
const targetSegments = _boundSegments(target, bounds);

for (let j = 0; j < subs.length; ++j) {
const sub = subs[j];
const subBounds = getBounds(property, tpoints[sub.start], tpoints[sub.end], sub.loop);
for (const tgt of targetSegments) {
const subBounds = getBounds(property, tpoints[tgt.start], tpoints[tgt.end], tgt.loop);
const fillSources = _boundSegment(segment, points, subBounds);

for (let k = 0; k < fillSources.length; k++) {
for (const fillSource of fillSources) {
parts.push({
source: fillSources[k],
target: sub,
source: fillSource,
target: tgt,
start: {
[property]: _getEdge(bounds, subBounds, 'start', Math.max)
},
end: {
[property]: _getEdge(bounds, subBounds, 'end', Math.min)
}

});
}
}
Expand Down Expand Up @@ -468,11 +465,10 @@ function _fill(ctx, cfg) {
const {line, target, property, color, scale} = cfg;
const segments = _segments(line, target, property);

ctx.fillStyle = color;
for (let i = 0, ilen = segments.length; i < ilen; ++i) {
const {source: src, target: tgt, start, end} = segments[i];

for (const {source: src, target: tgt, start, end} of segments) {
const {style: {backgroundColor = color} = {}} = src;
ctx.save();
ctx.fillStyle = backgroundColor;

clipBounds(ctx, scale, getBounds(property, start, end));

Expand Down
23 changes: 23 additions & 0 deletions test/fixtures/plugin.filler/line/segments/gap.js
@@ -0,0 +1,23 @@
module.exports = {
config: {
type: 'line',
data: {
labels: ['a', 'b', 'c', 'd', 'e', 'f'],
datasets: [{
data: [1, 3, NaN, NaN, 2, 1],
borderColor: 'transparent',
backgroundColor: 'black',
fill: true,
segment: {
backgroundColor: ctx => ctx.p0.skip || ctx.p1.skip ? 'red' : undefined,
}
}]
},
options: {
scales: {
x: {display: false},
y: {display: false}
}
}
}
};
Binary file added test/fixtures/plugin.filler/line/segments/gap.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions test/fixtures/plugin.filler/line/segments/slope.js
@@ -0,0 +1,30 @@
function slope({p0, p1}) {
return (p0.y - p1.y) / (p1.x - p0.x);
}

module.exports = {
config: {
type: 'line',
data: {
labels: ['a', 'b', 'c', 'd', 'e', 'f'],
datasets: [{
data: [1, 2, 3, 3, 2, 1],
backgroundColor: 'black',
borderColor: 'orange',
fill: true,
segment: {
backgroundColor: ctx => slope(ctx) > 0 ? 'green' : slope(ctx) < 0 ? 'red' : undefined,
}
}]
},
options: {
plugins: {
legend: false
},
scales: {
x: {display: false},
y: {display: false}
}
}
}
};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 10 additions & 10 deletions test/specs/helpers.segment.tests.js
Expand Up @@ -14,39 +14,39 @@ describe('helpers.segments', function() {
});

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}]);
expect(_boundSegment(segment, points, {property: 'x', start: 5, end: 15})).toEqual([{start: 0, end: 1, loop: false, style: undefined}]);
});

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}]);
expect(_boundSegment(segment, points, {property: 'x', start: 10, end: 10})).toEqual([{start: 0, end: 0, loop: false, style: undefined}]);
});

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}]);
expect(_boundSegment(segment, points, {property: 'x', start: 11, end: 14})).toEqual([{start: 0, end: 1, loop: false, style: undefined}]);
});

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}]);
expect(_boundSegment(segment, points, {property: 'x', start: 22, end: 22})).toEqual([{start: 1, end: 2, loop: false, style: undefined}]);
});

it('should find whole segment', function() {
expect(_boundSegment(segment, points, {property: 'x', start: 0, end: 50})).toEqual([{start: 0, end: 2, loop: false}]);
expect(_boundSegment(segment, points, {property: 'x', start: 0, end: 50})).toEqual([{start: 0, end: 2, loop: false, style: undefined}]);
});

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}]);
expect(_boundSegment(segment, points, {property: 'x', start: 10.001, end: 29.999})).toEqual([{start: 0, end: 2, loop: false, style: undefined}]);
});

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}]);
expect(_boundSegment(segment, points, {property: 'x', start: 25, end: 35})).toEqual([{start: 1, end: 2, loop: false, style: undefined}]);
});

it('should find multiple segments', function() {
const points2 = [{x: 0, y: 100}, {x: 1, y: 50}, {x: 2, y: 70}, {x: 4, y: 80}, {x: 5, y: -100}];
expect(_boundSegment({start: 0, end: 4, loop: false}, points2, {property: 'y', start: 60, end: 60})).toEqual([
{start: 0, end: 1, loop: false},
{start: 1, end: 2, loop: false},
{start: 3, end: 4, loop: false},
{start: 0, end: 1, loop: false, style: undefined},
{start: 1, end: 2, loop: false, style: undefined},
{start: 3, end: 4, loop: false, style: undefined},
]);
});
});
Expand Down
1 change: 1 addition & 0 deletions types/index.esm.d.ts
Expand Up @@ -1691,6 +1691,7 @@ export interface LineOptions extends CommonElementOptions {
stepped: 'before' | 'after' | 'middle' | boolean;

segment: {
backgroundColor: Scriptable<Color|undefined, ScriptableLineSegmentContext>,
borderColor: Scriptable<Color|undefined, ScriptableLineSegmentContext>,
borderCapStyle: Scriptable<CanvasLineCap|undefined, ScriptableLineSegmentContext>;
borderDash: Scriptable<number[]|undefined, ScriptableLineSegmentContext>;
Expand Down
1 change: 1 addition & 0 deletions types/tests/controllers/line_segments.ts
Expand Up @@ -7,6 +7,7 @@ const chart = new Chart('id', {
datasets: [{
data: [],
segment: {
backgroundColor: ctx => ctx.p0.skip ? 'transparent' : undefined,
borderColor: ctx => ctx.p0.skip ? 'gray' : undefined,
borderWidth: ctx => ctx.p1.parsed.y > 10 ? 5 : undefined,
}
Expand Down

0 comments on commit ba84cc5

Please sign in to comment.