]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Enable point labels hiding when overlapped (#11055)
authorstockiNail <stocki.nail@gmail.com>
Thu, 27 Apr 2023 22:28:55 +0000 (00:28 +0200)
committerGitHub <noreply@github.com>
Thu, 27 Apr 2023 22:28:55 +0000 (18:28 -0400)
* Enable point labels hiding when overlapped

* fix cc

* fallback CC updates

* fixes CC

docs/axes/radial/linear.md
src/scales/scale.radialLinear.js
src/types/index.d.ts
test/fixtures/controller.polarArea/pointLabels/displayAuto-180.js [new file with mode: 0644]
test/fixtures/controller.polarArea/pointLabels/displayAuto-180.png [new file with mode: 0644]
test/fixtures/controller.polarArea/pointLabels/displayAuto.js [new file with mode: 0644]
test/fixtures/controller.polarArea/pointLabels/displayAuto.png [new file with mode: 0644]
test/fixtures/controller.polarArea/pointLabels/overlapping.js [new file with mode: 0644]
test/fixtures/controller.polarArea/pointLabels/overlapping.png [new file with mode: 0644]

index 465649825b952acc4206fee0cacd7e9b8e02ffec..2ed3a9e005a739482e92c7873050cdec16e0c056 100644 (file)
@@ -154,7 +154,7 @@ Namespace: `options.scales[scaleId].pointLabels`
 | `backdropColor` | [`Color`](../../general/colors.md) | `true` | `undefined` | Background color of the point label.
 | `backdropPadding` | [`Padding`](../../general/padding.md) | | `2` | Padding of label backdrop.
 | `borderRadius` | `number`\|`object` | `true` | `0` | Border radius of the point label
-| `display` | `boolean` | | `true` | If true, point labels are shown.
+| `display` | `boolean`\|`string` | | `true` | If true, point labels are shown.  When `display: 'auto'`, the label is hidden if it overlaps with another label.
 | `callback` | `function` | | | Callback function to transform data labels to point labels. The default implementation simply returns the current string.
 | `color` | [`Color`](../../general/colors.md) | Yes | `Chart.defaults.color` | Color of label.
 | `font` | `Font` | Yes | `Chart.defaults.font` | See [Fonts](../../general/fonts.md)
index 7d41d36830ce9e10bc719dc4935ad1cbd858ac68..ae44adbcd5c5b04887a74a2057d99c620ea6815c 100644 (file)
@@ -1,5 +1,5 @@
 import defaults from '../core/core.defaults.js';
-import {_longestText, addRoundedRectPath, renderText} from '../helpers/helpers.canvas.js';
+import {_longestText, addRoundedRectPath, renderText, _isPointInArea} from '../helpers/helpers.canvas.js';
 import {HALF_PI, TAU, toDegrees, toRadians, _normalizeAngle, PI} from '../helpers/helpers.math.js';
 import LinearScaleBase from './scale.linearbase.js';
 import Ticks from '../core/core.ticks.js';
@@ -136,36 +136,66 @@ function updateLimits(limits, orig, angle, hLimits, vLimits) {
   }
 }
 
