Skip to content

Commit eff39c0

Browse files
authored
Enable point labels hiding when overlapped (#11055)
* Enable point labels hiding when overlapped * fix cc * fallback CC updates * fixes CC
1 parent ee7e928 commit eff39c0

File tree

9 files changed

+168
-55
lines changed

9 files changed

+168
-55
lines changed

docs/axes/radial/linear.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ Namespace: `options.scales[scaleId].pointLabels`
154154
| `backdropColor` | [`Color`](../../general/colors.md) | `true` | `undefined` | Background color of the point label.
155155
| `backdropPadding` | [`Padding`](../../general/padding.md) | | `2` | Padding of label backdrop.
156156
| `borderRadius` | `number`\|`object` | `true` | `0` | Border radius of the point label
157-
| `display` | `boolean` | | `true` | If true, point labels are shown.
157+
| `display` | `boolean`\|`string` | | `true` | If true, point labels are shown. When `display: 'auto'`, the label is hidden if it overlaps with another label.
158158
| `callback` | `function` | | | Callback function to transform data labels to point labels. The default implementation simply returns the current string.
159159
| `color` | [`Color`](../../general/colors.md) | Yes | `Chart.defaults.color` | Color of label.
160160
| `font` | `Font` | Yes | `Chart.defaults.font` | See [Fonts](../../general/fonts.md)

src/scales/scale.radialLinear.js

+92-52
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import defaults from '../core/core.defaults.js';
2-
import {_longestText, addRoundedRectPath, renderText} from '../helpers/helpers.canvas.js';
2+
import {_longestText, addRoundedRectPath, renderText, _isPointInArea} from '../helpers/helpers.canvas.js';
33
import {HALF_PI, TAU, toDegrees, toRadians, _normalizeAngle, PI} from '../helpers/helpers.math.js';
44
import LinearScaleBase from './scale.linearbase.js';
55
import Ticks from '../core/core.ticks.js';
@@ -136,36 +136,66 @@ function updateLimits(limits, orig, angle, hLimits, vLimits) {
136136
}
137137
}
138138

139+
function createPointLabelItem(scale, index, itemOpts) {
140+
const outerDistance = scale.drawingArea;
141+
const {extra, additionalAngle, padding, size} = itemOpts;
142+
const pointLabelPosition = scale.getPointPosition(index, outerDistance + extra + padding, additionalAngle);
143+
const angle = Math.round(toDegrees(_normalizeAngle(pointLabelPosition.angle + HALF_PI)));
144+
const y = yForAngle(pointLabelPosition.y, size.h, angle);
145+
const textAlign = getTextAlignForAngle(angle);
146+
const left = leftForTextAlign(pointLabelPosition.x, size.w, textAlign);
147+
return {
148+
// if to draw or overlapped
149+
visible: true,
150+
151+
// Text position
152+
x: pointLabelPosition.x,
153+
y,
154+
155+
// Text rendering data
156+
textAlign,
157+
158+
// Bounding box
159+
left,
160+
top: y,
161+
right: left + size.w,
162+
bottom: y + size.h
163+
};
164+
}
165+
166+
function isNotOverlapped(item, area) {
167+
if (!area) {
168+
return true;
169+
}
170+
const {left, top, right, bottom} = item;
171+
const apexesInArea = _isPointInArea({x: left, y: top}, area) || _isPointInArea({x: left, y: bottom}, area) ||
172+
_isPointInArea({x: right, y: top}, area) || _isPointInArea({x: right, y: bottom}, area);
173+
return !apexesInArea;
174+
}
175+
139176
function buildPointLabelItems(scale, labelSizes, padding) {
140177
const items = [];
141178
const valueCount = scale._pointLabels.length;
142179
const opts = scale.options;
143-
const extra = getTickBackdropHeight(opts) / 2;
144-
const outerDistance = scale.drawingArea;
145-
const additionalAngle = opts.pointLabels.centerPointLabels ? PI / valueCount : 0;
180+
const {centerPointLabels, display} = opts.pointLabels;
181+
const itemOpts = {
182+
extra: getTickBackdropHeight(opts) / 2,
183+
additionalAngle: centerPointLabels ? PI / valueCount : 0
184+
};
185+
let area;
146186

147187
for (let i = 0; i < valueCount; i++) {
148-
const pointLabelPosition = scale.getPointPosition(i, outerDistance + extra + padding[i], additionalAngle);
149-
const angle = Math.round(toDegrees(_normalizeAngle(pointLabelPosition.angle + HALF_PI)));
150-
const size = labelSizes[i];
151-
const y = yForAngle(pointLabelPosition.y, size.h, angle);
152-
const textAlign = getTextAlignForAngle(angle);
153-
const left = leftForTextAlign(pointLabelPosition.x, size.w, textAlign);
154-
155-
items.push({
156-
// Text position
157-
x: pointLabelPosition.x,
158-
y,
159-
160-
// Text rendering data
161-
textAlign,
162-
163-
// Bounding box
164-
left,
165-
top: y,
166-
right: left + size.w,
167-
bottom: y + size.h
168-
});
188+
itemOpts.padding = padding[i];
189+
itemOpts.size = labelSizes[i];
190+
191+
const item = createPointLabelItem(scale, i, itemOpts);
192+
items.push(item);
193+
if (display === 'auto') {
194+
item.visible = isNotOverlapped(item, area);
195+
if (item.visible) {
196+
area = item;
197+
}
198+
}
169199
}
170200
return items;
171201
}
@@ -198,39 +228,49 @@ function yForAngle(y, h, angle) {
198228
return y;
199229
}
200230

