223 lines
7.7 KiB
JavaScript
223 lines
7.7 KiB
JavaScript
|
/* *
|
||
|
*
|
||
|
* Highcharts module to hide overlapping data labels.
|
||
|
* This module is included in Highcharts.
|
||
|
*
|
||
|
* (c) 2009-2024 Torstein Honsi
|
||
|
*
|
||
|
* License: www.highcharts.com/license
|
||
|
*
|
||
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||
|
*
|
||
|
* */
|
||
|
'use strict';
|
||
|
import U from '../Core/Utilities.js';
|
||
|
const { addEvent, fireEvent, objectEach, pick } = U;
|
||
|
/* *
|
||
|
*
|
||
|
* Functions
|
||
|
*
|
||
|
* */
|
||
|
/**
|
||
|
* Hide overlapping labels. Labels are moved and faded in and out on zoom to
|
||
|
* provide a smooth visual impression.
|
||
|
*
|
||
|
* @requires modules/overlapping-datalabels
|
||
|
*
|
||
|
* @private
|
||
|
* @function Highcharts.Chart#hideOverlappingLabels
|
||
|
* @param {Array<Highcharts.SVGElement>} labels
|
||
|
* Rendered data labels
|
||
|
*/
|
||
|
function chartHideOverlappingLabels(labels) {
|
||
|
const chart = this, len = labels.length, isIntersectRect = (box1, box2) => !(box2.x >= box1.x + box1.width ||
|
||
|
box2.x + box2.width <= box1.x ||
|
||
|
box2.y >= box1.y + box1.height ||
|
||
|
box2.y + box2.height <= box1.y);
|
||
|
/**
|
||
|
* Get the box with its position inside the chart, as opposed to getBBox
|
||
|
* that only reports the position relative to the parent.
|
||
|
*/
|
||
|
function getAbsoluteBox(label) {
|
||
|
if (label && (!label.alignAttr || label.placed)) {
|
||
|
const padding = label.box ? 0 : (label.padding || 0), pos = label.alignAttr || {
|
||
|
x: label.attr('x'),
|
||
|
y: label.attr('y')
|
||
|
}, bBox = label.getBBox();
|
||
|
label.width = bBox.width;
|
||
|
label.height = bBox.height;
|
||
|
return {
|
||
|
x: pos.x + (label.parentGroup?.translateX || 0) + padding,
|
||
|
y: pos.y + (label.parentGroup?.translateY || 0) + padding,
|
||
|
width: (label.width || 0) - 2 * padding,
|
||
|
height: (label.height || 0) - 2 * padding
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
let label, label1, label2, box1, box2, isLabelAffected = false;
|
||
|
for (let i = 0; i < len; i++) {
|
||
|
label = labels[i];
|
||
|
if (label) {
|
||
|
// Mark with initial opacity
|
||
|
label.oldOpacity = label.opacity;
|
||
|
label.newOpacity = 1;
|
||
|
label.absoluteBox = getAbsoluteBox(label);
|
||
|
}
|
||
|
}
|
||
|
// Prevent a situation in a gradually rising slope, that each label will
|
||
|
// hide the previous one because the previous one always has lower rank.
|
||
|
labels.sort((a, b) => (b.labelrank || 0) - (a.labelrank || 0));
|
||
|
// Detect overlapping labels
|
||
|
for (let i = 0; i < len; ++i) {
|
||
|
label1 = labels[i];
|
||
|
box1 = label1 && label1.absoluteBox;
|
||
|
for (let j = i + 1; j < len; ++j) {
|
||
|
label2 = labels[j];
|
||
|
box2 = label2 && label2.absoluteBox;
|
||
|
if (box1 &&
|
||
|
box2 &&
|
||
|
label1 !== label2 && // #6465, polar chart with connectEnds
|
||
|
label1.newOpacity !== 0 &&
|
||
|
label2.newOpacity !== 0 &&
|
||
|
// #15863 dataLabels are no longer hidden by translation
|
||
|
label1.visibility !== 'hidden' &&
|
||
|
label2.visibility !== 'hidden') {
|
||
|
if (isIntersectRect(box1, box2)) {
|
||
|
(label1.labelrank < label2.labelrank ? label1 : label2)
|
||
|
.newOpacity = 0;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// Hide or show
|
||
|
for (const label of labels) {
|
||
|
if (hideOrShow(label, chart)) {
|
||
|
isLabelAffected = true;
|
||
|
}
|
||
|
}
|
||
|
if (isLabelAffected) {
|
||
|
fireEvent(chart, 'afterHideAllOverlappingLabels');
|
||
|
}
|
||
|
}
|
||
|
/** @private */
|
||
|
function compose(ChartClass) {
|
||
|
const chartProto = ChartClass.prototype;
|
||
|
if (!chartProto.hideOverlappingLabels) {
|
||
|
chartProto.hideOverlappingLabels = chartHideOverlappingLabels;
|
||
|
addEvent(ChartClass, 'render', onChartRender);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Hide or show labels based on opacity.
|
||
|
*
|
||
|
* @private
|
||
|
* @function hideOrShow
|
||
|
* @param {Highcharts.SVGElement} label
|
||
|
* The label.
|
||
|
* @param {Highcharts.Chart} chart
|
||
|
* The chart that contains the label.
|
||
|
* @return {boolean}
|
||
|
* Whether label is affected
|
||
|
*/
|
||
|
function hideOrShow(label, chart) {
|
||
|
let complete, newOpacity, isLabelAffected = false;
|
||
|
if (label) {
|
||
|
newOpacity = label.newOpacity;
|
||
|
if (label.oldOpacity !== newOpacity) {
|
||
|
// Toggle data labels
|
||
|
if (label.hasClass('highcharts-data-label')) {
|
||
|
// Make sure the label is completely hidden to avoid catching
|
||
|
// clicks (#4362)
|
||
|
label[newOpacity ? 'removeClass' : 'addClass']('highcharts-data-label-hidden');
|
||
|
complete = function () {
|
||
|
if (!chart.styledMode) {
|
||
|
label.css({
|
||
|
pointerEvents: newOpacity ? 'auto' : 'none'
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
isLabelAffected = true;
|
||
|
// Animate or set the opacity
|
||
|
label[label.isOld ? 'animate' : 'attr']({ opacity: newOpacity }, void 0, complete);
|
||
|
fireEvent(chart, 'afterHideOverlappingLabel');
|
||
|
// Toggle other labels, tick labels
|
||
|
}
|
||
|
else {
|
||
|
label.attr({
|
||
|
opacity: newOpacity
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
label.isOld = true;
|
||
|
}
|
||
|
return isLabelAffected;
|
||
|
}
|
||
|
/**
|
||
|
* Collect potential overlapping data labels. Stack labels probably don't need
|
||
|
* to be considered because they are usually accompanied by data labels that lie
|
||
|
* inside the columns.
|
||
|
* @private
|
||
|
*/
|
||
|
function onChartRender() {
|
||
|
const chart = this;
|
||
|
let labels = [];
|
||
|
// Consider external label collectors
|
||
|
for (const collector of (chart.labelCollectors || [])) {
|
||
|
labels = labels.concat(collector());
|
||
|
}
|
||
|
for (const yAxis of (chart.yAxis || [])) {
|
||
|
if (yAxis.stacking &&
|
||
|
yAxis.options.stackLabels &&
|
||
|
!yAxis.options.stackLabels.allowOverlap) {
|
||
|
objectEach(yAxis.stacking.stacks, (stack) => {
|
||
|
objectEach(stack, (stackItem) => {
|
||
|
if (stackItem.label) {
|
||
|
labels.push(stackItem.label);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
for (const series of (chart.series || [])) {
|
||
|
if (series.visible && series.hasDataLabels?.()) { // #3866
|
||
|
const push = (points) => {
|
||
|
for (const point of points) {
|
||
|
if (point.visible) {
|
||
|
(point.dataLabels || []).forEach((label) => {
|
||
|
const options = label.options || {};
|
||
|
label.labelrank = pick(options.labelrank, point.labelrank, point.shapeArgs?.height); // #4118
|
||
|
// Allow overlap if the option is explicitly true
|
||
|
if (
|
||
|
// #13449
|
||
|
options.allowOverlap ??
|
||
|
// Pie labels outside have a separate placement
|
||
|
// logic, skip the overlap logic
|
||
|
Number(options.distance) > 0) {
|
||
|
label.oldOpacity = label.opacity;
|
||
|
label.newOpacity = 1;
|
||
|
hideOrShow(label, chart);
|
||
|
// Do not allow overlap
|
||
|
}
|
||
|
else {
|
||
|
labels.push(label);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
push(series.nodes || []);
|
||
|
push(series.points);
|
||
|
}
|
||
|
}
|
||
|
this.hideOverlappingLabels(labels);
|
||
|
}
|
||
|
/* *
|
||
|
*
|
||
|
* Default Export
|
||
|
*
|
||
|
* */
|
||
|
const OverlappingDataLabels = {
|
||
|
compose
|
||
|
};
|
||
|
export default OverlappingDataLabels;
|