mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
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:
parent
a647e0d007
commit
3dffb4fb8e
@ -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);
|
||||
}
|
||||
|
||||
|
||||
33
src/helpers/helpers.dataset.ts
Normal file
33
src/helpers/helpers.dataset.ts
Normal 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)
|
||||
};
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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
10
src/types/index.d.ts
vendored
@ -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<
|
||||
|
||||
78
test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.js
vendored
Normal file
78
test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.js
vendored
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
BIN
test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.png
vendored
Normal file
BIN
test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
77
test/fixtures/plugin.filler/line/dataset/clip-bounds-x.js
vendored
Normal file
77
test/fixtures/plugin.filler/line/dataset/clip-bounds-x.js
vendored
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
BIN
test/fixtures/plugin.filler/line/dataset/clip-bounds-x.png
vendored
Normal file
BIN
test/fixtures/plugin.filler/line/dataset/clip-bounds-x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
77
test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.js
vendored
Normal file
77
test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.js
vendored
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
BIN
test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.png
vendored
Normal file
BIN
test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
76
test/fixtures/plugin.filler/line/dataset/clip-bounds-y.js
vendored
Normal file
76
test/fixtures/plugin.filler/line/dataset/clip-bounds-y.js
vendored
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
BIN
test/fixtures/plugin.filler/line/dataset/clip-bounds-y.png
vendored
Normal file
BIN
test/fixtures/plugin.filler/line/dataset/clip-bounds-y.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Loading…
x
Reference in New Issue
Block a user