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, setAnimation } = A; 13import F from '../Foundation.js'; 14const { registerEventOptions } = F; 15import H from '../Globals.js'; 16const { composed, marginNames } = H; 17import Series from '../Series/Series.js'; 18import Point from '../Series/Point.js'; 19import R from '../Renderer/RendererUtilities.js'; 20const { distribute } = R; 21import T from '../Templating.js'; 22const { format } = T; 23import U from '../Utilities.js'; 24const { addEvent, createElement, css, defined, discardElement, find, fireEvent, isNumber, merge, pick, pushUnique, relativeLength, stableSort, syncTimeout } = U; 25/* * 26 * 27 * Class 28 * 29 * */ 30/** 31 * The overview of the chart's series. The legend object is instantiated 32 * internally in the chart constructor, and is available from the `chart.legend` 33 * property. Each chart has only one legend. 34 * 35 * @class 36 * @name Highcharts.Legend 37 * 38 * @param {Highcharts.Chart} chart 39 * The chart instance. 40 * 41 * @param {Highcharts.LegendOptions} options 42 * Legend options. 43 */ 44class Legend { 45 /* * 46 * 47 * Functions 48 * 49 * */ 50 /** 51 * Initialize the legend. 52 * 53 * @private 54 * @function Highcharts.Legend#init 55 * 56 * @param {Highcharts.Chart} chart 57 * The chart instance. 58 * 59 * @param {Highcharts.LegendOptions} options 60 * Legend options. 61 */ 62 constructor(chart, options) { 63 /* * 64 * 65 * Properties 66 * 67 * */ 68 this.allItems = []; 69 this.initialItemY = 0; 70 this.itemHeight = 0; 71 this.itemMarginBottom = 0; 72 this.itemMarginTop = 0; 73 this.itemX = 0; 74 this.itemY = 0; 75 this.lastItemY = 0; 76 this.lastLineHeight = 0; 77 this.legendHeight = 0; 78 this.legendWidth = 0; 79 this.maxItemWidth = 0; 80 this.maxLegendWidth = 0; 81 this.offsetWidth = 0; 82 this.padding = 0; 83 this.pages = []; 84 this.symbolHeight = 0; 85 this.symbolWidth = 0; 86 this.titleHeight = 0; 87 this.totalItemWidth = 0; 88 this.widthOption = 0; 89 /** 90 * Chart of this legend. 91 * 92 * @readonly 93 * @name Highcharts.Legend#chart 94 * @type {Highcharts.Chart} 95 */ 96 this.chart = chart; 97 this.setOptions(options); 98 if (options.enabled) { 99 // Render it 100 this.render(); 101 registerEventOptions(this, options); 102 // Move checkboxes 103 addEvent(this.chart, 'endResize', function () { 104 this.legend.positionCheckboxes(); 105 }); 106 } 107 // On Legend.init and Legend.update, make sure that proximate layout 108 // events are either added or removed (#18362). 109 addEvent(this.chart, 'render', () => { 110 if (this.options.enabled && this.proximate) { 111 this.proximatePositions(); 112 this.positionItems(); 113 } 114 }); 115 } 116 /** 117 * @private 118 * @function Highcharts.Legend#setOptions 119 * @param {Highcharts.LegendOptions} options 120 */ 121 setOptions(options) { 122 const padding = pick(options.padding, 8); 123 /** 124 * Legend options. 125 * 126 * @readonly 127 * @name Highcharts.Legend#options 128 * @type {Highcharts.LegendOptions} 129 */ 130 this.options = options; 131 if (!this.chart.styledMode) { 132 this.itemStyle = options.itemStyle; 133 this.itemHiddenStyle = merge(this.itemStyle, options.itemHiddenStyle); 134 } 135 this.itemMarginTop = options.itemMarginTop; 136 this.itemMarginBottom = options.itemMarginBottom; 137 this.padding = padding; 138 this.initialItemY = padding - 5; // 5 is pixels above the text 139 this.symbolWidth = pick(options.symbolWidth, 16); 140 this.pages = []; 141 this.proximate = options.layout === 'proximate' && !this.chart.inverted; 142 // #12705: baseline has to be reset on every update 143 this.baseline = void 0; 144 } 145 /** 146 * Update the legend with new options. Equivalent to running `chart.update` 147 * with a legend configuration option. 148 * 149 * @sample highcharts/legend/legend-update/ 150 * Legend update 151 * 152 * @function Highcharts.Legend#update 153 * 154 * @param {Highcharts.LegendOptions} options 155 * Legend options. 156 * 157 * @param {boolean} [redraw=true] 158 * Whether to redraw the chart after the axis is altered. If doing more 159 * operations on the chart, it is a good idea to set redraw to fal
159se and 160 * call {@link Chart#redraw} after. Whether to redraw the chart. 161 * 162 * @emits Highcharts.Legends#event:afterUpdate 163 */ 164 update(options, redraw) { 165 const chart = this.chart; 166 this.setOptions(merge(true, this.options, options)); 167 if ('events' in this.options) { 168 // Legend event handlers 169 registerEventOptions(this, this.options); 170 } 171 this.destroy(); 172 chart.isDirtyLegend = chart.isDirtyBox = true; 173 if (pick(redraw, true)) { 174 chart.redraw(); 175 } 176 fireEvent(this, 'afterUpdate', { redraw }); 177 } 178 /** 179 * Set the colors for the legend item. 180 * 181 * @private 182 * @function Highcharts.Legend#colorizeItem 183 * @param {Highcharts.BubbleLegendItem|Highcharts.Point|Highcharts.Series} item 184 * A Series or Point instance 185 * @param {boolean} [visible=false] 186 * Dimmed or colored 187 * 188 * @todo 189 * Make events official: Fires the event `afterColorizeItem`. 190 */ 191 colorizeItem(item, visible) { 192 const originalColor = item.color, { area, group, label, line, symbol } = item.legendItem || {}; 193 if (item instanceof Series || item instanceof Point) { 194 item.color = item.options?.legendSymbolColor || originalColor; 195 } 196 group?.[visible ? 'removeClass' : 'addClass']('highcharts-legend-item-hidden'); 197 if (!this.chart.styledMode) { 198 const { itemHiddenStyle = {} } = this, hiddenColor = itemHiddenStyle.color, { fillColor, fillOpacity, lineColor, marker } = item.options, colorizeHidden = (attr) => { 199 if (!visible) { 200 if (attr.fill) { 201 attr.fill = hiddenColor; 202 } 203 if (attr.stroke) { 204 attr.stroke = hiddenColor; 205 } 206 } 207 return attr; 208 }; 209 label?.css(merge(visible ? this.itemStyle : itemHiddenStyle)); 210 line?.attr(colorizeHidden({ stroke: lineColor || item.color })); 211 if (symbol) { 212 // Apply marker options 213 symbol.attr(colorizeHidden(marker && symbol.isMarker ? // #585 214 item.pointAttribs() : 215 { fill: item.color })); 216 } 217 area?.attr(colorizeHidden({ 218 fill: fillColor || item.color, 219 'fill-opacity': fillColor ? 1 : (fillOpacity ?? 0.75) 220 })); 221 } 222 item.color = originalColor; 223 fireEvent(this, 'afterColorizeItem', { item, visible }); 224 } 225 /** 226 * @private 227 * @function Highcharts.Legend#positionItems 228 */ 229 positionItems() { 230 // Now that the legend width and height are established, put the items 231 // in the final position 232 this.allItems.forEach(this.positionItem, this); 233 if (!this.chart.isResizing) { 234 this.positionCheckboxes(); 235 } 236 } 237 /** 238 * Position the legend item. 239 * 240 * @private 241 * @function Highcharts.Legend#positionItem 242 * @param {Highcharts.BubbleLegendItem|Highcharts.Point|Highcharts.Series} item 243 * The item to position 244 */ 245 positionItem(item) { 246 const legend = this, { group, x = 0, y = 0 } = item.legendItem || {}, options = legend.options, symbolPadding = options.symbolPadding, ltr = !options.rtl, checkbox = item.checkbox; 247 if (group && group.element) { 248 const attribs = { 249 translateX: ltr ? 250 x : 251 legend.legendWidth - x - 2 * symbolPadding - 4, 252 translateY: y 253 }; 254 const complete = () => { 255 fireEvent(this, 'afterPositionItem', { item }); 256 }; 257 group[defined(group.translateY) ? 'animate' : 'attr'](attribs, void 0, complete); 258 } 259 if (checkbox) { 260 checkbox.x = x; 261 checkbox.y = y; 262 } 263 } 264 /** 265 * Destroy a single legend item, used internally on removing series items. 266 * 267 * @private 268 * @function Highcharts.Legend#destroyItem 269 * @param {Highcharts.BubbleLegendItem|Highcharts.Point|Highcharts.Series} item 270 * The item to remove 271 */ 272 destroyItem(item) { 273 const checkbox = item.checkbox, legendItem = item.legendItem || {}; 274 // Destroy SVG elements 275 for (const key of ['group', 'label', 'line', 'symbol']) { 276 if (legendItem[key]) { 277 legendItem[key] = legendItem[key].destroy(); 278 } 279 } 280 if (checkbox) { 281 discardElement(checkbox); 282 } 283 item.legendItem = void 0; 284 } 285 /** 286 * Destroy the legend. Used internally. To reflow objects, `chart.redraw` 287 * must be called after destruction. 288 * 289 * @private 290 * @function Highcharts.Legend#destroy 291 */ 292 destroy() { 293 const legend = this; 294 // Destroy items 295 for (const item of this.getAllItems()) { 296 this.destroyItem(item); 297 } 298 // Destroy legend elements 299 for (const key of [ 300 'clipRect', 301 'up', 302 'down', 303 'pager', 304 'nav', 305 'box', 306 'title', 307 'group' 308 ]) { 309 if (legend[key]) { 310 legend[key] = legend[key].destroy(); 311 } 312 } 313 this.display = null; // Reset in .render on update. 314 } 315 /** 316 * Position the checkboxes after the width is determined. 317 * 318 * @private 319 * @function Highcharts.Legend#positionCheckboxes 320 */ 321 positionCheckboxes() { 322 const alignAttr = this.group && this.group.alignAttr, clipHeight = this.clipHeight || this.legendHeight, titleHeight = this.titleHeight; 323 let translateY; 324 if (alignAttr) { 325 translateY = alignAttr.translateY;
vendor: 4,451 bytes, lines 326-425
326 this.allItems.forEach(function (item) { 327 const checkbox = item.checkbox; 328 let top; 329 if (checkbox) { 330 top = translateY + titleHeight + checkbox.y + 331 (this.scrollOffset || 0) + 3; 332 css(checkbox, { 333 left: (alignAttr.translateX + item.checkboxOffset + 334 checkbox.x - 20) + 'px', 335 top: top + 'px', 336 display: this.proximate || (top > translateY - 6 && 337 top < translateY + clipHeight - 6) ? 338 '' : 339 'none' 340 }); 341 } 342 }, this); 343 } 344 } 345 /** 346 * Render the legend title on top of the legend. 347 * 348 * @private 349 * @function Highcharts.Legend#renderTitle 350 */ 351 renderTitle() { 352 const options = this.options, padding = this.padding, titleOptions = options.title; 353 let bBox, titleHeight = 0; 354 if (titleOptions.text) { 355 if (!this.title) { 356 /** 357 * SVG element of the legend title. 358 * 359 * @readonly 360 * @name Highcharts.Legend#title 361 * @type {Highcharts.SVGElement} 362 */ 363 this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, void 0, void 0, void 0, options.useHTML, void 0, 'legend-title') 364 .attr({ zIndex: 1 }); 365 if (!this.chart.styledMode) { 366 this.title.css(titleOptions.style); 367 } 368 this.title.add(this.group); 369 } 370 // Set the max title width (#7253) 371 if (!titleOptions.width) { 372 this.title.css({ 373 width: this.maxLegendWidth + 'px' 374 }); 375 } 376 bBox = this.title.getBBox(); 377 titleHeight = bBox.height; 378 this.offsetWidth = bBox.width; // #1717 379 this.contentGroup.attr({ translateY: titleHeight }); 380 } 381 this.titleHeight = titleHeight; 382 } 383 /** 384 * Set the legend item text. 385 * 386 * @function Highcharts.Legend#setText 387 * @param {Highcharts.Point|Highcharts.Series} item 388 * The item for which to update the text in the legend. 389 */ 390 setText(item) { 391 const options = this.options; 392 item.legendItem.label.attr({ 393 text: options.labelFormat ? 394 format(options.labelFormat, item, this.chart) : 395 options.labelFormatter.call(item) 396 }); 397 } 398 /** 399 * Render a single specific legend item. Called internally from the `render` 400 * function. 401 * 402 * @private 403 * @function Highcharts.Legend#renderItem 404 * @param {Highcharts.BubbleLegendItem|Highcharts.Point|Highcharts.Series} item 405 * The item to render. 406 */ 407 renderItem(item) { 408 const legend = this, legendItem = item.legendItem = item.legendItem || {}, chart = legend.chart, renderer = chart.renderer, options = legend.options, horizontal = options.layout === 'horizontal', symbolWidth = legend.symbolWidth, symbolPadding = options.symbolPadding || 0, itemStyle = legend.itemStyle, itemHiddenStyle = legend.itemHiddenStyle, itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, ltr = !options.rtl, isSeries = !item.series, series = !isSeries && item.series.drawLegendSymbol ? 409 item.series : 410 item, seriesOptions = series.options, showCheckbox = (!!legend.createCheckboxForItem && 411 seriesOptions && 412 seriesOptions.showCheckbox), useHTML = options.useHTML, itemClassName = item.options.className; 413 let label = legendItem.label, 414 // Full width minus text width 415 itemExtraWidth = symbolWidth + symbolPadding + 416 itemDistance + (showCheckbox ? 20 : 0); 417 if (!label) { // Generate it once, later move it 418 // Generate the group box, a group to hold the symbol and text. Text 419 // is to be appended in Legend class. 420 legendItem.group = renderer 421 .g('legend-item') 422 .addClass('highcharts-' + series.type + '-series ' + 423 'highcharts-color-' + item.colorIndex + 424 (itemClassName ? ' ' + itemClassName : '') + 425 (isSeries ?
vendor: 5,942 bytes, lines 426-557
426 ' highcharts-series-' + item.index : 427 '')) 428 .attr({ zIndex: 1 }) 429 .add(legend.scrollGroup); 430 // Generate the list item text and add it to the group 431 legendItem.label = label = renderer.text('', ltr ? 432 symbolWidth + symbolPadding : 433 -symbolPadding, legend.baseline || 0, useHTML); 434 if (!chart.styledMode) { 435 // Merge to prevent modifying original (#1021) 436 label.css(merge(item.visible ? 437 itemStyle : 438 itemHiddenStyle)); 439 } 440 label 441 .attr({ 442 align: ltr ? 'left' : 'right', 443 zIndex: 2 444 }) 445 .add(legendItem.group); 446 // Get the baseline for the first item - the font size is equal for 447 // all 448 if (!legend.baseline) { 449 legend.fontMetrics = renderer.fontMetrics(label); 450 legend.baseline = 451 legend.fontMetrics.f + 3 + legend.itemMarginTop; 452 label.attr('y', legend.baseline); 453 legend.symbolHeight = 454 pick(options.symbolHeight, legend.fontMetrics.f); 455 if (options.squareSymbol) { 456 legend.symbolWidth = pick(options.symbolWidth, Math.max(legend.symbolHeight, 16)); 457 itemExtraWidth = legend.symbolWidth + symbolPadding + 458 itemDistance + (showCheckbox ? 20 : 0); 459 if (ltr) { 460 label.attr('x', legend.symbolWidth + symbolPadding); 461 } 462 } 463 } 464 // Draw the legend symbol inside the group box 465 series.drawLegendSymbol(legend, item); 466 if (legend.setItemEvents) { 467 legend.setItemEvents(item, label, useHTML); 468 } 469 } 470 // Add the HTML checkbox on top 471 if (showCheckbox && !item.checkbox && legend.createCheckboxForItem) { 472 legend.createCheckboxForItem(item); 473 } 474 // Colorize the items 475 legend.colorizeItem(item, item.visible); 476 // Take care of max width and text overflow (#6659) 477 if (chart.styledMode || !itemStyle.width) { 478 label.css({ 479 width: ((options.itemWidth || 480 legend.widthOption || 481 chart.spacingBox.width) - itemExtraWidth) + 'px' 482 }); 483 } 484 // Always update the text 485 legend.setText(item); 486 // Calculate the positions for the next line 487 const bBox = label.getBBox(); 488 const fontMetricsH = (legend.fontMetrics && legend.fontMetrics.h) || 0; 489 item.itemWidth = item.checkboxOffset = 490 options.itemWidth || 491 legendItem.labelWidth || 492 bBox.width + itemExtraWidth; 493 legend.maxItemWidth = Math.max(legend.maxItemWidth, item.itemWidth); 494 legend.totalItemWidth += item.itemWidth; 495 legend.itemHeight = item.itemHeight = Math.round(legendItem.labelHeight || 496 // Use bBox for multiline (#16398) 497 (bBox.height > fontMetricsH * 1.5 ? bBox.height : fontMetricsH)); 498 } 499 /** 500 * Get the position of the item in the layout. We now know the 501 * maxItemWidth from the previous loop. 502 * 503 * @private 504 * @function Highcharts.Legend#layoutItem 505 * @param {Highcharts.BubbleLegendItem|Highcharts.Point|Highcharts.Series} item 506 */ 507 layoutItem(item) { 508 const options = this.options, padding = this.padding, horizontal = options.layout === 'horizontal', itemHeight = item.itemHeight, itemMarginBottom = this.itemMarginBottom, itemMarginTop = this.itemMarginTop, itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, maxLegendWidth = this.maxLegendWidth, itemWidth = (options.alignColumns && 509 this.totalItemWidth > maxLegendWidth) ? 510 this.maxItemWidth : 511 item.itemWidth, legendItem = item.legendItem || {}; 512 // If the item exceeds the width, start a new line 513 if (horizontal && 514 this.itemX - padding + itemWidth > maxLegendWidth) { 515 this.itemX = padding; 516 if (this.lastLineHeight) { // Not for the first line (#10167) 517 this.itemY += (itemMarginTop + 518 this.lastLineHeight + 519 itemMarginBottom); 520 } 521 this.lastLineHeight = 0; // Reset for next line (#915, #3976) 522 } 523 // Set the edge positions 524 this.lastItemY = itemMarginTop + this.itemY + itemMarginBottom; 525 this.lastLineHeight = Math.max(// #915 526 itemHeight, this.lastLineHeight); 527 // Cache the position of the newly generated or reordered items 528 legendItem.x = this.itemX; 529 legendItem.y = this.itemY; 530 // Advance 531 if (horizontal) { 532 this.itemX += itemWidth; 533 } 534 else { 535 this.itemY += 536 itemMarginTop + itemHeight + itemMarginBottom; 537 this.lastLineHeight = itemHeight; 538 } 539 // The width of the widest item 540 this.offsetWidth = this.widthOption || Math.max((horizontal ? this.itemX - padding - (item.checkbox ? 541 // Decrease by itemDistance only when no checkbox #4853 542 0 : 543 itemDistance) : itemWidth) + padding, this.offsetWidth); 544 } 545 /** 546 * Get all items, which is one item per series for most series and one 547 * item per point for pie series and its derivatives. Fires the event 548 * `afterGetAllItems`. 549 * 550 * @private 551 * @function Highcharts.Legend#getAllItems 552 * @return {Array<(Highcharts.BubbleLegendItem|Highcharts.Point|Highcharts.Series)>} 553 * The current items in the legend. 554 * @emits Highcharts.Legend#event:afterGetAllItems 555 */ 556 getAllItems() { 557 let allItems = [];
558 this.chart.series.forEach(function (series) { 559 const seriesOptions = series && series.options; 560 // Handle showInLegend. If the series is linked to another series, 561 // defaults to false. 562 if (series && pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? void 0 : false, true)) { 563 // Use points or series for the legend item depending on 564 // legendType 565 allItems = allItems.concat((series.legendItem || {}).labels || 566 (seriesOptions.legendType === 'point' ? 567 series.data : 568 series)); 569 } 570 }); 571 fireEvent(this, 'afterGetAllItems', { allItems }); 572 return allItems; 573 } 574 /** 575 * Get a short, three letter string reflecting the alignment and layout. 576 * 577 * @private 578 * @function Highcharts.Legend#getAlignment 579 * @return {string} 580 * The alignment, empty string if floating 581 */ 582 getAlignment() { 583 const options = this.options; 584 // Use the first letter of each alignment option in order to detect 585 // the side. (#4189 - use charAt(x) notation instead of [x] for IE7) 586 if (this.proximate) { 587 return options.align.charAt(0) + 'tv'; 588 } 589 return options.floating ? '' : (options.align.charAt(0) + 590 options.verticalAlign.charAt(0) + 591 options.layout.charAt(0)); 592 } 593 /** 594 * Adjust the chart margins by reserving space for the legend on only one 595 * side of the chart. If the position is set to a corner, top or bottom is 596 * reserved for horizontal legends and left or right for vertical ones. 597 * 598 * @private 599 * @function Highcharts.Legend#adjustMargins 600 * @param {Array<number>} margin 601 * @param {Array<number>} spacing 602 */ 603 adjustMargins(margin, spacing) { 604 const chart = this.chart, options = this.options, alignment = this.getAlignment(); 605 if (alignment) { 606 ([ 607 /(lth|ct|rth)/, 608 /(rtv|rm|rbv)/, 609 /(rbh|cb|lbh)/, 610 /(lbv|lm|ltv)/ 611 ]).forEach(function (alignments, side) { 612 if (alignments.test(alignment) && !defined(margin[side])) { 613 // Now we have detected on which side of the chart we should 614 // reserve space for the legend 615 chart[marginNames[side]] = Math.max(chart[marginNames[side]], (chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] + 616 [1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] + 617 pick(options.margin, 12) + 618 spacing[side] + 619 (chart.titleOffset[side] || 0))); 620 } 621 }); 622 } 623 } 624 /** 625 * @private 626 * @function Highcharts.Legend#proximatePositions 627 */ 628 proximatePositions() { 629 const chart = this.chart, boxes = [], alignLeft = this.options.align === 'left'; 630 this.allItems.forEach(function (item) { 631 let lastPoint, height, useFirstPoint = alignLeft, target, top; 632 if (item.yAxis) { 633 if (item.xAxis.options.reversed) { 634 useFirstPoint = !useFirstPoint; 635 } 636 if (item.points) { 637 lastPoint = find(useFirstPoint ? 638 item.points : 639 item.points.slice(0).reverse(), function (item) { 640 return isNumber(item.plotY); 641 }); 642 } 643 height = this.itemMarginTop + 644 item.legendItem.label.getBBox().height + 645 this.itemMarginBottom; 646 top = item.yAxis.top - chart.plotTop; 647 if (item.visible) { 648 target = lastPoint ? 649 lastPoint.plotY : 650 item.yAxis.height; 651 target += top - 0.3 * height; 652 } 653 else { 654 target = top + item.yAxis.height; 655 } 656 boxes.push({ 657 target: target, 658 size: height, 659 item 660 }); 661 } 662 }, this); 663 let legendItem; 664 for (const box of distribute(boxes, chart.plotHeight)) { 665 legendItem = box.item.legendItem || {}; 666 if (isNumber(box.pos)) { 667 legendItem.y = chart.plotTop - chart.spacing[0] + box.pos; 668 } 669 } 670 } 671 /**
672 * Render the legend. This method can be called both before and after 673 * `chart.render`. If called after, it will only rearrange items instead 674 * of creating new ones. Called internally on initial render and after 675 * redraws. 676 * 677 * @private 678 * @function Highcharts.Legend#render 679 */ 680 render() { 681 const legend = this, chart = legend.chart, renderer = chart.renderer, options = legend.options, padding = legend.padding, 682 // Add each series or point 683 allItems = legend.getAllItems(); 684 let display, legendWidth, legendHeight, legendGroup = legend.group, allowedWidth, box = legend.box; 685 legend.itemX = padding; 686 legend.itemY = legend.initialItemY; 687 legend.offsetWidth = 0; 688 legend.lastItemY = 0; 689 legend.widthOption = relativeLength(options.width, chart.spacingBox.width - padding); 690 // Compute how wide the legend is allowed to be 691 allowedWidth = chart.spacingBox.width - 2 * padding - options.x; 692 if (['rm', 'lm'].indexOf(legend.getAlignment().substring(0, 2)) > -1) { 693 allowedWidth /= 2; 694 } 695 legend.maxLegendWidth = legend.widthOption || allowedWidth; 696 if (!legendGroup) { 697 /** 698 * SVG group of the legend. 699 * 700 * @readonly 701 * @name Highcharts.Legend#group 702 * @type {Highcharts.SVGElement} 703 */ 704 legend.group = legendGroup = renderer 705 .g('legend') 706 .addClass(options.className || '') 707 .attr({ zIndex: 7 }) 708 .add(); 709 legend.contentGroup = renderer 710 .g() 711 .attr({ zIndex: 1 }) // Above background 712 .add(legendGroup); 713 legend.scrollGroup = renderer 714 .g() 715 .add(legend.contentGroup); 716 } 717 legend.renderTitle(); 718 // Sort by legendIndex 719 stableSort(allItems, (a, b) => ((a.options && a.options.legendIndex) || 0) - 720 ((b.options && b.options.legendIndex) || 0)); 721 // Reversed legend 722 if (options.reversed) { 723 allItems.reverse(); 724 } 725 /** 726 * All items for the legend, which is an array of series for most series 727 * and an array of points for pie series and its derivatives. 728 * 729 * @readonly 730 * @name Highcharts.Legend#allItems 731 * @type {Array<(Highcharts.Point|Highcharts.Series)>} 732 */ 733 legend.allItems = allItems; 734 legend.display = display = !!allItems.length; 735 // Render the items. First we run a loop to set the text and properties 736 // and read all the bounding boxes. The next loop computes the item 737 // positions based on the bounding boxes. 738 legend.lastLineHeight = 0; 739 legend.maxItemWidth = 0; 740 legend.totalItemWidth = 0; 741 legend.itemHeight = 0; 742 allItems.forEach(legend.renderItem, legend); 743 allItems.forEach(legend.layoutItem, legend); 744 // Get the box 745 legendWidth = (legend.widthOption || legend.offsetWidth) + padding; 746 legendHeight = legend.lastItemY + legend.lastLineHeight + 747 legend.titleHeight; 748 legendHeight = legend.handleOverflow(legendHeight); 749 legendHeight += padding; 750 // Draw the border and/or background 751 if (!box) { 752 /** 753 * SVG element of the legend box. 754 * 755 * @readonly 756 * @name Highcharts.Legend#box 757 * @type {Highcharts.SVGElement} 758 */ 759 legend.box = box = renderer.rect() 760 .addClass('highcharts-legend-box') 761 .attr({ 762 r: options.borderRadius 763 }) 764 .add(legendGroup); 765 } 766 // Presentational 767 if (!chart.styledMode) { 768 box 769 .attr({ 770 stroke: options.borderColor, 771 'stroke-width': options.borderWidth || 0, 772 fill: options.backgroundColor || 'none' 773 }) 774 .shadow(options.shadow); 775 } 776 if (legendWidth > 0 && legendHeight > 0) { 777 box[box.placed ? 'animate' : 'attr'](box.crisp.call({}, { 778 x: 0, 779 y: 0, 780 width: legendWidth, 781 height: legendHeight 782 }, box.strokeWidth())); 783 } 784 // Hide the border if no items 785 legendGroup[display ? 'show' : 'hide'](); 786 // Open for responsiveness 787 if (chart.styledMode && legendGroup.getStyle('display') === 'none') { 788 legendWidth = legendHeight = 0; 789 } 790 legend.legendWidth = legendWidth; 791 legend.legendHeight = legendHeight; 792 if (display) { 793 legend.align(); 794 } 795 if (!this.proximate) { 796 this.positionItems(); 797 }
vendor: 10,536 bytes, lines 798-1059
798 fireEvent(this, 'afterRender'); 799 } 800 /** 801 * Align the legend to chart's box. 802 * 803 * @private 804 * @function Highcharts.align 805 * @param {Highcharts.BBoxObject} alignTo 806 */ 807 align(alignTo = this.chart.spacingBox) { 808 const chart = this.chart, options = this.options; 809 // If aligning to the top and the layout is horizontal, adjust for 810 // the title (#7428) 811 let y = alignTo.y; 812 if (/(lth|ct|rth)/.test(this.getAlignment()) && 813 chart.titleOffset[0] > 0) { 814 y += chart.titleOffset[0]; 815 } 816 else if (/(lbh|cb|rbh)/.test(this.getAlignment()) && 817 chart.titleOffset[2] > 0) { 818 y -= chart.titleOffset[2]; 819 } 820 if (y !== alignTo.y) { 821 alignTo = merge(alignTo, { y }); 822 } 823 if (!chart.hasRendered) { 824 // Avoid animation when adjusting alignment for responsiveness and 825 // colorAxis label layout 826 this.group.placed = false; 827 } 828 this.group.align(merge(options, { 829 width: this.legendWidth, 830 height: this.legendHeight, 831 verticalAlign: this.proximate ? 'top' : options.verticalAlign 832 }), true, alignTo); 833 } 834 /** 835 * Set up the overflow handling by adding navigation with up and down arrows 836 * below the legend. 837 * 838 * @private 839 * @function Highcharts.Legend#handleOverflow 840 */ 841 handleOverflow(legendHeight) { 842 const legend = this, chart = this.chart, renderer = chart.renderer, options = this.options, optionsY = options.y, alignTop = options.verticalAlign === 'top', padding = this.padding, maxHeight = options.maxHeight, navOptions = options.navigation, animation = pick(navOptions.animation, true), arrowSize = navOptions.arrowSize || 12, pages = this.pages, allItems = this.allItems, clipToHeight = function (height) { 843 if (typeof height === 'number') { 844 clipRect.attr({ 845 height: height 846 }); 847 } 848 else if (clipRect) { // Reset (#5912) 849 legend.clipRect = clipRect.destroy(); 850 legend.contentGroup.clip(); 851 } 852 // Use HTML 853 if (legend.contentGroup.div) { 854 legend.contentGroup.div.style.clip = height ? 855 'rect(' + padding + 'px,9999px,' + 856 (padding + height) + 'px,0)' : 857 'auto'; 858 } 859 }, addTracker = function (key) { 860 legend[key] = renderer 861 .circle(0, 0, arrowSize * 1.3) 862 .translate(arrowSize / 2, arrowSize / 2) 863 .add(nav); 864 if (!chart.styledMode) { 865 legend[key].attr('fill', 'rgba(0,0,0,0.0001)'); 866 } 867 return legend[key]; 868 }; 869 let clipHeight, lastY, legendItem, spaceHeight = (chart.spacingBox.height + 870 (alignTop ? -optionsY : optionsY) - padding), nav = this.nav, clipRect = this.clipRect; 871 // Adjust the height 872 if (options.layout === 'horizontal' && 873 options.verticalAlign !== 'middle' && 874 !options.floating) { 875 spaceHeight /= 2; 876 } 877 if (maxHeight) { 878 spaceHeight = Math.min(spaceHeight, maxHeight); 879 } 880 // Reset the legend height and adjust the clipping rectangle 881 pages.length = 0; 882 if (legendHeight && 883 spaceHeight > 0 && 884 legendHeight > spaceHeight && 885 navOptions.enabled !== false) { 886 this.clipHeight = clipHeight = 887 Math.max(spaceHeight - 20 - this.titleHeight - padding, 0); 888 this.currentPage = pick(this.currentPage, 1); 889 this.fullHeight = legendHeight; 890 // Fill pages with Y positions so that the top of each a legend item 891 // defines the scroll top for each page (#2098) 892 allItems.forEach((item, i) => { 893 legendItem = item.legendItem || {}; 894 const y = legendItem.y || 0, h = Math.round(legendItem.label.getBBox().height); 895 let len = pages.length; 896 if (!len || (y - pages[len - 1] > clipHeight && 897 (lastY || y) !== pages[len - 1])) { 898 pages.push(lastY || y); 899 len++; 900 } 901 // Keep track of which page each item is on 902 legendItem.pageIx = len - 1; 903 if (lastY) { 904 (allItems[i - 1].legendItem || {}).pageIx = len - 1; 905 } 906 // Add the last page if needed (#2617, #13683) 907 if ( 908 // Check the last item 909 i === allItems.length - 1 && 910 // If adding next page is needed (#18768) 911 y + h - pages[len - 1] > clipHeight && 912 y > pages[len - 1]) { 913 pages.push(y); 914 legendItem.pageIx = len; 915 } 916 if (y !== lastY) { 917 lastY = y; 918 } 919 }); 920 // Only apply clipping if needed. Clipping causes blurred legend in 921 // PDF export (#1787) 922 if (!clipRect) { 923 clipRect = legend.clipRect = 924 renderer.clipRect(0, padding - 2, 9999, 0); 925 legend.contentGroup.clip(clipRect); 926 } 927 clipToHeight(clipHeight); 928 // Add navigation elements 929 if (!nav) { 930 this.nav = nav = renderer.g() 931 .attr({ zIndex: 1 }) 932 .add(this.group); 933 this.up = renderer 934 .symbol('triangle', 0, 0, arrowSize, arrowSize) 935 .add(nav); 936 addTracker('upTracker') 937 .on('click', function () { 938 legend.scroll(-1, animation); 939 }); 940 this.pager = renderer.text('', 15, 10) 941 .addClass('highcharts-legend-navigation'); 942 if (!chart.styledMode && navOptions.style) { 943 this.pager.css(navOptions.style); 944 } 945 this.pager.add(nav); 946 this.down = renderer 947 .symbol('triangle-down', 0, 0, arrowSize, arrowSize) 948 .add(nav); 949 addTracker('downTracker') 950 .on('click', function () { 951 legend.scroll(1, animation); 952 }); 953 } 954 // Set initial position 955 legend.scroll(0); 956 legendHeight = spaceHeight; 957 // Reset 958 } 959 else if (nav) { 960 clipToHeight(); 961 this.nav = nav.destroy(); // #6322 962 this.scrollGroup.attr({ 963 translateY: 1 964 }); 965 this.clipHeight = 0; // #1379 966 } 967 return legendHeight; 968 } 969 /** 970 * Scroll the legend by a number of pages. 971 * 972 * @private 973 * @function Highcharts.Legend#scroll 974 * 975 * @param {number} scrollBy 976 * The number of pages to scroll. 977 * 978 * @param {boolean|Partial<Highcharts.AnimationOptionsObject>} [animation] 979 * Whether and how to apply animation. 980 * 981 */ 982 scroll(scrollBy, animation) { 983 const chart = this.chart, pages = this.pages, pageCount = pages.length, clipHeight = this.clipHeight, navOptions = this.options.navigation, pager = this.pager, padding = this.padding; 984 let currentPage = this.currentPage + scrollBy; 985 // When resizing while looking at the last page 986 if (currentPage > pageCount) { 987 currentPage = pageCount; 988 } 989 if (currentPage > 0) { 990 if (typeof animation !== 'undefined') { 991 setAnimation(animation, chart); 992 } 993 this.nav.attr({ 994 translateX: padding, 995 translateY: clipHeight + this.padding + 7 + this.titleHeight, 996 visibility: 'inherit' 997 }); 998 [this.up, this.upTracker].forEach(function (elem) { 999 elem.attr({ 1000 'class': currentPage === 1 ? 1001 'highcharts-legend-nav-inactive' : 1002 'highcharts-legend-nav-active' 1003 }); 1004 }); 1005 pager.attr({ 1006 text: currentPage + '/' + pageCount 1007 }); 1008 [this.down, this.downTracker].forEach(function (elem) { 1009 elem.attr({ 1010 // Adjust to text width 1011 x: 18 + this.pager.getBBox().width, 1012 'class': currentPage === pageCount ? 1013 'highcharts-legend-nav-inactive' : 1014 'highcharts-legend-nav-active' 1015 }); 1016 }, this); 1017 if (!chart.styledMode) { 1018 this.up 1019 .attr({ 1020 fill: currentPage === 1 ? 1021 navOptions.inactiveColor : 1022 navOptions.activeColor 1023 }); 1024 this.upTracker 1025 .css({ 1026 cursor: currentPage === 1 ? 'default' : 'pointer' 1027 }); 1028 this.down 1029 .attr({ 1030 fill: currentPage === pageCount ? 1031 navOptions.inactiveColor : 1032 navOptions.activeColor 1033 }); 1034 this.downTracker 1035 .css({ 1036 cursor: currentPage === pageCount ? 1037 'default' : 1038 'pointer' 1039 }); 1040 } 1041 this.scrollOffset = -pages[currentPage - 1] + this.initialItemY; 1042 this.scrollGroup.animate({ 1043 translateY: this.scrollOffset 1044 }); 1045 this.currentPage = currentPage; 1046 this.positionCheckboxes(); 1047 // Fire event after scroll animation is complete 1048 const animOptions = animObject(pick(animation, chart.renderer.globalAnimation, true)); 1049 syncTimeout(() => { 1050 fireEvent(this, 'afterScroll', { currentPage }); 1051 }, animOptions.duration); 1052 } 1053 } 1054 /** 1055 * @private 1056 * @function Highcharts.Legend#setItemEvents 1057 * @param {Highcharts.BubbleLegendItem|Point|Highcharts.Series} item 1058 * @param {Highcharts.SVGElement} legendLabel 1059 * @param {boolean} [useHTML=false]
vendor: 7,687 bytes, lines 1060-1274
1060 * @emits Highcharts.Legend#event:itemClick 1061 */ 1062 setItemEvents(item, legendLabel, useHTML) { 1063 const legend = this, legendItem = item.legendItem || {}, boxWrapper = legend.chart.renderer.boxWrapper, isPoint = item instanceof Point, isSeries = item instanceof Series, activeClass = 'highcharts-legend-' + 1064 (isPoint ? 'point' : 'series') + '-active', styledMode = legend.chart.styledMode, 1065 // When `useHTML`, the symbol is rendered in other group, so 1066 // we need to apply events listeners to both places 1067 legendElements = useHTML ? 1068 [legendLabel, legendItem.symbol] : 1069 [legendItem.group]; 1070 const setOtherItemsState = (state) => { 1071 legend.allItems.forEach((otherItem) => { 1072 if (item !== otherItem) { 1073 [otherItem] 1074 .concat(otherItem.linkedSeries || []) 1075 .forEach((otherItem) => { 1076 otherItem.setState(state, !isPoint); 1077 }); 1078 } 1079 }); 1080 }; 1081 // Set the events on the item group, or in case of useHTML, the item 1082 // itself (#1249) 1083 for (const element of legendElements) { 1084 if (element) { 1085 element 1086 .on('mouseover', function () { 1087 if (item.visible) { 1088 setOtherItemsState('inactive'); 1089 } 1090 item.setState('hover'); 1091 // A CSS class to dim or hide other than the hovered 1092 // series. 1093 // Works only if hovered series is visible (#10071). 1094 if (item.visible) { 1095 boxWrapper.addClass(activeClass); 1096 } 1097 if (!styledMode) { 1098 legendLabel.css(legend.options.itemHoverStyle); 1099 } 1100 }) 1101 .on('mouseout', function () { 1102 if (!legend.chart.styledMode) { 1103 legendLabel.css(merge(item.visible ? 1104 legend.itemStyle : 1105 legend.itemHiddenStyle)); 1106 } 1107 setOtherItemsState(''); 1108 // A CSS class to dim or hide other than the hovered 1109 // series. 1110 boxWrapper.removeClass(activeClass); 1111 item.setState(); 1112 }) 1113 .on('click', function (event) { 1114 const defaultItemClick = function () { 1115 if (item.setVisible) { 1116 item.setVisible(); 1117 } 1118 // Reset inactive state 1119 setOtherItemsState(item.visible ? 'inactive' : ''); 1120 }; 1121 // A CSS class to dim or hide other than the hovered 1122 // series. Event handling in iOS causes the activeClass 1123 // to be added prior to click in some cases (#7418). 1124 boxWrapper.removeClass(activeClass); 1125 fireEvent(legend, 'itemClick', { 1126 // Pass over the click/touch event. #4. 1127 browserEvent: event, 1128 legendItem: item 1129 }, defaultItemClick); 1130 // Deprecated logic 1131 // Click the name or symbol 1132 if (isPoint) { 1133 item.firePointEvent('legendItemClick', { 1134 browserEvent: event 1135 }); 1136 } 1137 else if (isSeries) { 1138 fireEvent(item, 'legendItemClick', { 1139 browserEvent: event 1140 }); 1141 } 1142 }); 1143 } 1144 } 1145 } 1146 /** 1147 * @private 1148 * @function Highcharts.Legend#createCheckboxForItem 1149 * @param {Highcharts.BubbleLegendItem|Point|Highcharts.Series} item 1150 * @emits Highcharts.Series#event:checkboxClick 1151 */ 1152 createCheckboxForItem(item) { 1153 const legend = this; 1154 item.checkbox = createElement('input', { 1155 type: 'checkbox', 1156 className: 'highcharts-legend-checkbox', 1157 checked: item.selected, 1158 defaultChecked: item.selected // Required by IE7 1159 }, legend.options.itemCheckboxStyle, legend.chart.container); 1160 addEvent(item.checkbox, 'click', function (event) { 1161 const target = event.target; 1162 fireEvent(item.series || item, 'checkboxClick', { 1163 checked: target.checked, 1164 item: item 1165 }, function () { 1166 item.select(); 1167 }); 1168 }); 1169 } 1170} 1171/* * 1172 * 1173 * Class Namespace 1174 * 1175 * */ 1176(function (Legend) { 1177 /* * 1178 * 1179 * Declarations 1180 * 1181 * */ 1182 /* * 1183 * 1184 * Functions 1185 * 1186 * */ 1187 /** 1188 * @private 1189 */ 1190 function compose(ChartClass) { 1191 if (pushUnique(composed, 'Core.Legend')) { 1192 addEvent(ChartClass, 'beforeMargins', function () { 1193 /** 1194 * The legend contains an interactive overview over chart items, 1195 * usually individual series or points depending on the series 1196 * type. The color axis and bubble legend are also rendered in 1197 * the chart legend. 1198 * 1199 * @name Highcharts.Chart#legend 1200 * @type {Highcharts.Legend} 1201 */ 1202 this.legend = new Legend(this, this.options.legend); 1203 }); 1204 } 1205 } 1206 Legend.compose = compose; 1207})(Legend || (Legend = {})); 1208/* * 1209 * 1210 * Default Export 1211 * 1212 * */ 1213export default Legend; 1214/* * 1215 * 1216 * API Declarations 1217 * 1218 * */ 1219/** 1220 * @interface Highcharts.LegendItemObject 1221 */ /** 1222* @name Highcharts.LegendItemObject#item 1223* @type {Highcharts.SVGElement|undefined} 1224*/ /** 1225* @name Highcharts.LegendItemObject#line 1226* @type {Highcharts.SVGElement|undefined} 1227*/ /** 1228* @name Highcharts.LegendItemObject#symbol 1229* @type {Highcharts.SVGElement|undefined} 1230*/ 1231/** 1232 * Gets fired when the legend item is clicked. The default 1233 * action is to toggle the visibility of the series or point. This can be 1234 * prevented by returning `false` or calling `event.preventDefault()`. 1235 * 1236 * @callback Highcharts.LegendItemClickCallbackFunction 1237 * 1238 * @param {Highcharts.Legend} this 1239 * The legend on which the event occurred. 1240 * 1241 * @param {Highcharts.LegendItemClickEventObject} event 1242 * The event that occurred. 1243 */ 1244/** 1245 * Information about the legend click event. 1246 * 1247 * @interface Highcharts.LegendItemClickEventObject 1248 */ /** 1249* Related browser event. 1250* @name Highcharts.LegendItemClickEventObject#browserEvent 1251* @type {Highcharts.PointerEvent} 1252*/ /** 1253* Prevent the default action of toggle the visibility of the series or point. 1254* @name Highcharts.LegendItemClickEventObject#preventDefault 1255* @type {Function} 1256* */ /** 1257* Related legend item, it can be series, point, color axis or data class from 1258* color axis. 1259* @name Highcharts.LegendItemClickEventObject#legendItem 1260* @type {Highcharts.Series|Highcharts.Point|Highcharts.LegendItemObject} 1261* */ /** 1262* Related legend. 1263* @name Highcharts.LegendItemClickEventObject#target 1264* @type {Highcharts.Legend} 1265*/ /** 1266* Event type. 1267* @name Highcharts.LegendItemClickEventObject#type 1268* @type {"itemClick"} 1269*/ 1270/** 1271 * Gets fired when the legend item belonging to a point is clicked. The default 1272 * action is to toggle the visibility of the point. This can be prevented by 1273 * returning `false` or calling `event.preventDefault()`. 1274 *
1275 * **Note:** This option is deprecated in favor of 1276 * Highcharts.LegendItemClickCallbackFunction. 1277 * 1278 * @deprecated 11.4.4 1279 * @callback Highcharts.PointLegendItemClickCallbackFunction 1280 * 1281 * @param {Highcharts.Point} this 1282 * The point on which the event occurred. 1283 * 1284 * @param {Highcharts.PointLegendItemClickEventObject} event 1285 * The event that occurred. 1286 */ 1287/** 1288 * Information about the legend click event. 1289 * 1290 * **Note:** This option is deprecated in favor of 1291 * Highcharts.LegendItemClickEventObject. 1292 * 1293 * @deprecated 11.4.4 1294 * @interface Highcharts.PointLegendItemClickEventObject 1295 */ /** 1296* Related browser event. 1297* @name Highcharts.PointLegendItemClickEventObject#browserEvent 1298* @type {Highcharts.PointerEvent} 1299*/ /** 1300* Prevent the default action of toggle the visibility of the point. 1301* @name Highcharts.PointLegendItemClickEventObject#preventDefault 1302* @type {Function} 1303*/ /** 1304* Related point. 1305* @name Highcharts.PointLegendItemClickEventObject#target 1306* @type {Highcharts.Point} 1307*/ /** 1308* Event type. 1309* @name Highcharts.PointLegendItemClickEventObject#type 1310* @type {"legendItemClick"} 1311*/ 1312/** 1313 * Series color as used by the legend and some series types. 1314 * @name Highcharts.Series#color 1315 * @type {Highcharts.ColorType|undefined} 1316 */ /** 1317* Legend data for the series. 1318* @name Highcharts.Series#legendItem 1319* @type {Highcharts.LegendItemObject|undefined} 1320* @since 10.3.0 1321*/ 1322/** 1323 * Gets fired when the legend item belonging to a series is clicked. The default 1324 * action is to toggle the visibility of the series. This can be prevented by 1325 * returning `false` or calling `event.preventDefault()`. 1326 * 1327 * **Note:** This option is deprecated in favor of 1328 * Highcharts.LegendItemClickCallbackFunction. 1329 * 1330 * @deprecated 11.4.4 1331 * @callback Highcharts.SeriesLegendItemClickCallbackFunction 1332 * 1333 * @param {Highcharts.Series} this 1334 * The series where the event occurred. 1335 * 1336 * @param {Highcharts.SeriesLegendItemClickEventObject} event 1337 * The event that occurred. 1338 */ 1339/** 1340 * Information about the legend click event. 1341 * 1342 * **Note:** This option is deprecated in favor of 1343 * Highcharts.LegendItemClickEventObject. 1344 * 1345 * @deprecated 11.4.4 1346 * @interface Highcharts.SeriesLegendItemClickEventObject 1347 */ /** 1348* Related browser event. 1349* @name Highcharts.SeriesLegendItemClickEventObject#browserEvent 1350* @type {Highcharts.PointerEvent} 1351*/ /** 1352* Prevent the default action of toggle the visibility of the series. 1353* @name Highcharts.SeriesLegendItemClickEventObject#preventDefault 1354* @type {Function} 1355*/ /** 1356* Related series. 1357* @name Highcharts.SeriesLegendItemClickEventObject#target 1358* @type {Highcharts.Series} 1359*/ /** 1360* Event type. 1361* @name Highcharts.SeriesLegendItemClickEventObject#type 1362* @type {"legendItemClick"} 1363*/ 1364(''); // 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.