Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable point labels hiding when overlapped #11055

Merged
merged 4 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/axes/radial/linear.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
144 changes: 92 additions & 52 deletions src/scales/scale.radialLinear.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3384,10 +3384,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
Expand Down
25 changes: 25 additions & 0 deletions test/fixtures/controller.polarArea/pointLabels/displayAuto-180.js
Original file line number Diff line number Diff line change
@@ -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
}
};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions test/fixtures/controller.polarArea/pointLabels/displayAuto.js
Original file line number Diff line number Diff line change
@@ -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
}
};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions test/fixtures/controller.polarArea/pointLabels/overlapping.js
Original file line number Diff line number Diff line change
@@ -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
}
};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.