231+
function drawPointLabelBox(ctx, opts, item) {
232+
const {left, top, right, bottom} = item;
233+
const {backdropColor} = opts;
234+
235+
if (!isNullOrUndef(backdropColor)) {
236+
const borderRadius = toTRBLCorners(opts.borderRadius);
237+
const padding = toPadding(opts.backdropPadding);
238+
ctx.fillStyle = backdropColor;
239+
240+
const backdropLeft = left - padding.left;
241+
const backdropTop = top - padding.top;
242+
const backdropWidth = right - left + padding.width;
243+
const backdropHeight = bottom - top + padding.height;
244+
245+
if (Object.values(borderRadius).some(v => v !== 0)) {
246+
ctx.beginPath();
247+
addRoundedRectPath(ctx, {
248+
x: backdropLeft,
249+
y: backdropTop,
250+
w: backdropWidth,
251+
h: backdropHeight,
252+
radius: borderRadius,
253+
});
254+
ctx.fill();
255+
} else {
256+
ctx.fillRect(backdropLeft, backdropTop, backdropWidth, backdropHeight);
257+
}
258+
}
259+
}
260+
201261
function drawPointLabels(scale, labelCount) {
202262
const {ctx, options: {pointLabels}} = scale;
203263

204264
for (let i = labelCount - 1; i >= 0; i--) {
265+
const item = scale._pointLabelItems[i];
266+
if (!item.visible) {
267+
// overlapping
268+
continue;
269+
}
205270
const optsAtIndex = pointLabels.setContext(scale.getPointLabelContext(i));
271+
drawPointLabelBox(ctx, optsAtIndex, item);
206272
const plFont = toFont(optsAtIndex.font);
207-
const {x, y, textAlign, left, top, right, bottom} = scale._pointLabelItems[i];
208-
const {backdropColor} = optsAtIndex;
209-
210-
if (!isNullOrUndef(backdropColor)) {
211-
const borderRadius = toTRBLCorners(optsAtIndex.borderRadius);
212-
const padding = toPadding(optsAtIndex.backdropPadding);
213-
ctx.fillStyle = backdropColor;
214-
215-
const backdropLeft = left - padding.left;
216-
const backdropTop = top - padding.top;
217-
const backdropWidth = right - left + padding.width;
218-
const backdropHeight = bottom - top + padding.height;
219-
220-
if (Object.values(borderRadius).some(v => v !== 0)) {
221-
ctx.beginPath();
222-
addRoundedRectPath(ctx, {
223-
x: backdropLeft,
224-
y: backdropTop,
225-
w: backdropWidth,
226-
h: backdropHeight,
227-
radius: borderRadius,
228-
});
229-
ctx.fill();
230-
} else {
231-
ctx.fillRect(backdropLeft, backdropTop, backdropWidth, backdropHeight);
232-
}
233-
}
273+
const {x, y, textAlign} = item;
234274

235275
renderText(
236276
ctx,

src/types/index.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3500,10 +3500,10 @@ export type RadialLinearScaleOptions = CoreScaleOptions & {
35003500
borderRadius: Scriptable<number | BorderRadius, ScriptableScalePointLabelContext>;
35013501

35023502
/**
3503-
* if true, point labels are shown.
3503+
* if true, point labels are shown. When `display: 'auto'`, the label is hidden if it overlaps with another label.
35043504
* @default true
35053505
*/
3506-
display: boolean;
3506+
display: boolean | 'auto';
35073507
/**
35083508
* Color of label
35093509
* @see Defaults.color
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module.exports = {
2+
config: {
3+
type: 'polarArea',
4+
data: {
5+
datasets: [{
6+
data: new Array(50).fill(5),
7+
backgroundColor: ['#f003', '#0f03', '#00f3', '#0003']
8+
}],
9+
labels: new Array(50).fill(0).map((el, i) => ['label ' + i, 'line 2'])
10+
},
11+
options: {
12+
scales: {
13+
r: {
14+
startAngle: 180,
15+
pointLabels: {
16+
display: 'auto',
17+
}
18+
}
19+
}
20+
}
21+
},
22+
options: {
23+
spriteText: true
24+
}
25+
};
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module.exports = {
2+
config: {
3+
type: 'polarArea',
4+
data: {
5+
datasets: [{
6+
data: new Array(50).fill(5),
7+
backgroundColor: ['#f003', '#0f03', '#00f3', '#0003']
8+
}],
9+
labels: new Array(50).fill(0).map((el, i) => ['label ' + i, 'line 2'])
10+
},
11+
options: {
12+
scales: {
13+
r: {
14+
pointLabels: {
15+
display: 'auto',
16+
}
17+
}
18+
}
19+
}
20+
},
21+
options: {
22+
spriteText: true
23+
}
24+
};
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module.exports = {
2+
config: {
3+
type: 'polarArea',
4+
data: {
5+
datasets: [{
6+
data: new Array(50).fill(5),
7+
backgroundColor: ['#f003', '#0f03', '#00f3', '#0003']
8+
}],
9+
labels: new Array(50).fill(0).map((el, i) => ['label ' + i, 'line 2'])
10+
},
11+
options: {
12+
scales: {
13+
r: {
14+
pointLabels: {
15+
display: true,
16+
}
17+
}
18+
}
19+
}
20+
},
21+
options: {
22+
spriteText: true
23+
}
24+
};
Loading

0 commit comments

Comments
 (0)