+function createPointLabelItem(scale, index, itemOpts) {
+  const outerDistance = scale.drawingArea;
+  const {extra, additionalAngle, padding, size} = itemOpts;
+  const pointLabelPosition = scale.getPointPosition(index, outerDistance + extra + padding, additionalAngle);
+  const angle = Math.round(toDegrees(_normalizeAngle(pointLabelPosition.angle + HALF_PI)));
+  const y = yForAngle(pointLabelPosition.y, size.h, angle);
+  const textAlign = getTextAlignForAngle(angle);
+  const left = leftForTextAlign(pointLabelPosition.x, size.w, textAlign);
+  return {
+    // if to draw or overlapped
+    visible: true,
+
+    // Text position
+    x: pointLabelPosition.x,
+    y,
+
+    // Text rendering data
+    textAlign,
+
+    // Bounding box
+    left,
+    top: y,
+    right: left + size.w,
+    bottom: y + size.h
+  };
+}
+
+function isNotOverlapped(item, area) {
+  if (!area) {
+    return true;
+  }
+  const {left, top, right, bottom} = item;
+  const apexesInArea = _isPointInArea({x: left, y: top}, area) || _isPointInArea({x: left, y: bottom}, area) ||
+    _isPointInArea({x: right, y: top}, area) || _isPointInArea({x: right, y: bottom}, area);
+  return !apexesInArea;
+}
+
 function buildPointLabelItems(scale, labelSizes, padding) {
   const items = [];
   const valueCount = scale._pointLabels.length;
   const opts = scale.options;
-  const extra = getTickBackdropHeight(opts) / 2;
-  const outerDistance = scale.drawingArea;
-  const additionalAngle = opts.pointLabels.centerPointLabels ? PI / valueCount : 0;
+  const {centerPointLabels, display} = opts.pointLabels;
+  const itemOpts = {
+    extra: getTickBackdropHeight(opts) / 2,
+    additionalAngle: centerPointLabels ? PI / valueCount : 0
+  };
+  let area;
 
   for (let i = 0; i < valueCount; i++) {
-    const pointLabelPosition = scale.getPointPosition(i, outerDistance + extra + padding[i], additionalAngle);
-    const angle = Math.round(toDegrees(_normalizeAngle(pointLabelPosition.angle + HALF_PI)));
-    const size = labelSizes[i];
-    const y = yForAngle(pointLabelPosition.y, size.h, angle);
-    const textAlign = getTextAlignForAngle(angle);
-    const left = leftForTextAlign(pointLabelPosition.x, size.w, textAlign);
-
-    items.push({
-      // Text position
-      x: pointLabelPosition.x,
-      y,
-
-      // Text rendering data
-      textAlign,
-
-      // Bounding box
-      left,
-      top: y,
-      right: left + size.w,
-      bottom: y + size.h
-    });
+    itemOpts.padding = padding[i];
+    itemOpts.size = labelSizes[i];
+
+    const item = createPointLabelItem(scale, i, itemOpts);
+    items.push(item);
+    if (display === 'auto') {
+      item.visible = isNotOverlapped(item, area);
+      if (item.visible) {
+        area = item;
+      }
+    }
   }
   return items;
 }
@@ -198,39 +228,49 @@ function yForAngle(y, h, angle) {
   return y;
 }
 
+function drawPointLabelBox(ctx, opts, item) {
+  const {left, top, right, bottom} = item;
+  const {backdropColor} = opts;
+
+  if (!isNullOrUndef(backdropColor)) {
+    const borderRadius = toTRBLCorners(opts.borderRadius);
+    const padding = toPadding(opts.backdropPadding);
+    ctx.fillStyle = backdropColor;
+
+    const backdropLeft = left - padding.left;
+    const backdropTop = top - padding.top;
+    const backdropWidth = right - left + padding.width;
+    const backdropHeight = bottom - top + padding.height;
+
+    if (Object.values(borderRadius).some(v => v !== 0)) {
+      ctx.beginPath();
+      addRoundedRectPath(ctx, {
+        x: backdropLeft,
+        y: backdropTop,
+        w: backdropWidth,
+        h: backdropHeight,
+        radius: borderRadius,
+      });
+      ctx.fill();
+    } else {
+      ctx.fillRect(backdropLeft, backdropTop, backdropWidth, backdropHeight);
+    }
+  }
+}
+
 function drawPointLabels(scale, labelCount) {
   const {ctx, options: {pointLabels}} = scale;
 
   for (let i = labelCount - 1; i >= 0; i--) {
+    const item = scale._pointLabelItems[i];
+    if (!item.visible) {
+      // overlapping
+      continue;
+    }
     const optsAtIndex = pointLabels.setContext(scale.getPointLabelContext(i));
+    drawPointLabelBox(ctx, optsAtIndex, item);
     const plFont = toFont(optsAtIndex.font);
-    const {x, y, textAlign, left, top, right, bottom} = scale._pointLabelItems[i];
-    const {backdropColor} = optsAtIndex;
-
-    if (!isNullOrUndef(backdropColor)) {
-      const borderRadius = toTRBLCorners(optsAtIndex.borderRadius);
-      const padding = toPadding(optsAtIndex.backdropPadding);
-      ctx.fillStyle = backdropColor;
-
-      const backdropLeft = left - padding.left;
-      const backdropTop = top - padding.top;
-      const backdropWidth = right - left + padding.width;
-      const backdropHeight = bottom - top + padding.height;
-
-      if (Object.values(borderRadius).some(v => v !== 0)) {
-        ctx.beginPath();
-        addRoundedRectPath(ctx, {
-          x: backdropLeft,
-          y: backdropTop,
-          w: backdropWidth,
-          h: backdropHeight,
-          radius: borderRadius,
-        });
-        ctx.fill();
-      } else {
-        ctx.fillRect(backdropLeft, backdropTop, backdropWidth, backdropHeight);
-      }
-    }
+    const {x, y, textAlign} = item;
 
     renderText(
       ctx,
index c4f042ec16e9d4b398cd95f4341efdf05033c655..3cbf1b6aafbd4961e28552b3b74d6b84da098466 100644 (file)
@@ -3500,10 +3500,10 @@ export type RadialLinearScaleOptions = CoreScaleOptions & {
     borderRadius: Scriptable<number | BorderRadius, ScriptableScalePointLabelContext>;
 
     /**
-     * if true, point labels are shown.
+     * if true, point labels are shown. When `display: 'auto'`, the label is hidden if it overlaps with another label.
      * @default true
      */
-    display: boolean;
+    display: boolean | 'auto';
     /**
      * Color of label
      * @see Defaults.color
diff --git a/test/fixtures/controller.polarArea/pointLabels/displayAuto-180.js b/test/fixtures/controller.polarArea/pointLabels/displayAuto-180.js
new file mode 100644 (file)
index 0000000..91e47c6
--- /dev/null
@@ -0,0 +1,25 @@
+module.exports = {
+  config: {
+    type: 'polarArea',
+    data: {
+      datasets: [{
+        data: new Array(50).fill(5),
+        backgroundColor: ['#f003', '#0f03', '#00f3', '#0003']
+      }],
+      labels: new Array(50).fill(0).map((el, i) => ['label ' + i, 'line 2'])
+    },
+    options: {
+      scales: {
+        r: {
+          startAngle: 180,
+          pointLabels: {
+            display: 'auto',
+          }
+        }
+      }
+    }
+  },
+  options: {
+    spriteText: true
+  }
+};
diff --git a/test/fixtures/controller.polarArea/pointLabels/displayAuto-180.png b/test/fixtures/controller.polarArea/pointLabels/displayAuto-180.png
new file mode 100644 (file)
index 0000000..c0bb673
Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/displayAuto-180.png differ
diff --git a/test/fixtures/controller.polarArea/pointLabels/displayAuto.js b/test/fixtures/controller.polarArea/pointLabels/displayAuto.js
new file mode 100644 (file)
index 0000000..14b85ea
--- /dev/null
@@ -0,0 +1,24 @@
+module.exports = {
+  config: {
+    type: 'polarArea',
+    data: {
+      datasets: [{
+        data: new Array(50).fill(5),
+        backgroundColor: ['#f003', '#0f03', '#00f3', '#0003']
+      }],
+      labels: new Array(50).fill(0).map((el, i) => ['label ' + i, 'line 2'])
+    },
+    options: {
+      scales: {
+        r: {
+          pointLabels: {
+            display: 'auto',
+          }
+        }
+      }
+    }
+  },
+  options: {
+    spriteText: true
+  }
+};
diff --git a/test/fixtures/controller.polarArea/pointLabels/displayAuto.png b/test/fixtures/controller.polarArea/pointLabels/displayAuto.png
new file mode 100644 (file)
index 0000000..271fbd2
Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/displayAuto.png differ
diff --git a/test/fixtures/controller.polarArea/pointLabels/overlapping.js b/test/fixtures/controller.polarArea/pointLabels/overlapping.js
new file mode 100644 (file)
index 0000000..bd97ccd
--- /dev/null
@@ -0,0 +1,24 @@
+module.exports = {
+  config: {
+    type: 'polarArea',
+    data: {
+      datasets: [{
+        data: new Array(50).fill(5),
+        backgroundColor: ['#f003', '#0f03', '#00f3', '#0003']
+      }],
+      labels: new Array(50).fill(0).map((el, i) => ['label ' + i, 'line 2'])
+    },
+    options: {
+      scales: {
+        r: {
+          pointLabels: {
+            display: true,
+          }
+        }
+      }
+    }
+  },
+  options: {
+    spriteText: true
+  }
+};
diff --git a/test/fixtures/controller.polarArea/pointLabels/overlapping.png b/test/fixtures/controller.polarArea/pointLabels/overlapping.png
new file mode 100644 (file)
index 0000000..33dcebd
Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/overlapping.png differ