diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index 465649825b9..2ed3a9e005a 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -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) diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index 7d41d36830c..ae44adbcd5c 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -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, diff --git a/src/types/index.d.ts b/src/types/index.d.ts index c4f042ec16e..3cbf1b6aafb 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -3500,10 +3500,10 @@ export type RadialLinearScaleOptions = CoreScaleOptions & { borderRadius: Scriptable; /** - * 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 index 00000000000..91e47c69e00 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/displayAuto-180.js @@ -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 index 00000000000..c0bb6730ede 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 index 00000000000..14b85eab055 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/displayAuto.js @@ -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 index 00000000000..271fbd2ed52 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 index 00000000000..bd97ccd85ca --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/overlapping.js @@ -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 index 00000000000..33dcebd0bec Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/overlapping.png differ