1/* * 2 * 3 * (c) 2010-2024 Torstein Honsi 4 * 5 * License: www.highcharts.com/license 6 * 7 * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! 8 * 9 * */ 10'use strict'; 11import DataLabel from '../../Core/Series/DataLabel.js'; 12import H from '../../Core/Globals.js'; 13const { composed, noop } = H; 14import R from '../../Core/Renderer/RendererUtilities.js'; 15const { distribute } = R; 16import SeriesRegistry from '../../Core/Series/SeriesRegistry.js'; 17const { series: Series } = SeriesRegistry; 18import U from '../../Core/Utilities.js'; 19const { arrayMax, clamp, defined, pick, pushUnique, relativeLength } = U; 20/* * 21 * 22 * Composition 23 * 24 * */ 25var ColumnDataLabel; 26(function (ColumnDataLabel) { 27 /* * 28 * 29 * Constants 30 * 31 * */ 32 const dataLabelPositioners = { 33 // Based on the value computed in Highcharts' distribute algorithm. 34 radialDistributionY: function (point, dataLabel) { 35 return (dataLabel.dataLabelPosition?.top || 0) + 36 point.distributeBox.pos; 37 }, 38 // Get the x - use the natural x position for labels near the top and 39 // bottom, to prevent the top and botton slice connectors from touching 40 // each other on either side. Based on the value computed in Highcharts' 41 // distribute algorithm. 42 radialDistributionX: function (series, point, y, naturalY, dataLabel) { 43 const pos = dataLabel.dataLabelPosition; 44 return series.getX(y < (pos?.top || 0) + 2 || y > (pos?.bottom || 0) - 2 ? 45 naturalY : 46 y, point.half, point, dataLabel); 47 }, 48 // The dataLabels.distance determines the x position of the label 49 justify: function (point, dataLabel, radius, seriesCenter) { 50 return seriesCenter[0] + (point.half ? -1 : 1) * 51 (radius + (dataLabel.dataLabelPosition?.distance || 0)); 52 }, 53 // Left edges of the left-half labels touch the left edge of the plot 54 // area. Right edges of the right-half labels touch the right edge of 55 // the plot area. 56 alignToPlotEdges: function (dataLabel, half, plotWidth, plotLeft) { 57 const dataLabelWidth = dataLabel.getBBox().width; 58 return half ? dataLabelWidth + plotLeft : 59 plotWidth - dataLabelWidth - plotLeft; 60 }, 61 // Connectors of each side end in the same x position. Labels are 62 // aligned to them. Left edge of the widest left-half label touches the 63 // left edge of the plot area. Right edge of the widest right-half label 64 // touches the right edge of the plot area. 65 alignToConnectors: function (points, half, plotWidth, plotLeft) { 66 let maxDataLabelWidth = 0, dataLabelWidth; 67 // Find widest data label 68 points.forEach(function (point) { 69 dataLabelWidth = point.dataLabel.getBBox().width; 70 if (dataLabelWidth > maxDataLabelWidth) { 71 maxDataLabelWidth = dataLabelWidth; 72 } 73 }); 74 return half ? maxDataLabelWidth + plotLeft : 75 plotWidth - maxDataLabelWidth - plotLeft; 76 } 77 }; 78 /* * 79 * 80 * Functions 81 * 82 * */ 83 /** @private */ 84 function compose(PieSeriesClass) { 85 DataLabel.compose(Series); 86 if (pushUnique(composed, 'PieDataLabel')) { 87 const pieProto = PieSeriesClass.prototype; 88 pieProto.dataLabelPositioners = dataLabelPositioners; 89 pieProto.alignDataLabel = noop; 90 pieProto.drawDataLabels = drawDataLabels; 91 pieProto.getDataLabelPosition = getDataLabelPosition; 92 pieProto.placeDataLabels = placeDataLabels; 93 pieProto.verifyDataLabelOverflow = verifyDataLabelOverflow; 94 } 95 } 96 ColumnDataLabel.compose = compose; 97 /** @private */ 98 function getDataLabelPosition(point, distance) { 99 const halfPI = Math.PI / 2, { start = 0, end = 0 } = point.shapeArgs || {}; 100 let angle = point.angle || 0; 101 // If a large slice is crossing the lowest point, prefer rendering it 45 102 // degrees out at either lower right or lower left. That's where there's 103 // most likely to be space available and avoid text being truncated 104 // (#22100). Technically this logic should also apply to the top point,
vendor: 19,944 bytes, lines 105-507
105 // but that is more of an edge case since the default start angle is at 106 // the top. 107 if (distance > 0 && 108 // Crossing the bottom 109 start < halfPI && end > halfPI && 110 // Angle within the bottom quadrant 111 angle > halfPI / 2 && angle < halfPI * 1.5) { 112 angle = angle <= halfPI ? 113 Math.max(halfPI / 2, (start + halfPI) / 2) : 114 Math.min(halfPI * 1.5, (halfPI + end) / 2); 115 } 116 const { center, options } = this, r = center[2] / 2, cosAngle = Math.cos(angle), sinAngle = Math.sin(angle), x = center[0] + cosAngle * r, y = center[1] + sinAngle * r, finalConnectorOffset = Math.min((options.slicedOffset || 0) + (options.borderWidth || 0), distance / 5); // #1678 117 return { 118 natural: { 119 // Initial position of the data label - it's utilized for 120 // finding the final position for the label 121 x: x + cosAngle * distance, 122 y: y + sinAngle * distance 123 }, 124 computed: { 125 // Used for generating connector path - initialized later in 126 // drawDataLabels function x: undefined, y: undefined 127 }, 128 // Left - pie on the left side of the data label 129 // Right - pie on the right side of the data label 130 // Center - data label overlaps the pie 131 alignment: distance < 0 ? 'center' : point.half ? 'right' : 'left', 132 connectorPosition: { 133 angle, 134 breakAt: { 135 x: x + cosAngle * finalConnectorOffset, 136 y: y + sinAngle * finalConnectorOffset 137 }, 138 touchingSliceAt: { 139 x, 140 y 141 } 142 }, 143 distance 144 }; 145 } 146 /** 147 * Override the base drawDataLabels method by pie specific functionality 148 * @private 149 */ 150 function drawDataLabels() { 151 const series = this, points = series.points, chart = series.chart, plotWidth = chart.plotWidth, plotHeight = chart.plotHeight, plotLeft = chart.plotLeft, maxWidth = Math.round(chart.chartWidth / 3), seriesCenter = series.center, radius = seriesCenter[2] / 2, centerY = seriesCenter[1], halves = [ 152 [], // Right 153 [] // Left 154 ], overflow = [0, 0, 0, 0], // Top, right, bottom, left 155 dataLabelPositioners = series.dataLabelPositioners; 156 let connector, dataLabelWidth, labelHeight, maxLabelDistance = 0; 157 // Get out if not enabled 158 if (!series.visible || !series.hasDataLabels?.()) { 159 return; 160 } 161 // Reset all labels that have been shortened 162 points.forEach((point) => { 163 (point.dataLabels || []).forEach((dataLabel) => { 164 if (dataLabel.shortened) { 165 dataLabel 166 .attr({ 167 width: 'auto' 168 }).css({ 169 width: 'auto', 170 textOverflow: 'clip' 171 }); 172 dataLabel.shortened = false; 173 } 174 }); 175 }); 176 // Run parent method 177 Series.prototype.drawDataLabels.apply(series); 178 points.forEach((point) => { 179 (point.dataLabels || []).forEach((dataLabel, i) => { 180 const r = seriesCenter[2] / 2, dataLabelOptions = dataLabel.options, distance = relativeLength(dataLabelOptions?.distance || 0, r); 181 // Arrange points for collision detection 182 if (i === 0) { 183 halves[point.half].push(point); 184 } 185 // Avoid long labels squeezing the pie size too far down 186 if (!defined(dataLabelOptions?.style?.width)) { 187 if (dataLabel.getBBox().width > maxWidth) { 188 dataLabel.css({ 189 // Use a fraction of the maxWidth to avoid wrapping 190 // close to the end of the string. 191 width: Math.round(maxWidth * 0.7) + 'px' 192 }); 193 dataLabel.shortened = true; 194 } 195 } 196 dataLabel.dataLabelPosition = this.getDataLabelPosition(point, distance); 197 maxLabelDistance = Math.max(maxLabelDistance, distance); 198 }); 199 }); 200 /* Loop over the points in each half, starting from the top and bottom 201 * of the pie to detect overlapping labels. 202 */ 203 halves.forEach((points, halfIdx) => { 204 const length = points.length, positions = []; 205 let top, bottom, size = 0, distributionLength; 206 if (!length) { 207 return; 208 } 209 // Sort by angle 210 series.sortByAngle(points, halfIdx - 0.5); 211 // Only do anti-collision when we have dataLabels outside the pie 212 // and have connectors. (#856) 213 if (maxLabelDistance > 0) { 214 top = Math.max(0, centerY - radius - maxLabelDistance); 215 bottom = Math.min(centerY + radius + maxLabelDistance, chart.plotHeight); 216 points.forEach((point) => { 217 // Check if specific points' label is outside the pie 218 (point.dataLabels || []).forEach((dataLabel) => { 219 const labelPosition = dataLabel.dataLabelPosition; 220 if (labelPosition && 221 labelPosition.distance > 0) { 222 // The point.top depends on point.labelDistance 223 // value. Used for calculation of y value in getX 224 // method 225 labelPosition.top = Math.max(0, centerY - radius - labelPosition.distance); 226 labelPosition.bottom = Math.min(centerY + radius + labelPosition.distance, chart.plotHeight); 227 size = dataLabel.getBBox().height || 21; 228 dataLabel.lineHeight = chart.renderer.fontMetrics(dataLabel.text || dataLabel).h + 2 * dataLabel.padding; 229 point.distributeBox = { 230 target: ((dataLabel.dataLabelPosition 231 ?.natural.y || 0) - 232 labelPosition.top + 233 dataLabel.lineHeight / 2), 234 size, 235 rank: point.y 236 }; 237 positions.push(point.distributeBox); 238 } 239 }); 240 }); 241 distributionLength = bottom + size - top; 242 distribute(positions, distributionLength, distributionLength / 5); 243 // Uncomment this to visualize the boxes 244 /* 245 points.forEach((point): void => { 246 const box = point.distributeBox; 247 point.dlBox?.destroy(); 248 if (box?.pos) { 249 point.dlBox = chart.renderer.rect( 250 chart.plotLeft + this.center[0] + ( 251 halfIdx ? 252 -this.center[2] / 2 - 100 : 253 this.center[2] / 2 254 ), 255 chart.plotTop + box.pos, 256 100, 257 box.size 258 ) 259 .attr({ 260 stroke: 'silver', 261 'stroke-width': 1 262 }) 263 .add(); 264 } 265 }); 266 // */ 267 } 268 // Now the used slots are sorted, fill them up sequentially 269 points.forEach((point) => { 270 (point.dataLabels || []).forEach((dataLabel) => { 271 const dataLabelOptions = (dataLabel.options || {}), distributeBox = point.distributeBox, labelPosition = dataLabel.dataLabelPosition, naturalY = labelPosition?.natural.y || 0, connectorPadding = dataLabelOptions 272 .connectorPadding || 0, lineHeight = dataLabel.lineHeight || 21, bBox = dataLabel.getBBox(), topOffset = (lineHeight - bBox.height) / 2; 273 let x = 0, y = naturalY, visibility = 'inherit'; 274 if (labelPosition) { 275 if (positions && 276 defined(distributeBox) && 277 labelPosition.distance > 0) { 278 if (typeof distributeBox.pos === 'undefined') { 279 visibility = 'hidden'; 280 } 281 else { 282 labelHeight = distributeBox.size; 283 // Find label's y position 284 y = dataLabelPositioners 285 .radialDistributionY(point, dataLabel); 286 } 287 } 288 // Find label's x position. The justify option is 289 // undocumented in the API - preserve support for it 290 if (dataLabelOptions.justify) { 291 x = dataLabelPositioners.justify(point, dataLabel, radius, seriesCenter); 292 } 293 else { 294 switch (dataLabelOptions.alignTo) { 295 case 'connectors': 296 x = dataLabelPositioners.alignToConnectors(points, halfIdx, plotWidth, plotLeft); 297 break; 298 case 'plotEdges': 299 x = dataLabelPositioners.alignToPlotEdges(dataLabel, halfIdx, plotWidth, plotLeft); 300 break; 301 default: 302 x = dataLabelPositioners.radialDistributionX(series, point, y - topOffset, naturalY, dataLabel); 303 } 304 } 305 // Record the placement and visibility 306 labelPosition.attribs = { 307 visibility, 308 align: labelPosition.alignment 309 }; 310 labelPosition.posAttribs = { 311 x: x + 312 (dataLabelOptions.x || 0) + // (#12985) 313 ({ 314 left: connectorPadding, 315 right: -connectorPadding 316 }[labelPosition.alignment] || 0), 317 y: y + 318 (dataLabelOptions.y || 0) - // (#12985) 319 // Vertically center 320 lineHeight / 2 321 }; 322 labelPosition.computed.x = x; 323 labelPosition.computed.y = y - topOffset; 324 // Detect overflowing data labels 325 if (pick(dataLabelOptions.crop, true)) { 326 dataLabelWidth = dataLabel.getBBox().width; 327 let sideOverflow; 328 // Overflow left 329 if (x - dataLabelWidth < connectorPadding && 330 halfIdx === 1 // Left half 331 ) { 332 sideOverflow = Math.round(dataLabelWidth - x + connectorPadding); 333 overflow[3] = Math.max(sideOverflow, overflow[3]); 334 // Overflow right 335 } 336 else if (x + dataLabelWidth > 337 plotWidth - connectorPadding && 338 halfIdx === 0 // Right half 339 ) { 340 sideOverflow = Math.round(x + 341 dataLabelWidth - 342 plotWidth + 343 connectorPadding); 344 overflow[1] = Math.max(sideOverflow, overflow[1]); 345 } 346 // Overflow top 347 if (y - labelHeight / 2 < 0) { 348 overflow[0] = Math.max(Math.round(-y + labelHeight / 2), overflow[0]); 349 // Overflow left 350 } 351 else if (y + labelHeight / 2 > plotHeight) { 352 overflow[2] = Math.max(Math.round(y + labelHeight / 2 - plotHeight), overflow[2]); 353 } 354 labelPosition.sideOverflow = sideOverflow; 355 } 356 } 357 }); // For each data label of the point 358 }); // For each point 359 }); // For each half 360 // Do not apply the final placement and draw the connectors until we 361 // have verified that labels are not spilling over. 362 if (arrayMax(overflow) === 0 || 363 this.verifyDataLabelOverflow(overflow)) { 364 // Place the labels in the final position 365 this.placeDataLabels(); 366 this.points.forEach((point) => { 367 (point.dataLabels || []).forEach((dataLabel) => { 368 // #8864: every connector can have individual options 369 const { connectorColor, connectorWidth = 1 } = (dataLabel.options || {}), labelPosition = dataLabel.dataLabelPosition; 370 // Draw the connector 371 if (connectorWidth) { 372 let isNew; 373 connector = dataLabel.connector; 374 if (labelPosition && labelPosition.distance > 0) { 375 isNew = !connector; 376 if (!connector) { 377 dataLabel.connector = connector = chart.renderer 378 .path() 379 .addClass('highcharts-data-label-connector ' + 380 ' highcharts-color-' + 381 point.colorIndex + 382 (point.className ? 383 ' ' + point.className : 384 '')) 385 .add(series.dataLabelsGroup); 386 } 387 if (!chart.styledMode) { 388 connector.attr({ 389 'stroke-width': connectorWidth, 390 'stroke': (connectorColor || 391 point.color || 392 "#666666" /* Palette.neutralColor60 */) 393 }); 394 } 395 connector[isNew ? 'attr' : 'animate']({ 396 d: point.getConnectorPath(dataLabel) 397 }); 398 connector.attr({ 399 visibility: labelPosition.attribs?.visibility 400 }); 401 } 402 else if (connector) { 403 dataLabel.connector = connector.destroy(); 404 } 405 } 406 }); 407 }); 408 } 409 } 410 /** 411 * Perform the final placement of the data labels after we have verified 412 * that they fall within the plot area. 413 * @private 414 */ 415 function placeDataLabels() { 416 this.points.forEach((point) => { 417 (point.dataLabels || []).forEach((dataLabel) => { 418 const labelPosition = dataLabel.dataLabelPosition; 419 if (labelPosition) { 420 // Shorten data labels with ellipsis if they still overflow 421 // after the pie has reached minSize (#223). 422 if (labelPosition.sideOverflow) { 423 dataLabel.css({ 424 width: (Math.max(dataLabel.getBBox().width - 425 labelPosition.sideOverflow, 0)) + 'px', 426 textOverflow: ((dataLabel.options?.style || {}) 427 .textOverflow || 428 'ellipsis') 429 }); 430 dataLabel.shortened = true; 431 } 432 dataLabel.attr(labelPosition.attribs); 433 dataLabel[dataLabel.moved ? 'animate' : 'attr'](labelPosition.posAttribs); 434 dataLabel.moved = true; 435 } 436 else if (dataLabel) { 437 dataLabel.attr({ y: -9999 }); 438 } 439 }); 440 // Clear for update 441 delete point.distributeBox; 442 }, this); 443 } 444 /** 445 * Verify whether the data labels are allowed to draw, or we should run more 446 * translation and data label positioning to keep them inside the plot area. 447 * Returns true when data labels are ready to draw. 448 * @private 449 */ 450 function verifyDataLabelOverflow(overflow) { 451 const center = this.center, options = this.options, centerOption = options.center, minSize = options.minSize || 80; 452 let newSize = minSize, 453 // If a size is set, return true and don't try to shrink the pie 454 // to fit the labels. 455 ret = options.size !== null; 456 if (!ret) { 457 // Handle horizontal size and center 458 if (centerOption[0] !== null) { // Fixed center 459 newSize = Math.max(center[2] - 460 Math.max(overflow[1], overflow[3]), minSize); 461 } 462 else { // Auto center 463 newSize = Math.max( 464 // Horizontal overflow 465 center[2] - overflow[1] - overflow[3], minSize); 466 // Horizontal center 467 center[0] += (overflow[3] - overflow[1]) / 2; 468 } 469 // Handle vertical size and center 470 if (centerOption[1] !== null) { // Fixed center 471 newSize = clamp(newSize, minSize, center[2] - Math.max(overflow[0], overflow[2])); 472 } 473 else { // Auto center 474 newSize = clamp(newSize, minSize, 475 // Vertical overflow 476 center[2] - overflow[0] - overflow[2]); 477 // Vertical center 478 center[1] += (overflow[0] - overflow[2]) / 2; 479 } 480 // If the size must be decreased, we need to run translate and 481 // drawDataLabels again 482 if (newSize < center[2]) { 483 center[2] = newSize; 484 center[3] = Math.min(// #3632 485 options.thickness ? 486 Math.max(0, newSize - options.thickness * 2) : 487 Math.max(0, relativeLength(options.innerSize || 0, newSize)), newSize); // #6647 488 this.translate(center); 489 if (this.drawDataLabels) { 490 this.drawDataLabels(); 491 } 492 // Else, return true to indicate that the pie and its labels is 493 // within the plot area 494 } 495 else { 496 ret = true; 497 } 498 } 499 return ret; 500 } 501})(ColumnDataLabel || (ColumnDataLabel = {})); 502/* * 503 * 504 * Default Export 505 * 506 * */ 507export default ColumnDataLabel;
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.