1/* * 2 * 3 * Highcharts module to hide overlapping data labels. 4 * This module is included in Highcharts. 5 * 6 * (c) 2009-2024 Torstein Honsi 7 * 8 * License: www.highcharts.com/license 9 * 10 * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! 11 * 12 * */ 13'use strict'; 14import GeometryUtilities from '../Core/Geometry/GeometryUtilities.js'; 15const { pointInPolygon } = GeometryUtilities; 16import U from '../Core/Utilities.js'; 17const { addEvent, fireEvent, objectEach, pick } = U; 18/* * 19 * 20 * Functions 21 * 22 * */ 23/** 24 * Hide overlapping labels. Labels are moved and faded in and out on zoom to 25 * provide a smooth visual impression. 26 * 27 * @requires modules/overlapping-datalabels 28 * 29 * @private 30 * @function Highcharts.Chart#hideOverlappingLabels 31 * @param {Array<Highcharts.SVGElement>} labels 32 * Rendered data labels 33 */ 34function chartHideOverlappingLabels(labels) { 35 const chart = this, len = labels.length, isIntersectRect = (box1, box2) => !(box2.x >= box1.x + box1.width || 36 box2.x + box2.width <= box1.x || 37 box2.y >= box1.y + box1.height || 38 box2.y + box2.height <= box1.y), isPolygonOverlap = (box1Poly, box2Poly) => { 39 for (const p of box1Poly) { 40 if (pointInPolygon({ x: p[0], y: p[1] }, box2Poly)) { 41 return true; 42 } 43 } 44 return false; 45 }; 46 /** 47 * Get the box with its position inside the chart, as opposed to getBBox 48 * that only reports the position relative to the parent. 49 */ 50 function getAbsoluteBox(label) { 51 if (label && (!label.alignAttr || label.placed)) { 52 const padding = label.box ? 0 : (label.padding || 0), pos = label.alignAttr || { 53 x: label.attr('x'), 54 y: label.attr('y') 55 }, bBox = label.getBBox(); 56 label.width = bBox.width; 57 label.height = bBox.height; 58 return { 59 x: pos.x + (label.parentGroup?.translateX || 0) + padding, 60 y: pos.y + (label.parentGroup?.translateY || 0) + padding, 61 width: (label.width || 0) - 2 * padding, 62 height: (label.height || 0) - 2 * padding, 63 polygon: bBox?.polygon 64 }; 65 } 66 } 67 let label, label1, label2, box1, box2, isLabelAffected = false; 68 for (let i = 0; i < len; i++) { 69 label = labels[i]; 70 if (label) { 71 // Mark with initial opacity 72 label.oldOpacity = label.opacity; 73 label.newOpacity = 1; 74 label.absoluteBox = getAbsoluteBox(label); 75 } 76 } 77 // Prevent a situation in a gradually rising slope, that each label will 78 // hide the previous one because the previous one always has lower rank. 79 labels.sort((a, b) => (b.labelrank || 0) - (a.labelrank || 0)); 80 // Detect overlapping labels 81 for (let i = 0; i < len; ++i) { 82 label1 = labels[i]; 83 box1 = label1 && label1.absoluteBox; 84 const box1Poly = box1?.polygon; 85 for (let j = i + 1; j < len; ++j) { 86 label2 = labels[j]; 87 box2 = label2 && label2.absoluteBox; 88 let toHide = false;
89 if (box1 && 90 box2 && 91 label1 !== label2 && // #6465, polar chart with connectEnds 92 label1.newOpacity !== 0 && 93 label2.newOpacity !== 0 && 94 // #15863 dataLabels are no longer hidden by translation 95 label1.visibility !== 'hidden' && 96 label2.visibility !== 'hidden') { 97 const box2Poly = box2.polygon; 98 // If labels have polygons, only evaluate 99 // based on polygons 100 if (box1Poly && 101 box2Poly && 102 box1Poly !== box2Poly) { 103 if (isPolygonOverlap(box1Poly, box2Poly)) { 104 toHide = true; 105 } 106 // If there are no polygons, evaluate rectangles coliding 107 } 108 else if (isIntersectRect(box1, box2)) { 109 toHide = true; 110 } 111 if (toHide) { 112 const overlappingLabel = (label1.labelrank < label2.labelrank ? 113 label1 : 114 label2), labelText = overlappingLabel.text; 115 overlappingLabel.newOpacity = 0; 116 if (labelText?.element.querySelector('textPath')) { 117 labelText.hide(); 118 } 119 } 120 } 121 } 122 } 123 // Hide or show 124 for (const label of labels) { 125 if (hideOrShow(label, chart)) { 126 isLabelAffected = true; 127 } 128 } 129 if (isLabelAffected) { 130 fireEvent(chart, 'afterHideAllOverlappingLabels'); 131 } 132} 133/** @private */ 134function compose(ChartClass) { 135 const chartProto = ChartClass.prototype; 136 if (!chartProto.hideOverlappingLabels) { 137 chartProto.hideOverlappingLabels = chartHideOverlappingLabels; 138 addEvent(ChartClass, 'render', onChartRender); 139 } 140} 141/** 142 * Hide or show labels based on opacity. 143 * 144 * @private 145 * @function hideOrShow 146 * @param {Highcharts.SVGElement} label 147 * The label. 148 * @param {Highcharts.Chart} chart 149 * The chart that contains the label. 150 * @return {boolean} 151 * Whether label is affected 152 */ 153function hideOrShow(label, chart) { 154 let complete, newOpacity, isLabelAffected = false; 155 if (label) { 156 newOpacity = label.newOpacity; 157 if (label.oldOpacity !== newOpacity) { 158 // Toggle data labels 159 if (label.hasClass('highcharts-data-label')) { 160 // Make sure the label is completely hidden to avoid catching 161 // clicks (#4362) 162 label[newOpacity ? 'removeClass' : 'addClass']('highcharts-data-label-hidden'); 163 complete = function () { 164 if (!chart.styledMode) { 165 label.css({ 166 pointerEvents: newOpacity ? 'auto' : 'none' 167 }); 168 } 169 }; 170 isLabelAffected = true; 171 // Animate or set the opacity 172 label[label.isOld ? 'animate' : 'attr']({ opacity: newOpacity }, void 0, complete); 173 fireEvent(chart, 'afterHideOverlappingLabel'); 174 // Toggle other labels, tick labels 175 } 176 else { 177 label.attr({ 178 opacity: newOpacity 179 }); 180 } 181 } 182 label.isOld = true; 183 } 184 return isLabelAffected; 185} 186/** 187 * Collect potential overlapping data labels. Stack labels probably don't need 188 * to be considered because they are usually accompanied by data labels that lie 189 * inside the columns. 190 * @private 191 */ 192function onChartRender() { 193 const chart = this; 194 let labels = []; 195 // Consider external label collectors 196 for (const collector of (chart.labelCollectors || [])) { 197 labels = labels.concat(collector()); 198 } 199 for (const yAxis of (chart.yAxis || [])) { 200 if (yAxis.stacking && 201 yAxis.options.stackLabels && 202 !yAxis.options.stackLabels.allowOverlap) { 203 objectEach(yAxis.stacking.stacks, (stack) => { 204 objectEach(stack, (stackItem) => { 205 if (stackItem.label) { 206 labels.push(stackItem.label); 207 } 208 }); 209 }); 210 } 211 } 212 for (const series of (chart.series || [])) { 213 if (series.visible && series.hasDataLabels?.()) { // #3866 214 const push = (points) => { 215 for (const point of points) { 216 if (point.visible) {
217 (point.dataLabels || []).forEach((label) => { 218 const options = label.options || {}; 219 label.labelrank = pick(options.labelrank, point.labelrank, point.shapeArgs?.height); // #4118 220 // Allow overlap if the option is explicitly true 221 if ( 222 // #13449 223 options.allowOverlap ?? 224 // Pie labels outside have a separate placement 225 // logic, skip the overlap logic 226 Number(options.distance) > 0) { 227 label.oldOpacity = label.opacity; 228 label.newOpacity = 1; 229 hideOrShow(label, chart); 230 // Do not allow overlap 231 } 232 else { 233 labels.push(label); 234 } 235 }); 236 } 237 } 238 }; 239 push(series.nodes || []); 240 push(series.points); 241 } 242 } 243 this.hideOverlappingLabels(labels); 244} 245/* * 246 * 247 * Default Export 248 * 249 * */ 250const OverlappingDataLabels = { 251 compose 252}; 253export default OverlappingDataLabels;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.