Skip to content

Commit

Permalink
Enable point labels hiding when overlapped (#11055)
Browse files Browse the repository at this point in the history
* Enable point labels hiding when overlapped

* fix cc

* fallback CC updates

* fixes CC
  • Loading branch information
stockiNail committed Apr 27, 2023
1 parent ee7e928 commit eff39c0
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 55 deletions.
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 @@ -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
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.

0 comments on commit eff39c0

Please sign in to comment.