Enable point labels hiding when overlapped (#11055)

* Enable point labels hiding when overlapped

* fix cc

* fallback CC updates

* fixes CC
This commit is contained in:
stockiNail 2023-04-28 00:28:55 +02:00 committed by GitHub
parent ee7e928cfe
commit eff39c0769
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 168 additions and 55 deletions

View 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)

View 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);
itemOpts.padding = padding[i];
itemOpts.size = labelSizes[i];
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
});
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 optsAtIndex = pointLabels.setContext(scale.getPointLabelContext(i));
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 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} = item;
renderText(
ctx,

View 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

View File

@ -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
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@ -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
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@ -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
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB