fix: respect dataset clipping area when filling line charts (#12057)

* fix(plugin.filler): respect dataset clipping area when filling line charts

The filling area must respect the dataset's clipping area when clipping is enabled. Before this change, the line would be clipped according to the dataset's area but the fill would overlap other datasets.

Closes #12052

* chore(plugin.filler): use @ts-expect-error instead of @ts-ignore
This commit is contained in:
Adrian Cerbaro 2025-04-14 10:41:14 -03:00 committed by GitHub
parent a647e0d007
commit 3dffb4fb8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 395 additions and 42 deletions

View File

@ -6,9 +6,8 @@ import {_detectPlatform} from '../platform/index.js';
import PluginService from './core.plugins.js';
import registry from './core.registry.js';
import Config, {determineAxis, getIndexAxis} from './core.config.js';
import {retinaScale, _isDomSupported} from '../helpers/helpers.dom.js';
import {each, callback as callCallback, uid, valueOrDefault, _elementsEqual, isNullOrUndef, setsEqual, defined, isFunction, _isClickEvent} from '../helpers/helpers.core.js';
import {clearCanvas, clipArea, createContext, unclipArea, _isPointInArea} from '../helpers/index.js';
import {clearCanvas, clipArea, createContext, unclipArea, _isPointInArea, _isDomSupported, retinaScale, getDatasetClipArea} from '../helpers/index.js';
// @ts-ignore
import {version} from '../../package.json';
import {debounce} from '../helpers/helpers.extras.js';
@ -101,23 +100,6 @@ function determineLastEvent(e, lastEvent, inChartArea, isClick) {
return e;
}
function getSizeForArea(scale, chartArea, field) {
return scale.options.clip ? scale[field] : chartArea[field];
}
function getDatasetArea(meta, chartArea) {
const {xScale, yScale} = meta;
if (xScale && yScale) {
return {
left: getSizeForArea(xScale, chartArea, 'left'),
right: getSizeForArea(xScale, chartArea, 'right'),
top: getSizeForArea(yScale, chartArea, 'top'),
bottom: getSizeForArea(yScale, chartArea, 'bottom')
};
}
return chartArea;
}
class Chart {
static defaults = defaults;
@ -800,31 +782,25 @@ class Chart {
*/
_drawDataset(meta) {
const ctx = this.ctx;
const clip = meta._clip;
const useClip = !clip.disabled;
const area = getDatasetArea(meta, this.chartArea);
const args = {
meta,
index: meta.index,
cancelable: true
};
// @ts-expect-error
const clip = getDatasetClipArea(this, meta);
if (this.notifyPlugins('beforeDatasetDraw', args) === false) {
return;
}
if (useClip) {
clipArea(ctx, {
left: clip.left === false ? 0 : area.left - clip.left,
right: clip.right === false ? this.width : area.right + clip.right,
top: clip.top === false ? 0 : area.top - clip.top,
bottom: clip.bottom === false ? this.height : area.bottom + clip.bottom
});
if (clip) {
clipArea(ctx, clip);
}
meta.controller.draw();
if (useClip) {
if (clip) {
unclipArea(ctx);
}

View File

@ -0,0 +1,33 @@
import type {Chart, ChartArea, ChartMeta, Scale, TRBL} from '../types/index.js';
function getSizeForArea(scale: Scale, chartArea: ChartArea, field: keyof ChartArea) {
return scale.options.clip ? scale[field] : chartArea[field];
}
function getDatasetArea(meta: ChartMeta, chartArea: ChartArea): TRBL {
const {xScale, yScale} = meta;
if (xScale && yScale) {
return {
left: getSizeForArea(xScale, chartArea, 'left'),
right: getSizeForArea(xScale, chartArea, 'right'),
top: getSizeForArea(yScale, chartArea, 'top'),
bottom: getSizeForArea(yScale, chartArea, 'bottom')
};
}
return chartArea;
}
export function getDatasetClipArea(chart: Chart, meta: ChartMeta): TRBL | false {
const clip = meta._clip;
if (clip.disabled) {
return false;
}
const area = getDatasetArea(meta, chart.chartArea);
return {
left: clip.left === false ? 0 : area.left - (clip.left === true ? 0 : clip.left),
right: clip.right === false ? chart.width : area.right + (clip.right === true ? 0 : clip.right),
top: clip.top === false ? 0 : area.top - (clip.top === true ? 0 : clip.top),
bottom: clip.bottom === false ? chart.height : area.bottom + (clip.bottom === true ? 0 : clip.bottom)
};
}

View File

@ -13,3 +13,4 @@ export * from './helpers.options.js';
export * from './helpers.math.js';
export * from './helpers.rtl.js';
export * from './helpers.segment.js';
export * from './helpers.dataset.js';

View File

@ -1,35 +1,37 @@
import {clipArea, unclipArea} from '../../helpers/index.js';
import {clipArea, unclipArea, getDatasetClipArea} from '../../helpers/index.js';
import {_findSegmentEnd, _getBounds, _segments} from './filler.segment.js';
import {_getTarget} from './filler.target.js';
export function _drawfill(ctx, source, area) {
const target = _getTarget(source);
const {line, scale, axis} = source;
const {chart, index, line, scale, axis} = source;
const lineOpts = line.options;
const fillOption = lineOpts.fill;
const color = lineOpts.backgroundColor;
const {above = color, below = color} = fillOption || {};
const meta = chart.getDatasetMeta(index);
const clip = getDatasetClipArea(chart, meta);
if (target && line.points.length) {
clipArea(ctx, area);
doFill(ctx, {line, target, above, below, area, scale, axis});
doFill(ctx, {line, target, above, below, area, scale, axis, clip});
unclipArea(ctx);
}
}
function doFill(ctx, cfg) {
const {line, target, above, below, area, scale} = cfg;
const {line, target, above, below, area, scale, clip} = 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});
fill(ctx, {line, target, color: above, scale, property, clip});
ctx.restore();
ctx.save();
clipVertical(ctx, target, area.bottom);
}
fill(ctx, {line, target, color: below, scale, property});
fill(ctx, {line, target, color: below, scale, property, clip});
ctx.restore();
}
@ -65,7 +67,7 @@ function clipVertical(ctx, target, clipY) {
}
function fill(ctx, cfg) {
const {line, target, property, color, scale} = cfg;
const {line, target, property, color, scale, clip} = cfg;
const segments = _segments(line, target, property);
for (const {source: src, target: tgt, start, end} of segments) {
@ -75,7 +77,7 @@ function fill(ctx, cfg) {
ctx.save();
ctx.fillStyle = backgroundColor;
clipBounds(ctx, scale, notShape && _getBounds(property, start, end));
clipBounds(ctx, scale, clip, notShape && _getBounds(property, start, end));
ctx.beginPath();
@ -103,12 +105,35 @@ function fill(ctx, cfg) {
}
}
function clipBounds(ctx, scale, bounds) {
const {top, bottom} = scale.chart.chartArea;
function clipBounds(ctx, scale, clip, bounds) {
const chartArea = scale.chart.chartArea;
const {property, start, end} = bounds || {};
if (property === 'x') {
if (property === 'x' || property === 'y') {
let left, top, right, bottom;
if (property === 'x') {
left = start;
top = chartArea.top;
right = end;
bottom = chartArea.bottom;
} else {
left = chartArea.left;
top = start;
right = chartArea.right;
bottom = end;
}
ctx.beginPath();
ctx.rect(start, top, end - start, bottom - top);
if (clip) {
left = Math.max(left, clip.left);
right = Math.min(right, clip.right);
top = Math.max(top, clip.top);
bottom = Math.min(bottom, clip.bottom);
}
ctx.rect(left, top, right - left, bottom - top);
ctx.clip();
}
}

10
src/types/index.d.ts vendored
View File

@ -429,6 +429,15 @@ export declare const RadarController: ChartComponent & {
prototype: RadarController;
new (chart: Chart, datasetIndex: number): RadarController;
};
interface ChartMetaClip {
left: number | boolean;
top: number | boolean;
right: number | boolean;
bottom: number | boolean;
disabled: boolean;
}
interface ChartMetaCommon<TElement extends Element = Element, TDatasetElement extends Element = Element> {
type: string;
controller: DatasetController;
@ -462,6 +471,7 @@ interface ChartMetaCommon<TElement extends Element = Element, TDatasetElement ex
_sorted: boolean;
_stacked: boolean | 'single';
_parsed: unknown[];
_clip: ChartMetaClip;
}
export type ChartMeta<

View File

@ -0,0 +1,78 @@
const labels = [1, 2, 3, 4, 5, 6, 7];
const values = [65, 59, 80, 81, 56, 55, 40];
module.exports = {
description: 'https://github.com/chartjs/Chart.js/issues/12052',
config: {
type: 'line',
data: {
labels,
datasets: [
{
data: values.map(v => v - 10),
fill: '1',
borderColor: 'rgb(255, 0, 0)',
backgroundColor: 'rgba(255, 0, 0, 0.25)',
xAxisID: 'x1',
},
{
data: values,
fill: false,
borderColor: 'rgb(255, 0, 0)',
xAxisID: 'x1',
},
{
data: values,
fill: false,
borderColor: 'rgb(0, 0, 255)',
xAxisID: 'x2',
},
{
data: values.map(v => v + 10),
fill: '-1',
borderColor: 'rgb(0, 0, 255)',
backgroundColor: 'rgba(0, 0, 255, 0.25)',
xAxisID: 'x2',
}
]
},
options: {
clip: false,
indexAxis: 'y',
animation: false,
responsive: false,
plugins: {
legend: false,
title: false,
tooltip: false
},
elements: {
point: {
radius: 0
},
line: {
cubicInterpolationMode: 'monotone',
borderColor: 'transparent',
tension: 0
}
},
scales: {
x2: {
axis: 'x',
stack: 'stack',
max: 80,
display: false,
},
x1: {
min: 50,
axis: 'x',
stack: 'stack',
display: false,
},
y: {
display: false,
}
}
}
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -0,0 +1,77 @@
const labels = [1, 2, 3, 4, 5, 6, 7];
const values = [65, 59, 80, 81, 56, 55, 40];
module.exports = {
description: 'https://github.com/chartjs/Chart.js/issues/12052',
config: {
type: 'line',
data: {
labels,
datasets: [
{
data: values.map(v => v - 10),
fill: '1',
borderColor: 'rgb(255, 0, 0)',
backgroundColor: 'rgba(255, 0, 0, 0.25)',
xAxisID: 'x1',
},
{
data: values,
fill: false,
borderColor: 'rgb(255, 0, 0)',
xAxisID: 'x1',
},
{
data: values,
fill: false,
borderColor: 'rgb(0, 0, 255)',
xAxisID: 'x2',
},
{
data: values.map(v => v + 10),
fill: '-1',
borderColor: 'rgb(0, 0, 255)',
backgroundColor: 'rgba(0, 0, 255, 0.25)',
xAxisID: 'x2',
}
]
},
options: {
indexAxis: 'y',
animation: false,
responsive: false,
plugins: {
legend: false,
title: false,
tooltip: false
},
elements: {
point: {
radius: 0
},
line: {
cubicInterpolationMode: 'monotone',
borderColor: 'transparent',
tension: 0
}
},
scales: {
x2: {
axis: 'x',
stack: 'stack',
max: 80,
display: false,
},
x1: {
min: 50,
axis: 'x',
stack: 'stack',
display: false,
},
y: {
display: false,
}
}
}
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,77 @@
const labels = [1, 2, 3, 4, 5, 6, 7];
const values = [65, 59, 80, 81, 56, 55, 40];
module.exports = {
description: 'https://github.com/chartjs/Chart.js/issues/12052',
config: {
type: 'line',
data: {
labels,
datasets: [
{
data: values.map(v => v - 10),
fill: '1',
borderColor: 'rgb(255, 0, 0)',
backgroundColor: 'rgba(255, 0, 0, 0.25)',
yAxisID: 'y1',
},
{
data: values,
fill: false,
borderColor: 'rgb(255, 0, 0)',
yAxisID: 'y1',
},
{
data: values,
fill: false,
borderColor: 'rgb(0, 0, 255)',
yAxisID: 'y2',
},
{
data: values.map(v => v + 10),
fill: '-1',
borderColor: 'rgb(0, 0, 255)',
backgroundColor: 'rgba(0, 0, 255, 0.25)',
yAxisID: 'y2',
}
]
},
options: {
clip: false,
animation: false,
responsive: false,
plugins: {
legend: false,
title: false,
tooltip: false
},
elements: {
point: {
radius: 0
},
line: {
cubicInterpolationMode: 'monotone',
borderColor: 'transparent',
tension: 0
}
},
scales: {
y2: {
axis: 'y',
stack: 'stack',
max: 80,
display: false,
},
y1: {
min: 50,
axis: 'y',
stack: 'stack',
display: false,
},
x: {
display: false,
}
}
}
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,76 @@
const labels = [1, 2, 3, 4, 5, 6, 7];
const values = [65, 59, 80, 81, 56, 55, 40];
module.exports = {
description: 'https://github.com/chartjs/Chart.js/issues/12052',
config: {
type: 'line',
data: {
labels,
datasets: [
{
data: values.map(v => v - 10),
fill: '1',
borderColor: 'rgb(255, 0, 0)',
backgroundColor: 'rgba(255, 0, 0, 0.25)',
yAxisID: 'y1',
},
{
data: values,
fill: false,
borderColor: 'rgb(255, 0, 0)',
yAxisID: 'y1',
},
{
data: values,
fill: false,
borderColor: 'rgb(0, 0, 255)',
yAxisID: 'y2',
},
{
data: values.map(v => v + 10),
fill: '-1',
borderColor: 'rgb(0, 0, 255)',
backgroundColor: 'rgba(0, 0, 255, 0.25)',
yAxisID: 'y2',
}
]
},
options: {
animation: false,
responsive: false,
plugins: {
legend: false,
title: false,
tooltip: false
},
elements: {
point: {
radius: 0
},
line: {
cubicInterpolationMode: 'monotone',
borderColor: 'transparent',
tension: 0
}
},
scales: {
y2: {
axis: 'y',
stack: 'stack',
max: 80,
display: false,
},
y1: {
min: 50,
axis: 'y',
stack: 'stack',
display: false,
},
x: {
display: false,
}
}
}
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB