From: Adrian Cerbaro Date: Mon, 14 Apr 2025 13:41:14 +0000 (-0300) Subject: fix: respect dataset clipping area when filling line charts (#12057) X-Git-Tag: v4.4.9~1 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=3dffb4fb8ebb529c0447d0ed6aa7e08479a591ed;p=thirdparty%2FChart.js.git 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 --- diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 47b238da8..e0408ae21 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -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); } diff --git a/src/helpers/helpers.dataset.ts b/src/helpers/helpers.dataset.ts new file mode 100644 index 000000000..000dcfe19 --- /dev/null +++ b/src/helpers/helpers.dataset.ts @@ -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) + }; +} diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 1917ce740..9fde7b859 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -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'; diff --git a/src/plugins/plugin.filler/filler.drawing.js b/src/plugins/plugin.filler/filler.drawing.js index 2e2fbd2b9..9abb513cf 100644 --- a/src/plugins/plugin.filler/filler.drawing.js +++ b/src/plugins/plugin.filler/filler.drawing.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(); } } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 14461328a..807fe8208 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -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 { type: string; controller: DatasetController; @@ -462,6 +471,7 @@ interface ChartMetaCommon 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, + } + } + } + }, +}; diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.png b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.png new file mode 100644 index 000000000..f050a4759 Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-x.js b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x.js new file mode 100644 index 000000000..0ba25ac31 --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x.js @@ -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, + } + } + } + }, +}; diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-x.png b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x.png new file mode 100644 index 000000000..4f1dfdd6c Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.js b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.js new file mode 100644 index 000000000..16a9759bb --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.js @@ -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, + } + } + } + }, +}; diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.png b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.png new file mode 100644 index 000000000..a2b8766f8 Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-y.js b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y.js new file mode 100644 index 000000000..cbfc6d403 --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y.js @@ -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, + } + } + } + }, +}; diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-y.png b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y.png new file mode 100644 index 000000000..137e0315b Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y.png differ