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 A from './Animation/AnimationUtilities.js'; 12const { animObject } = A; 13import F from './Templating.js'; 14const { format } = F; 15import H from './Globals.js'; 16const { composed, dateFormats, doc, isSafari } = H; 17import R from './Renderer/RendererUtilities.js'; 18const { distribute } = R; 19import RendererRegistry from './Renderer/RendererRegistry.js'; 20import U from './Utilities.js'; 21const { addEvent, clamp, css, discardElement, extend, fireEvent, isArray, isNumber, isObject, isString, merge, pick, pushUnique, splat, syncTimeout } = U; 22/* * 23 * 24 * Class 25 * 26 * */ 27/* eslint-disable no-invalid-this, valid-jsdoc */ 28/** 29 * Tooltip of a chart. 30 * 31 * @class 32 * @name Highcharts.Tooltip 33 * 34 * @param {Highcharts.Chart} chart 35 * The chart instance. 36 * 37 * @param {Highcharts.TooltipOptions} options 38 * Tooltip options. 39 * 40 * @param {Highcharts.Pointer} pointer 41 * The pointer instance. 42 */ 43class Tooltip { 44 /* * 45 * 46 * Constructors 47 * 48 * */ 49 constructor(chart, options, pointer) { 50 /* * 51 * 52 * Properties 53 * 54 * */ 55 this.allowShared = true; 56 this.crosshairs = []; 57 this.distance = 0; 58 this.isHidden = true; 59 this.isSticky = false; 60 this.options = {}; 61 this.outside = false;
62 this.chart = chart; 63 this.init(chart, options); 64 this.pointer = pointer; 65 } 66 /* * 67 * 68 * Functions 69 * 70 * */ 71 /** 72 * Build the body (lines) of the tooltip by iterating over the items and 73 * returning one entry for each item, abstracting this functionality allows 74 * to easily overwrite and extend it. 75 * 76 * @private 77 * @function Highcharts.Tooltip#bodyFormatter 78 */ 79 bodyFormatter(points) { 80 return points.map((point) => { 81 const tooltipOptions = point.series.tooltipOptions, formatPrefix = point.formatPrefix || 'point'; 82 return (tooltipOptions[formatPrefix + 'Formatter'] || 83 point.tooltipFormatter).call(point, tooltipOptions[formatPrefix + 'Format'] || ''); 84 }); 85 } 86 /** 87 * Destroy the single tooltips in a split tooltip. 88 * If the tooltip is active then it is not destroyed, unless forced to. 89 * 90 * @private 91 * @function Highcharts.Tooltip#cleanSplit 92 * 93 * @param {boolean} [force] 94 * Force destroy all tooltips. 95 */ 96 cleanSplit(force) { 97 this.chart.series.forEach(function (series) { 98 const tt = series && series.tt; 99 if (tt) { 100 if (!tt.isActive || force) { 101 series.tt = tt.destroy(); 102 } 103 else { 104 tt.isActive = false; 105 } 106 } 107 }); 108 } 109 /** 110 * In case no user defined formatter is given, this will be used. Note that 111 * the context here is an object holding point, series, x, y etc. 112 * 113 * @function Highcharts.Tooltip#defaultFormatter 114 * 115 * @param {Highcharts.Tooltip} tooltip 116 * 117 * @return {string|Array<string>} 118 * Returns a string (single tooltip and shared) 119 * or an array of strings (split tooltip) 120 */ 121 defaultFormatter(tooltip) { 122 const hoverPoints = this.points || splat(this); 123 let s; 124 // Build the header 125 s = [tooltip.headerFooterFormatter(hoverPoints[0])]; 126 // Build the values 127 s = s.concat(tooltip.bodyFormatter(hoverPoints)); 128 // Footer 129 s.push(tooltip.headerFooterFormatter(hoverPoints[0], true)); 130 return s; 131 } 132 /** 133 * Removes and destroys the tooltip and its elements. 134 * 135 * @function Highcharts.Tooltip#destroy 136 */ 137 destroy() { 138 // Destroy and clear local variables 139 if (this.label) { 140 this.label = this.label.destroy(); 141 } 142 if (this.split) { 143 this.cleanSplit(true); 144 if (this.tt) { 145 this.tt = this.tt.destroy(); 146 } 147 } 148 if (this.renderer) { 149 this.renderer = this.renderer.destroy(); 150 discardElement(this.container); 151 } 152 U.clearTimeout(this.hideTimer); 153 } 154 /** 155 * Extendable method to get the anchor position of the tooltip 156 * from a point or set of points 157 * 158 * @private 159 * @function Highcharts.Tooltip#getAnchor 160 */ 161 getAnchor(points, mouseEvent) { 162 const { chart, pointer } = this, inverted = chart.inverted, plotTop = chart.plotTop, plotLeft = chart.plotLeft; 163 let ret; 164 points = splat(points); 165 // If reversedStacks are false the tooltip position should be taken from 166 // the last point (#17948) 167 if (points[0].series && 168 points[0].series.yAxis && 169 !points[0].series.yAxis.options.reversedStacks) { 170 points = points.slice().reverse(); 171 } 172 // When tooltip follows mouse, relate the position to the mouse 173 if (this.followPointer && mouseEvent) { 174 if (typeof mouseEvent.chartX === 'undefined') { 175 mouseEvent = pointer.normalize(mouseEvent); 176 } 177 ret = [ 178 mouseEvent.chartX - plotLeft, 179 mouseEvent.chartY - plotTop 180 ]; 181 // Some series types use a specificly calculated tooltip position for 182 // each point 183 } 184 else if (points[0].tooltipPos) { 185 ret = points[0].tooltipPos; 186 // Calculate the average position and adjust for axis positions 187 } 188 else { 189 let chartX = 0, chartY = 0;
190 points.forEach(function (point) { 191 const pos = point.pos(true); 192 if (pos) { 193 chartX += pos[0]; 194 chartY += pos[1]; 195 } 196 }); 197 chartX /= points.length; 198 chartY /= points.length; 199 // When shared, place the tooltip next to the mouse (#424) 200 if (this.shared && points.length > 1 && mouseEvent) { 201 if (inverted) { 202 chartX = mouseEvent.chartX; 203 } 204 else { 205 chartY = mouseEvent.chartY; 206 } 207 } 208 // Use the average position for multiple points 209 ret = [chartX - plotLeft, chartY - plotTop]; 210 } 211 return ret.map(Math.round); 212 } 213 /** 214 * Get the CSS class names for the tooltip's label. Styles the label 215 * by `colorIndex` or user-defined CSS. 216 * 217 * @function Highcharts.Tooltip#getClassName 218 * 219 * @return {string} 220 * The class names. 221 */ 222 getClassName(point, isSplit, isHeader) { 223 const options = this.options, series = point.series, seriesOptions = series.options; 224 return [ 225 options.className, 226 'highcharts-label', 227 isHeader && 'highcharts-tooltip-header', 228 isSplit ? 'highcharts-tooltip-box' : 'highcharts-tooltip', 229 !isHeader && 'highcharts-color-' + pick(point.colorIndex, series.colorIndex), 230 (seriesOptions && seriesOptions.className) 231 ].filter(isString).join(' '); 232 } 233 /** 234 * Creates the Tooltip label element if it does not exist, then returns it. 235 * 236 * @function Highcharts.Tooltip#getLabel 237 * 238 * @return {Highcharts.SVGElement} 239 * Tooltip label 240 */ 241 getLabel({ anchorX, anchorY } = { anchorX: 0, anchorY: 0 }) { 242 const tooltip = this, styledMode = this.chart.styledMode, options = this.options, doSplit = this.split && this.allowShared; 243 let container = this.container, renderer = this.chart.renderer; 244 // If changing from a split tooltip to a non-split tooltip, we must 245 // destroy it in order to get the SVG right. #13868. 246 if (this.label) { 247 const wasSplit = !this.label.hasClass('highcharts-label'); 248 if ((!doSplit && wasSplit) || (doSplit && !wasSplit)) { 249 this.destroy(); 250 } 251 } 252 if (!this.label) { 253 if (this.outside) { 254 const chart = this.chart, chartStyle = chart.options.chart.style, Renderer = RendererRegistry.getRendererType(); 255 /** 256 * Reference to the tooltip's container, when 257 * [Highcharts.Tooltip#outside] is set to true, otherwise 258 * it's undefined. 259 * 260 * @name Highcharts.Tooltip#container 261 * @type {Highcharts.HTMLDOMElement|undefined} 262 */ 263 this.container = container = H.doc.createElement('div'); 264 container.className = ('highcharts-tooltip-container ' + 265 (chart.renderTo.className.match(/(highcharts[a-zA-Z0-9-]+)\s?/gm) || [].join(' '))); 266 // We need to set pointerEvents = 'none' as otherwise it makes 267 // the area under the tooltip non-hoverable even after the 268 // tooltip disappears, #19035. 269 css(container, { 270 position: 'absolute', 271 top: '1px', 272 pointerEvents: 'none', 273 zIndex: Math.max(this.options.style.zIndex || 0, (chartStyle && chartStyle.zIndex || 0) + 3) 274 }); 275 /** 276 * Reference to the tooltip's renderer, when 277 * [Highcharts.Tooltip#outside] is set to true, otherwise 278 * it's undefined. 279 * 280 * @name Highcharts.Tooltip#renderer 281 * @type {Highcharts.SVGRenderer|undefined} 282 */ 283 this.renderer = renderer = new Renderer(container, 0, 0, chartStyle, void 0, void 0, renderer.styledMode); 284 } 285 // Create the label 286 if (doSplit) { 287 this.label = renderer.g('tooltip'); 288 } 289 else { 290 this.label = renderer 291 .label('', anchorX, anchorY, options.shape, void 0, void 0, options.useHTML, void 0, 'tooltip') 292 .attr({ 293 padding: options.padding, 294 r: options.borderRadius 295 }); 296 if (!styledMode) { 297 this.label 298 .attr({ 299 fill: options.backgroundColor, 300 'stroke-width': options.borderWidth || 0 301 }) 302 // #2301, #2657 303 .css(options.style) 304 .css({ 305 pointerEvents: (options.style.pointerEvents || 306 (this.shouldStickOnContact() ? 'auto' : 'none')) 307 }); 308 } 309 } 310 // Split tooltip use updateTooltipContainer to position the tooltip 311 // container. 312 if (tooltip.outside) { 313 const label = this.label; 314 [label.xSetter, label.ySetter].forEach((setter, i) => { 315 label[i ? 'ySetter' : 'xSetter'] = (value) => { 316 setter.call(label, tooltip.distance); 317 label[i ? 'y' : 'x'] = value; 318 if (container) { 319 container.style[i ? 'top' : 'left'] = `${value}px`; 320 } 321 }; 322 }); 323 } 324 this.label 325 .attr({ zIndex: 8 }) 326 .shadow(options.shadow) 327 .add(); 328 } 329 if (container && !container.parentElement) { 330 H.doc.body.appendChild(container); 331 } 332 return this.label; 333 } 334 /** 335 * Get the total area available area to place the tooltip 336 * 337 * @private 338 */ 339 getPlayingField() { 340 const { body, documentElement } = doc, { chart, distance, outside }
vendor: 4,653 bytes, lines 340-438
340 = this; 341 return { 342 width: outside ? 343 // Subtract distance to prevent scrollbars 344 Math.max(body.scrollWidth, documentElement.scrollWidth, body.offsetWidth, documentElement.offsetWidth, documentElement.clientWidth) - (2 * distance) - 2 : 345 chart.chartWidth, 346 height: outside ? 347 Math.max(body.scrollHeight, documentElement.scrollHeight, body.offsetHeight, documentElement.offsetHeight, documentElement.clientHeight) : 348 chart.chartHeight 349 }; 350 } 351 /** 352 * Place the tooltip in a chart without spilling over and not covering the 353 * point itself. 354 * 355 * @function Highcharts.Tooltip#getPosition 356 * 357 * @param {number} boxWidth 358 * Width of the tooltip box. 359 * 360 * @param {number} boxHeight 361 * Height of the tooltip box. 362 * 363 * @param {Highcharts.Point} point 364 * Tooltip related point. 365 * 366 * @return {Highcharts.PositionObject} 367 * Recommended position of the tooltip. 368 */ 369 getPosition(boxWidth, boxHeight, point) { 370 const { distance, chart, outside, pointer } = this, { inverted, plotLeft, plotTop, polar } = chart, { plotX = 0, plotY = 0 } = point, ret = {}, 371 // Don't use h if chart isn't inverted (#7242) ??? 372 h = (inverted && point.h) || 0, // #4117 ??? 373 { height: outerHeight, width: outerWidth } = this.getPlayingField(), chartPosition = pointer.getChartPosition(), scaleX = (val) => (val * chartPosition.scaleX), scaleY = (val) => (val * chartPosition.scaleY), 374 // Build parameter arrays for firstDimension()/secondDimension() 375 buildDimensionArray = (dim) => { 376 const isX = dim === 'x'; 377 return [ 378 dim, // Dimension - x or y 379 isX ? outerWidth : outerHeight, 380 isX ? boxWidth : boxHeight 381 ].concat(outside ? [ 382 // If we are using tooltip.outside, we need to scale the 383 // position to match scaling of the container in case there 384 // is a transform/zoom on the container. #11329 385 isX ? scaleX(boxWidth) : scaleY(boxHeight), 386 isX ? chartPosition.left - distance + 387 scaleX(plotX + plotLeft) : 388 chartPosition.top - distance + 389 scaleY(plotY + plotTop), 390 0, 391 isX ? outerWidth : outerHeight 392 ] : [ 393 // Not outside, no scaling is needed 394 isX ? boxWidth : boxHeight, 395 isX ? plotX + plotLeft : plotY + plotTop, 396 isX ? plotLeft : plotTop, 397 isX ? plotLeft + chart.plotWidth : 398 plotTop + chart.plotHeight 399 ]); 400 }; 401 let first = buildDimensionArray('y'), second = buildDimensionArray('x'), swapped; 402 // Handle negative points or reversed axis (#13780) 403 let flipped = !!point.negative; 404 if (!polar && 405 chart.hoverSeries?.yAxis?.reversed) { 406 flipped = !flipped; 407 } 408 // The far side is right or bottom 409 const preferFarSide = !this.followPointer && 410 pick(point.ttBelow, polar ? false : !inverted === flipped), // #4984 411 /* 412 * Handle the preferred dimension. When the preferred dimension is 413 * tooltip on top or bottom of the point, it will look for space 414 * there. 415 * 416 * @private 417 */ 418 firstDimension = function (dim, outerSize, innerSize, scaledInnerSize, // #11329 419 point, min, max) { 420 const scaledDist = outside ? 421 (dim === 'y' ? scaleY(distance) : scaleX(distance)) : 422 distance, scaleDiff = (innerSize - scaledInnerSize) / 2, roomLeft = scaledInnerSize < point - distance, roomRight = point + distance + scaledInnerSize < outerSize, alignedLeft = point - scaledDist - innerSize + scaleDiff, alignedRight = point + scaledDist - scaleDiff; 423 if (preferFarSide && roomRight) { 424 ret[dim] = alignedRight; 425 } 426 else if (!preferFarSide && roomLeft) { 427 ret[dim] = alignedLeft; 428 } 429 else if (roomLeft) { 430 ret[dim] = Math.min(max - scaledInnerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h); 431 } 432 else if (roomRight) { 433 ret[dim] = Math.max(min, alignedRight + h + innerSize > outerSize ? 434 alignedRight : 435 alignedRight + h); 436 } 437 else { 438 return false;
vendor: 5,244 bytes, lines 439-601
439 } 440 }, 441 /* 442 * Handle the secondary dimension. If the preferred dimension is 443 * tooltip on top or bottom of the point, the second dimension is to 444 * align the tooltip above the point, trying to align center but 445 * allowing left or right align within the chart box. 446 * 447 * @private 448 */ 449 secondDimension = function (dim, outerSize, innerSize, scaledInnerSize, // #11329 450 point) { 451 // Too close to the edge, return false and swap dimensions 452 if (point < distance || point > outerSize - distance) { 453 return false; 454 } 455 // Align left/top 456 if (point < innerSize / 2) { 457 ret[dim] = 1; 458 // Align right/bottom 459 } 460 else if (point > outerSize - scaledInnerSize / 2) { 461 ret[dim] = outerSize - scaledInnerSize - 2; 462 // Align center 463 } 464 else { 465 ret[dim] = point - innerSize / 2; 466 } 467 }, 468 /* 469 * Swap the dimensions 470 */ 471 swap = function (count) { 472 [first, second] = [second, first]; 473 swapped = count; 474 }, run = () => { 475 if (firstDimension.apply(0, first) !== false) { 476 if (secondDimension.apply(0, second) === false && 477 !swapped) { 478 swap(true); 479 run(); 480 } 481 } 482 else if (!swapped) { 483 swap(true); 484 run(); 485 } 486 else { 487 ret.x = ret.y = 0; 488 } 489 }; 490 // Under these conditions, prefer the tooltip on the side of the point 491 if ((inverted && !polar) || this.len > 1) { 492 swap(); 493 } 494 run(); 495 return ret; 496 } 497 /** 498 * Hides the tooltip with a fade out animation. 499 * 500 * @function Highcharts.Tooltip#hide 501 * 502 * @param {number} [delay] 503 * The fade out in milliseconds. If no value is provided the value 504 * of the tooltip.hideDelay option is used. A value of 0 disables 505 * the fade out animation. 506 */ 507 hide(delay) { 508 const tooltip = this; 509 // Disallow duplicate timers (#1728, #1766) 510 U.clearTimeout(this.hideTimer); 511 delay = pick(delay, this.options.hideDelay); 512 if (!this.isHidden) { 513 this.hideTimer = syncTimeout(function () { 514 const label = tooltip.getLabel(); 515 // If there is a delay, fade out with the default duration. If 516 // the hideDelay is 0, we assume no animation is wanted, so we 517 // pass 0 duration. #12994. 518 tooltip.getLabel().animate({ 519 opacity: 0 520 }, { 521 duration: delay ? 150 : delay, 522 complete: () => { 523 // #3088, assuming we're only using this for tooltips 524 label.hide(); 525 // Clear the container for outside tooltip (#18490) 526 if (tooltip.container) { 527 tooltip.container.remove(); 528 } 529 } 530 }); 531 tooltip.isHidden = true; 532 }, delay); 533 } 534 } 535 /** 536 * Initialize tooltip. 537 * 538 * @private 539 * @function Highcharts.Tooltip#init 540 * 541 * @param {Highcharts.Chart} chart 542 * The chart instance. 543 * 544 * @param {Highcharts.TooltipOptions} options 545 * Tooltip options. 546 */ 547 init(chart, options) { 548 /** 549 * Chart of the tooltip. 550 * 551 * @readonly 552 * @name Highcharts.Tooltip#chart 553 * @type {Highcharts.Chart} 554 */ 555 this.chart = chart; 556 /** 557 * Used tooltip options. 558 * 559 * @readonly 560 * @name Highcharts.Tooltip#options 561 * @type {Highcharts.TooltipOptions} 562 */ 563 this.options = options; 564 /** 565 * List of crosshairs. 566 * 567 * @private 568 * @readonly 569 * @name Highcharts.Tooltip#crosshairs 570 * @type {Array<null>} 571 */ 572 this.crosshairs = []; 573 /** 574 * Tooltips are initially hidden. 575 * 576 * @private 577 * @readonly 578 * @name Highcharts.Tooltip#isHidden 579 * @type {boolean} 580 */ 581 this.isHidden = true; 582 /** 583 * True, if the tooltip is split into one label per series, with the 584 * header close to the axis. 585 * 586 * @readonly 587 * @name Highcharts.Tooltip#split 588 * @type {boolean|undefined} 589 */ 590 this.split = options.split && !chart.inverted && !chart.polar; 591 /** 592 * When the tooltip is shared, the entire plot area will capture mouse 593 * movement or touch events. 594 * 595 * @readonly 596 * @name Highcharts.Tooltip#shared 597 * @type {boolean|undefined} 598 */ 599 this.shared = options.shared || this.split; 600 /** 601 * Whether to allow the tooltip to render outside the chart'
601s SVG 602 * element box. By default (false), the tooltip is rendered within the 603 * chart's SVG element, which results in the tooltip being aligned 604 * inside the chart area. 605 * 606 * @readonly 607 * @name Highcharts.Tooltip#outside 608 * @type {boolean} 609 * 610 * @todo 611 * Split tooltip does not support outside in the first iteration. Should 612 * not be too complicated to implement. 613 */ 614 this.outside = pick(options.outside, Boolean(chart.scrollablePixelsX || chart.scrollablePixelsY)); 615 } 616 shouldStickOnContact(pointerEvent) { 617 return !!(!this.followPointer && 618 this.options.stickOnContact && 619 (!pointerEvent || this.pointer.inClass(pointerEvent.target, 'highcharts-tooltip'))); 620 } 621 /** 622 * Moves the tooltip with a soft animation to a new position. 623 * 624 * @private 625 * @function Highcharts.Tooltip#move 626 * 627 * @param {number} x 628 * 629 * @param {number} y 630 * 631 * @param {number} anchorX 632 * 633 * @param {number} anchorY 634 */ 635 move(x, y, anchorX, anchorY) { 636 const tooltip = this, animation = animObject(!tooltip.isHidden && tooltip.options.animation), skipAnchor = tooltip.followPointer || (tooltip.len || 0) > 1, attr = { x, y }; 637 if (!skipAnchor) { 638 attr.anchorX = anchorX; 639 attr.anchorY = anchorY; 640 } 641 animation.step = () => tooltip.drawTracker(); 642 tooltip.getLabel().animate(attr, animation); 643 } 644 /** 645 * Refresh the tooltip's text and position. 646 * 647 * @function Highcharts.Tooltip#refresh 648 * 649 * @param {Highcharts.Point|Array<Highcharts.Point>} pointOrPoints 650 * Either a point or an array of points. 651 * 652 * @param {Highcharts.PointerEventObject} [mouseEvent] 653 * Mouse event, that is responsible for the refresh and should be 654 * used for the tooltip update. 655 */ 656 refresh(pointOrPoints, mouseEvent) { 657 const tooltip = this, { chart, options, pointer, shared } = this, points = splat(pointOrPoints), point = points[0], formatString = options.format, formatter = options.formatter || tooltip.defaultFormatter, styledMode = chart.styledMode; 658 let wasShared = tooltip.allowShared; 659 if (!options.enabled || !point.series) { // #16820 660 return; 661 } 662 U.clearTimeout(this.hideTimer); 663 // A switch saying if this specific tooltip configuration allows shared 664 // or split modes 665 tooltip.allowShared = !(!isArray(pointOrPoints) && 666 pointOrPoints.series && 667 pointOrPoints.series.noSharedTooltip); 668 wasShared = wasShared && !tooltip.allowShared; 669 // Get the reference point coordinates (pie charts use tooltipPos) 670 tooltip.followPointer = (!tooltip.split && point.series.tooltipOptions.followPointer); 671 const anchor = tooltip.getAnchor(pointOrPoints, mouseEvent), x = anchor[0], y = anchor[1]; 672 // Shared tooltip, array is sent over 673 if (shared && tooltip.allowShared) { 674 pointer.applyInactiveState(points); 675 // Now set hover state for the chosen ones: 676 points.forEach((item) => item.setState('hover')); 677 point.points = points; 678 } 679 this.len = points.length; // #6128 680 const text = isString(formatString) ? 681 format(formatString, point, chart) : 682 formatter.call(point, tooltip); 683 // Reset the preliminary circular references 684 point.points = void 0; 685 // Register the current series 686 const currentSeries = point.series; 687 this.distance = pick(currentSeries.tooltipOptions.distance, 16); 688 // Update the inner HTML 689 if (text === false) { 690 this.hide(); 691 } 692 else { 693 // Update text 694 if (tooltip.split && tooltip.allowShared) { // #13868 695 this.renderSplit(text, points); 696 } 697 else { 698 let checkX = x; 699 let checkY = y; 700 if (mouseEvent && pointer.isDirectTouch) { 701 checkX = mouseEvent.chartX - chart.plotLeft; 702 checkY = mouseEvent.chartY - chart.plotTop; 703 } 704 // #11493, #13095 705 if (chart.polar || 706 currentSeries.options.clip === false || 707 points.some((p) => // #16004 708 pointer.isDirectTouch || // ##17929 709 p.series.shouldShowTooltip(checkX, checkY))) { 710 const label = tooltip.getLabel(wasShared && tooltip.tt || {}); 711 // Prevent the tooltip from flowing over the chart box 712 // (#6659) 713 if (!options.style.width || styledMode) { 714 label.css({ 715 width: (this.outside ? 716 this.getPlayingField() : 717 chart.spacingBox).width + 'px' 718 }); 719 } 720 label.attr({ 721 // Add class before the label BBox calculation (#21035) 722 'class': tooltip.getClassName(point), 723 text: text && text.join ? 724 text.join('') : 725 text 726 }); 727 // When the length of the label has increased, immediately 728 // update the x position to prevent tooltip from flowing 729 // outside the viewport during animation (#21371) 730 if (this.outside) { 731 label.attr({ 732 x: clamp(label.x || 0, 0, this.getPlayingField().width - 733 (label.width || 0) - 734 1) 735 }); 736 } 737 if (!styledMode) { 738 label.attr({ 739 stroke: (options.borderColor || 740 point.color || 741 currentSeries.color || 742 "#666666" /* Palette.neutralColor60 */) 743 }); 744 } 745 tooltip.updatePosition({ 746 plotX: x, 747 plotY: y, 748 negative: point.negative, 749 ttBelow: point.ttBelow, 750 h: anchor[2] || 0 751 }); 752 } 753 else { 754 tooltip.hide(); 755 return; 756 } 757 } 758 // Show it 759 if (tooltip.isHidden && tooltip.label) { 760 tooltip.label.attr({ 761 opacity: 1 762 }).show(); 763 } 764 tooltip.isHidden = false;
vendor: 1,523 bytes, lines 765-797
765 } 766 fireEvent(this, 'refresh'); 767 } 768 /** 769 * Render the split tooltip. Loops over each point's text and adds 770 * a label next to the point, then uses the distribute function to 771 * find best non-overlapping positions. 772 * 773 * @private 774 * @function Highcharts.Tooltip#renderSplit 775 * 776 * @param {string|Array<(boolean|string)>} labels 777 * 778 * @param {Array<Highcharts.Point>} points 779 */ 780 renderSplit(labels, points) { 781 const tooltip = this; 782 const { chart, chart: { chartWidth, chartHeight, plotHeight, plotLeft, plotTop, scrollablePixelsY = 0, scrollablePixelsX, styledMode }, distance, options, options: { positioner }, pointer } = tooltip; 783 const { scrollLeft = 0, scrollTop = 0 } = chart.scrollablePlotArea?.scrollingContainer || {}; 784 // The area which the tooltip should be limited to. Limit to scrollable 785 // plot area if enabled, otherwise limit to the chart container. If 786 // outside is true it should be the whole viewport 787 const bounds = (tooltip.outside && 788 typeof scrollablePixelsX !== 'number') ? 789 doc.documentElement.getBoundingClientRect() : { 790 left: scrollLeft, 791 right: scrollLeft + chartWidth, 792 top: scrollTop, 793 bottom: scrollTop + chartHeight 794 }; 795 const tooltipLabel = tooltip.getLabel(); 796 const ren = this.renderer || chart.renderer; 797 const headerTop = Boolean(chart.xAxis[0] && chart.xAxis[0].opposite);
798 const { left: chartLeft, top: chartTop } = pointer.getChartPosition(); 799 let distributionBoxTop = plotTop + scrollTop; 800 let headerHeight = 0; 801 let adjustedPlotHeight = plotHeight - scrollablePixelsY; 802 /** 803 * Calculates the anchor position for the partial tooltip 804 * 805 * @private 806 * @param {Highcharts.Point} point The point related to the tooltip 807 * @return {Object} Returns an object with anchorX and anchorY 808 */ 809 function getAnchor(point) { 810 const { isHeader, plotX = 0, plotY = 0, series } = point; 811 let anchorX; 812 let anchorY; 813 if (isHeader) { 814 // Set anchorX to plotX 815 anchorX = Math.max(plotLeft + plotX, plotLeft); 816 // Set anchorY to center of visible plot area. 817 anchorY = plotTop + plotHeight / 2; 818 } 819 else { 820 const { xAxis, yAxis } = series; 821 // Set anchorX to plotX. Limit to within xAxis. 822 anchorX = xAxis.pos + clamp(plotX, -distance, xAxis.len + distance); 823 // Set anchorY, limit to the scrollable plot area 824 if (series.shouldShowTooltip(0, yAxis.pos - plotTop + plotY, { 825 ignoreX: true 826 })) { 827 anchorY = yAxis.pos + plotY; 828 } 829 } 830 // Limit values to plot area 831 anchorX = clamp(anchorX, bounds.left - distance, bounds.right + distance); 832 return { anchorX, anchorY }; 833 } 834 /** 835 * Calculates the position of the partial tooltip 836 * 837 * @private 838 * @param {number} anchorX 839 * The partial tooltip anchor x position 840 * 841 * @param {number} anchorY 842 * The partial tooltip anchor y position 843 * 844 * @param {boolean|undefined} isHeader 845 * Whether the partial tooltip is a header 846 * 847 * @param {number} boxWidth 848 * Width of the partial tooltip 849 * 850 * @return {Highcharts.PositionObject} 851 * Returns the partial tooltip x and y position 852 */ 853 function defaultPositioner(anchorX, anchorY, isHeader, boxWidth, alignedLeft = true) { 854 let y; 855 let x; 856 if (isHeader) { 857 y = headerTop ? 0 : adjustedPlotHeight; 858 x = clamp(anchorX - (boxWidth / 2), bounds.left, bounds.right - boxWidth - (tooltip.outside ? chartLeft : 0)); 859 } 860 else { 861 y = anchorY - distributionBoxTop; 862 x = alignedLeft ? 863 anchorX - boxWidth - distance : 864 anchorX + distance; 865 x = clamp(x, alignedLeft ? x : bounds.left, bounds.right); 866 } 867 // NOTE: y is relative to distributionBoxTop 868 return { x, y }; 869 } 870 /** 871 * Updates the attributes and styling of the partial tooltip. Creates a 872 * new partial tooltip if it does not exists. 873 * 874 * @private 875 * @param {Highcharts.SVGElement|undefined} partialTooltip 876 * The partial tooltip to update 877 * @param {Highcharts.Point} point 878 * The point related to the partial tooltip 879 * @param {boolean|string} str The text for the partial tooltip 880 * @return {Highcharts.SVGElement} Returns the updated partial tooltip 881 */ 882 function updatePartialTooltip(partialTooltip, point, str) { 883 let tt = partialTooltip; 884 const { isHeader, series } = point; 885 if (!tt) { 886 const attribs = { 887 padding: options.padding, 888 r: options.borderRadius 889 }; 890 if (!styledMode) { 891 attribs.fill = options.backgroundColor; 892 attribs['stroke-width'] = options.borderWidth ?? 1; 893 } 894 tt = ren 895 .label('', 0, 0, (options[isHeader ? 'headerShape' : 'shape']), void 0, void 0, options.useHTML) 896 .addClass(tooltip.getClassName(point, true, isHeader)) 897 .attr(attribs) 898 .add(tooltipLabel); 899 } 900 tt.isActive = true; 901 tt.attr({ 902 text: str 903 }); 904 if (!styledMode) { 905 tt.css(options.style) 906 .attr({ 907 stroke: (options.borderColor || 908 point.color || 909 series.color || 910 "#333333" /* Palette.neutralColor80 */) 911 }); 912 } 913 return tt; 914 } 915 // Graceful degradation for legacy formatters 916 if (isString(labels)) {
917 labels = [false, labels]; 918 } 919 // Create the individual labels for header and points, ignore footer 920 let boxes = labels.slice(0, points.length + 1).reduce(function (boxes, str, i) { 921 if (str !== false && str !== '') { 922 const point = (points[i - 1] || 923 { 924 // Item 0 is the header. Instead of this, we could also 925 // use the crosshair label 926 isHeader: true, 927 plotX: points[0].plotX, 928 plotY: plotHeight, 929 series: {} 930 }); 931 const isHeader = point.isHeader; 932 // Store the tooltip label reference on the series 933 const owner = isHeader ? tooltip : point.series; 934 const tt = owner.tt = updatePartialTooltip(owner.tt, point, str.toString()); 935 // Get X position now, so we can move all to the other side in 936 // case of overflow 937 const bBox = tt.getBBox(); 938 const boxWidth = bBox.width + tt.strokeWidth(); 939 if (isHeader) { 940 headerHeight = bBox.height; 941 adjustedPlotHeight += headerHeight; 942 if (headerTop) { 943 distributionBoxTop -= headerHeight; 944 } 945 } 946 const { anchorX, anchorY } = getAnchor(point); 947 if (typeof anchorY === 'number') { 948 const size = bBox.height + 1; 949 const boxPosition = (positioner ? 950 positioner.call(tooltip, boxWidth, size, point) : 951 defaultPositioner(anchorX, anchorY, isHeader, boxWidth)); 952 boxes.push({ 953 // 0-align to the top, 1-align to the bottom 954 align: positioner ? 0 : void 0, 955 anchorX, 956 anchorY, 957 boxWidth, 958 point, 959 rank: pick(boxPosition.rank, isHeader ? 1 : 0), 960 size, 961 target: boxPosition.y, 962 tt, 963 x: boxPosition.x 964 }); 965 } 966 else { 967 // Hide tooltips which anchorY is outside the visible plot 968 // area 969 tt.isActive = false; 970 } 971 } 972 return boxes; 973 }, []); 974 // Realign the tooltips towards the right if there is not enough space 975 // to the left and there is space to the right 976 if (!positioner && boxes.some((box) => { 977 // Always realign if the beginning of a label is outside bounds 978 const { outside } = tooltip; 979 const boxStart = (outside ? chartLeft : 0) + box.anchorX; 980 if (boxStart < bounds.left && 981 boxStart + box.boxWidth < bounds.right) { 982 return true; 983 } 984 // Otherwise, check if there is more space available to the right 985 return boxStart < (chartLeft - bounds.left) + box.boxWidth && 986 bounds.right - boxStart > boxStart; 987 })) { 988 boxes = boxes.map((box) => { 989 const { x, y } = defaultPositioner(box.anchorX, box.anchorY, box.point.isHeader, box.boxWidth, false); 990 return extend(box, { 991 target: y, 992 x 993 }); 994 }); 995 } 996 // Clean previous run (for missing points) 997 tooltip.cleanSplit(); 998 // Distribute and put in place 999 distribute(boxes, adjustedPlotHeight); 1000 const boxExtremes = { 1001 left: chartLeft, 1002 right: chartLeft 1003 }; 1004 // Get the extremes from series tooltips 1005 boxes.forEach(function (box) { 1006 const { x, boxWidth, isHeader } = box; 1007 if (!isHeader) { 1008 if (tooltip.outside && chartLeft + x < boxExtremes.left) { 1009 boxExtremes.left = chartLeft + x; 1010 } 1011 if (!isHeader && 1012 tooltip.outside && 1013 boxExtremes.left + boxWidth > boxExtremes.right) { 1014 boxExtremes.right = chartLeft + x; 1015 } 1016 } 1017 });
1018 boxes.forEach(function (box) { 1019 const { x, anchorX, anchorY, pos, point: { isHeader } } = box; 1020 const attributes = { 1021 visibility: typeof pos === 'undefined' ? 'hidden' : 'inherit', 1022 x, 1023 /* NOTE: y should equal pos to be consistent with !split 1024 * tooltip, but is currently relative to plotTop. Is left as is 1025 * to avoid breaking change. Remove distributionBoxTop to make 1026 * it consistent. 1027 */ 1028 y: (pos || 0) + distributionBoxTop, 1029 anchorX, 1030 anchorY 1031 }; 1032 // Handle left-aligned tooltips overflowing the chart area 1033 if (tooltip.outside && x < anchorX) { 1034 const offset = chartLeft - boxExtremes.left; 1035 // Skip this if there is no overflow 1036 if (offset > 0) { 1037 if (!isHeader) { 1038 attributes.x = x + offset; 1039 attributes.anchorX = anchorX + offset; 1040 } 1041 if (isHeader) { 1042 attributes.x = (boxExtremes.right - boxExtremes.left) / 2; 1043 attributes.anchorX = anchorX + offset; 1044 } 1045 } 1046 } 1047 // Put the label in place 1048 box.tt.attr(attributes); 1049 }); 1050 /* If we have a separate tooltip container, then update the necessary 1051 * container properties. 1052 * Test that tooltip has its own container and renderer before executing 1053 * the operation. 1054 */ 1055 const { container, outside, renderer } = tooltip; 1056 if (outside && container && renderer) { 1057 // Set container size to fit the bounds 1058 const { width, height, x, y } = tooltipLabel.getBBox(); 1059 renderer.setSize(width + x, height + y, false); 1060 // Position the tooltip container to the chart container 1061 container.style.left = boxExtremes.left + 'px'; 1062 container.style.top = chartTop + 'px'; 1063 } 1064 // Workaround for #18927, artefacts left by the shadows of split 1065 // tooltips in Safari v16 (2023). Check again with later versions if we 1066 // can remove this. 1067 if (isSafari) { 1068 tooltipLabel.attr({ 1069 // Force a redraw of the whole group by chaining the opacity 1070 // slightly 1071 opacity: tooltipLabel.opacity === 1 ? 0.999 : 1 1072 }); 1073 } 1074 } 1075 /** 1076 * If the `stickOnContact` option is active, this will add a tracker shape. 1077 * 1078 * @private 1079 * @function Highcharts.Tooltip#drawTracker 1080 */ 1081 drawTracker() { 1082 const tooltip = this; 1083 if (!this.shouldStickOnContact()) { 1084 if (tooltip.tracker) { 1085 tooltip.tracker = tooltip.tracker.destroy(); 1086 } 1087 return; 1088 } 1089 const chart = tooltip.chart; 1090 const label = tooltip.label; 1091 const points = tooltip.shared ? chart.hoverPoints : chart.hoverPoint; 1092 if (!label || !points) { 1093 return; 1094 } 1095 const box = { 1096 x: 0, 1097 y: 0, 1098 width: 0, 1099 height: 0 1100 }; 1101 // Combine anchor and tooltip 1102 const anchorPos = this.getAnchor(points); 1103 const labelBBox = label.getBBox(); 1104 anchorPos[0] += chart.plotLeft - (label.translateX || 0); 1105 anchorPos[1] += chart.plotTop - (label.translateY || 0); 1106 // When the mouse pointer is between the anchor point and the label, 1107 // the label should stick. 1108 box.x = Math.min(0, anchorPos[0]); 1109 box.y = Math.min(0, anchorPos[1]); 1110 box.width = (anchorPos[0] < 0 ? 1111 Math.max(Math.abs(anchorPos[0]), labelBBox.width - anchorPos[0]) : 1112 Math.max(Math.abs(anchorPos[0]), labelBBox.width)); 1113 box.height = (anchorPos[1] < 0 ? 1114 Math.max(Math.abs(anchorPos[1]), labelBBox.height - Math.abs(anchorPos[1])) : 1115 Math.max(Math.abs(anchorPos[1]), labelBBox.height)); 1116 if (tooltip.tracker) { 1117 tooltip.tracker.attr(box); 1118 } 1119 else { 1120 tooltip.tracker = label.renderer 1121 .rect(box) 1122 .addClass('highcharts-tracker') 1123 .add(label); 1124 if (!chart.styledMode) { 1125 tooltip.tracker.attr({ 1126 fill: 'rgba(0,0,0,0)' 1127 }); 1128 } 1129 } 1130 } 1131 /** 1132 * @private 1133 */ 1134 styledModeFormat(formatString) { 1135 return formatString 1136 .replace('style="font-size: 0.8em"', 'class="highcharts-header"') 1137 .replace(/style="color:{(point|series)\.color}"/g, 'class="highcharts-color-{$1.colorIndex} ' + 1138 '{series.options.className} ' + 1139 '{point.options.className}"'); 1140 } 1141 /** 1142 * Format the footer/header of the tooltip 1143 * #3397: abstraction to enable formatting of footer and header 1144 * 1145 * @private 1146 * @function Highcharts.Tooltip#headerFooterFormatter 1147 */ 1148 headerFooterFormatter(point, isFooter) { 1149 const series = point.series, tooltipOptions = series.tooltipOptions, xAxis = series.xAxis, dateTime = xAxis && xAxis.dateTime, e = { 1150 isFooter, 1151 point 1152 }; 1153 let xDateFormat = tooltipOptions.xDateFormat || '', formatString = tooltipOptions[isFooter ? 'footerFormat' : 'headerFormat']; 1154 fireEvent(this, 'headerFormatter', e, function (e) { 1155 // Guess the best date format based on the closest point distance 1156 // (#568, #3418) 1157 if (dateTime && !xDateFormat && isNumber(point.key)) { 1158 xDateFormat = dateTime.getXDateFormat(point.key, tooltipOptions.dateTimeLabelFormats); 1159 } 1160 // Insert the footer date format if any 1161 if (dateTime && xDateFormat) { 1162 if (isObject(xDateFormat)) { 1163 const format = xDateFormat; 1164 dateFormats[0] = (timestamp) => series.chart.time.dateFormat(format, timestamp); 1165 xDateFormat = '%0'; 1166 }
1167 (point.tooltipDateKeys || ['key']).forEach((key) => { 1168 formatString = formatString.replace(new RegExp('point\\.' + key + '([ \\)}])', ''), `(point.${key}:${xDateFormat})$1`); 1169 }); 1170 } 1171 // Replace default header style with class name 1172 if (series.chart.styledMode) { 1173 formatString = this.styledModeFormat(formatString); 1174 } 1175 e.text = format(formatString, point, this.chart); 1176 }); 1177 return e.text || ''; 1178 } 1179 /** 1180 * Updates the tooltip with the provided tooltip options. 1181 * 1182 * @function Highcharts.Tooltip#update 1183 * 1184 * @param {Highcharts.TooltipOptions} options 1185 * The tooltip options to update. 1186 */ 1187 update(options) { 1188 this.destroy(); 1189 this.init(this.chart, merge(true, this.options, options)); 1190 } 1191 /** 1192 * Find the new position and perform the move 1193 * 1194 * @private 1195 * @function Highcharts.Tooltip#updatePosition 1196 * 1197 * @param {Highcharts.Point} point 1198 */ 1199 updatePosition(point) { 1200 const { chart, container, distance, options, pointer, renderer } = this, { height = 0, width = 0 } = this.getLabel(), 1201 // Needed for outside: true (#11688) 1202 { left, top, scaleX, scaleY } = pointer.getChartPosition(), pos = (options.positioner || this.getPosition).call(this, width, height, point), doc = H.doc; 1203 let anchorX = (point.plotX || 0) + chart.plotLeft, anchorY = (point.plotY || 0) + chart.plotTop, pad; 1204 // Set the renderer size dynamically to prevent document size to change. 1205 // Renderer only exists when tooltip is outside. 1206 if (renderer && container) { 1207 // Corrects positions, occurs with tooltip positioner (#16944) 1208 if (options.positioner) { 1209 pos.x += left - distance; 1210 pos.y += top - distance; 1211 } 1212 // Pad it by the border width and distance. Add 2 to make room for 1213 // the default shadow (#19314). 1214 pad = (options.borderWidth || 0) + 2 * distance + 2; 1215 renderer.setSize( 1216 // Clamp width to keep tooltip in viewport (#21698) 1217 // and subtract one since tooltip container has 'left: 1px;' 1218 clamp(width + pad, 0, doc.documentElement.clientWidth) - 1, height + pad, false); 1219 // Anchor and tooltip container need scaling if chart container has 1220 // scale transform/css zoom. #11329. 1221 if (scaleX !== 1 || scaleY !== 1) { 1222 css(container, { 1223 transform: `scale(${scaleX}, ${scaleY})` 1224 }); 1225 anchorX *= scaleX; 1226 anchorY *= scaleY; 1227 } 1228 anchorX += left - pos.x; 1229 anchorY += top - pos.y; 1230 } 1231 // Do the move 1232 this.move(Math.round(pos.x), Math.round(pos.y || 0), // Can be undefined (#3977) 1233 anchorX, anchorY); 1234 } 1235} 1236/* * 1237 * 1238 * Class namespace 1239 * 1240 * */ 1241(function (Tooltip) { 1242 /* * 1243 * 1244 * Declarations 1245 * 1246 * */ 1247 /* * 1248 * 1249 * Functions 1250 * 1251 * */ 1252 /** 1253 * @private 1254 */ 1255 function compose(PointerClass) { 1256 if (pushUnique(composed, 'Core.Tooltip')) { 1257 addEvent(PointerClass, 'afterInit', function () { 1258 const chart = this.chart; 1259 if (chart.options.tooltip) { 1260 /** 1261 * Tooltip object for points of series. 1262 * 1263 * @name Highcharts.Chart#tooltip 1264 * @type {Highcharts.Tooltip} 1265 */ 1266 chart.tooltip = new Tooltip(chart, chart.options.tooltip, this); 1267 } 1268 }); 1269 } 1270 } 1271 Tooltip.compose = compose; 1272})(Tooltip || (Tooltip = {})); 1273/* * 1274 * 1275 * Default export 1276 * 1277 * */ 1278export default Tooltip; 1279/* * 1280 * 1281 * API Declarations 1282 * 1283 * */ 1284/** 1285 * Callback function to format the text of the tooltip from scratch. 1286 * 1287 * In case of single or shared tooltips, a string should be returned. In case 1288 * of split tooltips, it should return an array where the first item is the 1289 * header, and subsequent items are mapped to the points. Return `false` to 1290 * disable tooltip for a specific point on series. 1291 * 1292 * @callback Highcharts.TooltipFormatterCallbackFunction 1293 * 1294 * @param {Highcharts.Point} this 1295 * The formatter's context is the hovered `Point` instance. In case of shared or 1296 * split tooltips, all points are available in `this.points`. 1297 * 1298 * @param {Highcharts.Tooltip} tooltip 1299 * The tooltip instance 1300 * 1301 * @return {false|string|Array<(string|null|undefined)>|null|undefined} 1302 * Formatted text or false 1303 */ 1304/** 1305 * A callback function to place the tooltip in a specific position. 1306 * 1307 * @callback Highcharts.TooltipPositionerCallbackFunction 1308 * 1309 * @param {Highcharts.Tooltip} this 1310 * Tooltip context of the callback. 1311 * 1312 * @param {number} labelWidth 1313 * Width of the tooltip. 1314 * 1315 * @param {number} labelHeight 1316 * Height of the tooltip. 1317 * 1318 * @param {Highcharts.TooltipPositionerPointObject} point 1319 * Point information for positioning a tooltip. 1320 * 1321 * @return {Highcharts.PositionObject} 1322 * New position for the tooltip. 1323 */ 1324/** 1325 * Point information for positioning a tooltip. 1326 * 1327 * @interface Highcharts.TooltipPositionerPointObject 1328 * @extends Highcharts.Point 1329 */ /** 1330* If `tooltip.split` option is enabled and positioner is called for each of the 1331* boxes separately, this property indicates the call on the xAxis header, which 1332* is not a point itself. 1333* @name Highcharts.TooltipPositionerPointObject#isHeader 1334* @type {boolean} 1335*/ /** 1336* The reference point relative to the plot area. Add chart.plotLeft to get the 1337* full coordinates. 1338* @name Highcharts.TooltipPositionerPointObject#plotX 1339* @type {number} 1340*/ /** 1341* The reference point relative to the plot area. Add chart.plotTop to get the 1342* full coordinates. 1343* @name Highcharts.TooltipPositionerPointObject#plotY 1344* @type {number} 1345*/ 1346/** 1347 * @typedef {"callout"|"circle"|"rect"} Highcharts.TooltipShapeValue 1348 */ 1349''; // Keeps doclets above in JS file
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.