]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
fix: respect dataset clipping area when filling line charts (#12057)
authorAdrian Cerbaro <adriancerbaro@gmail.com>
Mon, 14 Apr 2025 13:41:14 +0000 (10:41 -0300)
committerGitHub <noreply@github.com>
Mon, 14 Apr 2025 13:41:14 +0000 (15:41 +0200)
* 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

13 files changed:
src/core/core.controller.js
src/helpers/helpers.dataset.ts [new file with mode: 0644]
src/helpers/index.ts
src/plugins/plugin.filler/filler.drawing.js
src/types/index.d.ts
test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.js [new file with mode: 0644]
test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.png [new file with mode: 0644]
test/fixtures/plugin.filler/line/dataset/clip-bounds-x.js [new file with mode: 0644]
test/fixtures/plugin.filler/line/dataset/clip-bounds-x.png [new file with mode: 0644]
test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.js [new file with mode: 0644]
test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.png [new file with mode: 0644]
test/fixtures/plugin.filler/line/dataset/clip-bounds-y.js [new file with mode: 0644]
test/fixtures/plugin.filler/line/dataset/clip-bounds-y.png [new file with mode: 0644]

index 47b238da8aa2ec5753e8dfd56c578ec6d17c7c1c..e0408ae212ad7451d55186d3eb40980aadf5447e 100644 (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);
     }
 
diff --git a/src/helpers/helpers.dataset.ts b/src/helpers/helpers.dataset.ts
new file mode 100644 (file)
index 0000000..000dcfe
--- /dev/null
@@ -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)
+  };
+}
index 1917ce740a16aa3a0790ee7149a20abf4036901a..9fde7b85951e2c617aff3dc8a79b9f908fb4da15 100644 (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';
index 2e2fbd2b99ef718597f7b826777d1355dd67eadb..9abb513cff007b04ce586e09776624ba11f6ed4b 100644 (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();
   }
 }
index 14461328a923f401d8902ef67d303da6a5d949c1..807fe820879d25b4e4cca9736ae8c785176114e0 100644 (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<
diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.js b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.js
new file mode 100644 (file)
index 0000000..ff437ae
--- /dev/null
@@ -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,
+        }
+      }
+    }
+  },
+};
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 (file)
index 0000000..f050a47
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 (file)
index 0000000..0ba25ac
--- /dev/null
@@ -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 (file)
index 0000000..4f1dfdd
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 (file)
index 0000000..16a9759
--- /dev/null
@@ -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 (file)
index 0000000..a2b8766
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 (file)
index 0000000..cbfc6d4
--- /dev/null
@@ -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 (file)
index 0000000..137e031
Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y.png differ