Legend.js 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166
  1. /* *
  2. *
  3. * (c) 2010-2019 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';
  11. import Highcharts from './Globals.js';
  12. /**
  13. * Gets fired when the legend item belonging to a point is clicked. The default
  14. * action is to toggle the visibility of the point. This can be prevented by
  15. * returning `false` or calling `event.preventDefault()`.
  16. *
  17. * @callback Highcharts.PointLegendItemClickCallbackFunction
  18. *
  19. * @param {Highcharts.Point} this
  20. * The point on which the event occured.
  21. *
  22. * @param {Highcharts.PointLegendItemClickEventObject} event
  23. * The event that occured.
  24. */
  25. /**
  26. * Information about the legend click event.
  27. *
  28. * @interface Highcharts.PointLegendItemClickEventObject
  29. */ /**
  30. * Related browser event.
  31. * @name Highcharts.PointLegendItemClickEventObject#browserEvent
  32. * @type {Highcharts.PointerEvent}
  33. */ /**
  34. * Prevent the default action of toggle the visibility of the point.
  35. * @name Highcharts.PointLegendItemClickEventObject#preventDefault
  36. * @type {Function}
  37. */ /**
  38. * Related point.
  39. * @name Highcharts.PointLegendItemClickEventObject#target
  40. * @type {Highcharts.Point}
  41. */ /**
  42. * Event type.
  43. * @name Highcharts.PointLegendItemClickEventObject#type
  44. * @type {"legendItemClick"}
  45. */
  46. /**
  47. * Gets fired when the legend item belonging to a series is clicked. The default
  48. * action is to toggle the visibility of the series. This can be prevented by
  49. * returning `false` or calling `event.preventDefault()`.
  50. *
  51. * @callback Highcharts.SeriesLegendItemClickCallbackFunction
  52. *
  53. * @param {Highcharts.Series} this
  54. * The series where the event occured.
  55. *
  56. * @param {Highcharts.SeriesLegendItemClickEventObject} event
  57. * The event that occured.
  58. */
  59. /**
  60. * Information about the legend click event.
  61. *
  62. * @interface Highcharts.SeriesLegendItemClickEventObject
  63. */ /**
  64. * Related browser event.
  65. * @name Highcharts.SeriesLegendItemClickEventObject#browserEvent
  66. * @type {Highcharts.PointerEvent}
  67. */ /**
  68. * Prevent the default action of toggle the visibility of the series.
  69. * @name Highcharts.SeriesLegendItemClickEventObject#preventDefault
  70. * @type {Function}
  71. */ /**
  72. * Related series.
  73. * @name Highcharts.SeriesLegendItemClickEventObject#target
  74. * @type {Highcharts.Series}
  75. */ /**
  76. * Event type.
  77. * @name Highcharts.SeriesLegendItemClickEventObject#type
  78. * @type {"legendItemClick"}
  79. */
  80. import U from './Utilities.js';
  81. var defined = U.defined, discardElement = U.discardElement, isNumber = U.isNumber, pick = U.pick, relativeLength = U.relativeLength, setAnimation = U.setAnimation, syncTimeout = U.syncTimeout, wrap = U.wrap;
  82. var H = Highcharts, addEvent = H.addEvent, css = H.css, fireEvent = H.fireEvent, isFirefox = H.isFirefox, marginNames = H.marginNames, merge = H.merge, stableSort = H.stableSort, win = H.win;
  83. /* eslint-disable no-invalid-this, valid-jsdoc */
  84. /**
  85. * The overview of the chart's series. The legend object is instanciated
  86. * internally in the chart constructor, and is available from the `chart.legend`
  87. * property. Each chart has only one legend.
  88. *
  89. * @class
  90. * @name Highcharts.Legend
  91. *
  92. * @param {Highcharts.Chart} chart
  93. * The chart instance.
  94. *
  95. * @param {Highcharts.LegendOptions} options
  96. * Legend options.
  97. */
  98. Highcharts.Legend = function (chart, options) {
  99. this.init(chart, options);
  100. };
  101. Highcharts.Legend.prototype = {
  102. /**
  103. * Initialize the legend.
  104. *
  105. * @private
  106. * @function Highcharts.Legend#init
  107. *
  108. * @param {Highcharts.Chart} chart
  109. * The chart instance.
  110. *
  111. * @param {Highcharts.LegendOptions} options
  112. * Legend options.
  113. *
  114. * @return {void}
  115. */
  116. init: function (chart, options) {
  117. /**
  118. * Chart of this legend.
  119. *
  120. * @readonly
  121. * @name Highcharts.Legend#chart
  122. * @type {Highcharts.Chart}
  123. */
  124. this.chart = chart;
  125. this.setOptions(options);
  126. if (options.enabled) {
  127. // Render it
  128. this.render();
  129. // move checkboxes
  130. addEvent(this.chart, 'endResize', function () {
  131. this.legend.positionCheckboxes();
  132. });
  133. if (this.proximate) {
  134. this.unchartrender = addEvent(this.chart, 'render', function () {
  135. this.legend.proximatePositions();
  136. this.legend.positionItems();
  137. });
  138. }
  139. else if (this.unchartrender) {
  140. this.unchartrender();
  141. }
  142. }
  143. },
  144. /**
  145. * @private
  146. * @function Highcharts.Legend#setOptions
  147. * @param {Highcharts.LegendOptions} options
  148. * @return {void}
  149. */
  150. setOptions: function (options) {
  151. var padding = pick(options.padding, 8);
  152. /**
  153. * Legend options.
  154. *
  155. * @readonly
  156. * @name Highcharts.Legend#options
  157. * @type {Highcharts.LegendOptions}
  158. */
  159. this.options = options;
  160. if (!this.chart.styledMode) {
  161. this.itemStyle = options.itemStyle;
  162. this.itemHiddenStyle = merge(this.itemStyle, options.itemHiddenStyle);
  163. }
  164. this.itemMarginTop = options.itemMarginTop || 0;
  165. this.itemMarginBottom = options.itemMarginBottom || 0;
  166. this.padding = padding;
  167. this.initialItemY = padding - 5; // 5 is pixels above the text
  168. this.symbolWidth = pick(options.symbolWidth, 16);
  169. this.pages = [];
  170. this.proximate = options.layout === 'proximate' && !this.chart.inverted;
  171. },
  172. /**
  173. * Update the legend with new options. Equivalent to running `chart.update`
  174. * with a legend configuration option.
  175. *
  176. * @sample highcharts/legend/legend-update/
  177. * Legend update
  178. *
  179. * @function Highcharts.Legend#update
  180. *
  181. * @param {Highcharts.LegendOptions} options
  182. * Legend options.
  183. *
  184. * @param {boolean} [redraw=true]
  185. * Whether to redraw the chart after the axis is altered. If doing
  186. * more operations on the chart, it is a good idea to set redraw to
  187. * false and call {@link Chart#redraw} after.
  188. * Whether to redraw the chart.
  189. *
  190. * @return {void}
  191. *
  192. * @fires Highcharts.Legends#event:afterUpdate
  193. */
  194. update: function (options, redraw) {
  195. var chart = this.chart;
  196. this.setOptions(merge(true, this.options, options));
  197. this.destroy();
  198. chart.isDirtyLegend = chart.isDirtyBox = true;
  199. if (pick(redraw, true)) {
  200. chart.redraw();
  201. }
  202. fireEvent(this, 'afterUpdate');
  203. },
  204. /**
  205. * Set the colors for the legend item.
  206. *
  207. * @private
  208. * @function Highcharts.Legend#colorizeItem
  209. * @param {Highcharts.BubbleLegend|Highcharts.Point|Highcharts.Series} item
  210. * A Series or Point instance
  211. * @param {boolean} [visible=false]
  212. * Dimmed or colored
  213. * @return {void}
  214. *
  215. * @todo
  216. * Make events official: Fires the event `afterColorizeItem`.
  217. */
  218. colorizeItem: function (item, visible) {
  219. item.legendGroup[visible ? 'removeClass' : 'addClass']('highcharts-legend-item-hidden');
  220. if (!this.chart.styledMode) {
  221. var legend = this, options = legend.options, legendItem = item.legendItem, legendLine = item.legendLine, legendSymbol = item.legendSymbol, hiddenColor = legend.itemHiddenStyle.color, textColor = visible ?
  222. options.itemStyle.color :
  223. hiddenColor, symbolColor = visible ?
  224. (item.color || hiddenColor) :
  225. hiddenColor, markerOptions = item.options && item.options.marker, symbolAttr = { fill: symbolColor };
  226. if (legendItem) {
  227. legendItem.css({
  228. fill: textColor,
  229. color: textColor // #1553, oldIE
  230. });
  231. }
  232. if (legendLine) {
  233. legendLine.attr({ stroke: symbolColor });
  234. }
  235. if (legendSymbol) {
  236. // Apply marker options
  237. if (markerOptions && legendSymbol.isMarker) { // #585
  238. symbolAttr = item.pointAttribs();
  239. if (!visible) {
  240. // #6769
  241. symbolAttr.stroke = symbolAttr.fill = hiddenColor;
  242. }
  243. }
  244. legendSymbol.attr(symbolAttr);
  245. }
  246. }
  247. fireEvent(this, 'afterColorizeItem', { item: item, visible: visible });
  248. },
  249. /**
  250. * @private
  251. * @function Highcharts.Legend#positionItems
  252. * @return {void}
  253. */
  254. positionItems: function () {
  255. // Now that the legend width and height are established, put the items
  256. // in the final position
  257. this.allItems.forEach(this.positionItem, this);
  258. if (!this.chart.isResizing) {
  259. this.positionCheckboxes();
  260. }
  261. },
  262. /**
  263. * Position the legend item.
  264. *
  265. * @private
  266. * @function Highcharts.Legend#positionItem
  267. * @param {Highcharts.BubbleLegend|Highcharts.Point|Highcharts.Series} item
  268. * The item to position
  269. * @return {void}
  270. */
  271. positionItem: function (item) {
  272. var legend = this, options = legend.options, symbolPadding = options.symbolPadding, ltr = !options.rtl, legendItemPos = item._legendItemPos, itemX = legendItemPos[0], itemY = legendItemPos[1], checkbox = item.checkbox, legendGroup = item.legendGroup;
  273. if (legendGroup && legendGroup.element) {
  274. legendGroup[defined(legendGroup.translateY) ? 'animate' : 'attr']({
  275. translateX: ltr ?
  276. itemX :
  277. legend.legendWidth - itemX - 2 * symbolPadding - 4,
  278. translateY: itemY
  279. });
  280. }
  281. if (checkbox) {
  282. checkbox.x = itemX;
  283. checkbox.y = itemY;
  284. }
  285. },
  286. /**
  287. * Destroy a single legend item, used internally on removing series items.
  288. *
  289. * @private
  290. * @function Highcharts.Legend#destroyItem
  291. * @param {Highcharts.BubbleLegend|Highcharts.Point|Highcharts.Series} item
  292. * The item to remove
  293. * @return {void}
  294. */
  295. destroyItem: function (item) {
  296. var checkbox = item.checkbox;
  297. // destroy SVG elements
  298. ['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'].forEach(function (key) {
  299. if (item[key]) {
  300. item[key] = item[key].destroy();
  301. }
  302. });
  303. if (checkbox) {
  304. discardElement(item.checkbox);
  305. }
  306. },
  307. /**
  308. * Destroy the legend. Used internally. To reflow objects, `chart.redraw`
  309. * must be called after destruction.
  310. *
  311. * @private
  312. * @function Highcharts.Legend#destroy
  313. * @return {void}
  314. */
  315. destroy: function () {
  316. /**
  317. * @private
  318. * @param {string} key
  319. * @return {void}
  320. */
  321. function destroyItems(key) {
  322. if (this[key]) {
  323. this[key] = this[key].destroy();
  324. }
  325. }
  326. // Destroy items
  327. this.getAllItems().forEach(function (item) {
  328. ['legendItem', 'legendGroup'].forEach(destroyItems, item);
  329. });
  330. // Destroy legend elements
  331. [
  332. 'clipRect',
  333. 'up',
  334. 'down',
  335. 'pager',
  336. 'nav',
  337. 'box',
  338. 'title',
  339. 'group'
  340. ].forEach(destroyItems, this);
  341. this.display = null; // Reset in .render on update.
  342. },
  343. /**
  344. * Position the checkboxes after the width is determined.
  345. *
  346. * @private
  347. * @function Highcharts.Legend#positionCheckboxes
  348. * @return {void}
  349. */
  350. positionCheckboxes: function () {
  351. var alignAttr = this.group && this.group.alignAttr, translateY, clipHeight = this.clipHeight || this.legendHeight, titleHeight = this.titleHeight;
  352. if (alignAttr) {
  353. translateY = alignAttr.translateY;
  354. this.allItems.forEach(function (item) {
  355. var checkbox = item.checkbox, top;
  356. if (checkbox) {
  357. top = translateY + titleHeight + checkbox.y +
  358. (this.scrollOffset || 0) + 3;
  359. css(checkbox, {
  360. left: (alignAttr.translateX + item.checkboxOffset +
  361. checkbox.x - 20) + 'px',
  362. top: top + 'px',
  363. display: this.proximate || (top > translateY - 6 &&
  364. top < translateY + clipHeight - 6) ?
  365. '' :
  366. 'none'
  367. });
  368. }
  369. }, this);
  370. }
  371. },
  372. /**
  373. * Render the legend title on top of the legend.
  374. *
  375. * @private
  376. * @function Highcharts.Legend#renderTitle
  377. * @return {void}
  378. */
  379. renderTitle: function () {
  380. var options = this.options, padding = this.padding, titleOptions = options.title, titleHeight = 0, bBox;
  381. if (titleOptions.text) {
  382. if (!this.title) {
  383. /**
  384. * SVG element of the legend title.
  385. *
  386. * @readonly
  387. * @name Highcharts.Legend#title
  388. * @type {Highcharts.SVGElement}
  389. */
  390. this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, options.useHTML, null, 'legend-title')
  391. .attr({ zIndex: 1 });
  392. if (!this.chart.styledMode) {
  393. this.title.css(titleOptions.style);
  394. }
  395. this.title.add(this.group);
  396. }
  397. // Set the max title width (#7253)
  398. if (!titleOptions.width) {
  399. this.title.css({
  400. width: this.maxLegendWidth + 'px'
  401. });
  402. }
  403. bBox = this.title.getBBox();
  404. titleHeight = bBox.height;
  405. this.offsetWidth = bBox.width; // #1717
  406. this.contentGroup.attr({ translateY: titleHeight });
  407. }
  408. this.titleHeight = titleHeight;
  409. },
  410. /**
  411. * Set the legend item text.
  412. *
  413. * @function Highcharts.Legend#setText
  414. *
  415. * @param {Highcharts.Point|Highcharts.Series} item
  416. * The item for which to update the text in the legend.
  417. *
  418. * @return {void}
  419. */
  420. setText: function (item) {
  421. var options = this.options;
  422. item.legendItem.attr({
  423. text: options.labelFormat ?
  424. H.format(options.labelFormat, item, this.chart) :
  425. options.labelFormatter.call(item)
  426. });
  427. },
  428. /**
  429. * Render a single specific legend item. Called internally from the `render`
  430. * function.
  431. *
  432. * @private
  433. * @function Highcharts.Legend#renderItem
  434. *
  435. * @param {Highcharts.BubbleLegend|Highcharts.Point|Highcharts.Series} item
  436. * The item to render.
  437. *
  438. * @return {void}
  439. */
  440. renderItem: function (item) {
  441. var legend = this, chart = legend.chart, renderer = chart.renderer, options = legend.options, horizontal = options.layout === 'horizontal', symbolWidth = legend.symbolWidth, symbolPadding = options.symbolPadding, itemStyle = legend.itemStyle, itemHiddenStyle = legend.itemHiddenStyle, itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, ltr = !options.rtl, bBox, li = item.legendItem, isSeries = !item.series, series = !isSeries && item.series.drawLegendSymbol ?
  442. item.series :
  443. item, seriesOptions = series.options, showCheckbox = legend.createCheckboxForItem &&
  444. seriesOptions &&
  445. seriesOptions.showCheckbox,
  446. // full width minus text width
  447. itemExtraWidth = symbolWidth + symbolPadding +
  448. itemDistance + (showCheckbox ? 20 : 0), useHTML = options.useHTML, itemClassName = item.options.className;
  449. if (!li) { // generate it once, later move it
  450. // Generate the group box, a group to hold the symbol and text. Text
  451. // is to be appended in Legend class.
  452. item.legendGroup = renderer
  453. .g('legend-item')
  454. .addClass('highcharts-' + series.type + '-series ' +
  455. 'highcharts-color-' + item.colorIndex +
  456. (itemClassName ? ' ' + itemClassName : '') +
  457. (isSeries ?
  458. ' highcharts-series-' + item.index :
  459. ''))
  460. .attr({ zIndex: 1 })
  461. .add(legend.scrollGroup);
  462. // Generate the list item text and add it to the group
  463. item.legendItem = li = renderer.text('', ltr ?
  464. symbolWidth + symbolPadding :
  465. -symbolPadding, legend.baseline || 0, useHTML);
  466. if (!chart.styledMode) {
  467. // merge to prevent modifying original (#1021)
  468. li.css(merge(item.visible ?
  469. itemStyle :
  470. itemHiddenStyle));
  471. }
  472. li
  473. .attr({
  474. align: ltr ? 'left' : 'right',
  475. zIndex: 2
  476. })
  477. .add(item.legendGroup);
  478. // Get the baseline for the first item - the font size is equal for
  479. // all
  480. if (!legend.baseline) {
  481. legend.fontMetrics = renderer.fontMetrics(chart.styledMode ? 12 : itemStyle.fontSize, li);
  482. legend.baseline =
  483. legend.fontMetrics.f + 3 + legend.itemMarginTop;
  484. li.attr('y', legend.baseline);
  485. }
  486. // Draw the legend symbol inside the group box
  487. legend.symbolHeight =
  488. options.symbolHeight || legend.fontMetrics.f;
  489. series.drawLegendSymbol(legend, item);
  490. if (legend.setItemEvents) {
  491. legend.setItemEvents(item, li, useHTML);
  492. }
  493. }
  494. // Add the HTML checkbox on top
  495. if (showCheckbox && !item.checkbox) {
  496. legend.createCheckboxForItem(item);
  497. }
  498. // Colorize the items
  499. legend.colorizeItem(item, item.visible);
  500. // Take care of max width and text overflow (#6659)
  501. if (chart.styledMode || !itemStyle.width) {
  502. li.css({
  503. width: (options.itemWidth ||
  504. legend.widthOption ||
  505. chart.spacingBox.width) - itemExtraWidth
  506. });
  507. }
  508. // Always update the text
  509. legend.setText(item);
  510. // calculate the positions for the next line
  511. bBox = li.getBBox();
  512. item.itemWidth = item.checkboxOffset =
  513. options.itemWidth ||
  514. item.legendItemWidth ||
  515. bBox.width + itemExtraWidth;
  516. legend.maxItemWidth = Math.max(legend.maxItemWidth, item.itemWidth);
  517. legend.totalItemWidth += item.itemWidth;
  518. legend.itemHeight = item.itemHeight = Math.round(item.legendItemHeight || bBox.height || legend.symbolHeight);
  519. },
  520. /**
  521. * Get the position of the item in the layout. We now know the
  522. * maxItemWidth from the previous loop.
  523. *
  524. * @private
  525. * @function Highcharts.Legend#layoutItem
  526. *
  527. * @param {Highcharts.BubbleLegend|Highcharts.Point|Highcharts.Series} item
  528. *
  529. * @return {void}
  530. */
  531. layoutItem: function (item) {
  532. var 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 &&
  533. this.totalItemWidth > maxLegendWidth) ?
  534. this.maxItemWidth :
  535. item.itemWidth;
  536. // If the item exceeds the width, start a new line
  537. if (horizontal &&
  538. this.itemX - padding + itemWidth > maxLegendWidth) {
  539. this.itemX = padding;
  540. if (this.lastLineHeight) { // Not for the first line (#10167)
  541. this.itemY += (itemMarginTop +
  542. this.lastLineHeight +
  543. itemMarginBottom);
  544. }
  545. this.lastLineHeight = 0; // reset for next line (#915, #3976)
  546. }
  547. // Set the edge positions
  548. this.lastItemY = itemMarginTop + this.itemY + itemMarginBottom;
  549. this.lastLineHeight = Math.max(// #915
  550. itemHeight, this.lastLineHeight);
  551. // cache the position of the newly generated or reordered items
  552. item._legendItemPos = [this.itemX, this.itemY];
  553. // advance
  554. if (horizontal) {
  555. this.itemX += itemWidth;
  556. }
  557. else {
  558. this.itemY +=
  559. itemMarginTop + itemHeight + itemMarginBottom;
  560. this.lastLineHeight = itemHeight;
  561. }
  562. // the width of the widest item
  563. this.offsetWidth = this.widthOption || Math.max((horizontal ? this.itemX - padding - (item.checkbox ?
  564. // decrease by itemDistance only when no checkbox #4853
  565. 0 :
  566. itemDistance) : itemWidth) + padding, this.offsetWidth);
  567. },
  568. /**
  569. * Get all items, which is one item per series for most series and one
  570. * item per point for pie series and its derivatives. Fires the event
  571. * `afterGetAllItems`.
  572. *
  573. * @private
  574. * @function Highcharts.Legend#getAllItems
  575. * @return {Array<(Highcharts.BubbleLegend|Highcharts.Point|Highcharts.Series)>}
  576. * The current items in the legend.
  577. * @fires Highcharts.Legend#event:afterGetAllItems
  578. */
  579. getAllItems: function () {
  580. var allItems = [];
  581. this.chart.series.forEach(function (series) {
  582. var seriesOptions = series && series.options;
  583. // Handle showInLegend. If the series is linked to another series,
  584. // defaults to false.
  585. if (series && pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? void 0 : false, true)) {
  586. // Use points or series for the legend item depending on
  587. // legendType
  588. allItems = allItems.concat(series.legendItems ||
  589. (seriesOptions.legendType === 'point' ?
  590. series.data :
  591. series));
  592. }
  593. });
  594. fireEvent(this, 'afterGetAllItems', { allItems: allItems });
  595. return allItems;
  596. },
  597. /**
  598. * Get a short, three letter string reflecting the alignment and layout.
  599. *
  600. * @private
  601. * @function Highcharts.Legend#getAlignment
  602. *
  603. * @return {string}
  604. * The alignment, empty string if floating
  605. */
  606. getAlignment: function () {
  607. var options = this.options;
  608. // Use the first letter of each alignment option in order to detect
  609. // the side. (#4189 - use charAt(x) notation instead of [x] for IE7)
  610. if (this.proximate) {
  611. return options.align.charAt(0) + 'tv';
  612. }
  613. return options.floating ? '' : (options.align.charAt(0) +
  614. options.verticalAlign.charAt(0) +
  615. options.layout.charAt(0));
  616. },
  617. /**
  618. * Adjust the chart margins by reserving space for the legend on only one
  619. * side of the chart. If the position is set to a corner, top or bottom is
  620. * reserved for horizontal legends and left or right for vertical ones.
  621. *
  622. * @private
  623. * @function Highcharts.Legend#adjustMargins
  624. * @param {Array<number>} margin
  625. * @param {Array<number>} spacing
  626. * @return {void}
  627. */
  628. adjustMargins: function (margin, spacing) {
  629. var chart = this.chart, options = this.options, alignment = this.getAlignment();
  630. if (alignment) {
  631. ([
  632. /(lth|ct|rth)/,
  633. /(rtv|rm|rbv)/,
  634. /(rbh|cb|lbh)/,
  635. /(lbv|lm|ltv)/
  636. ]).forEach(function (alignments, side) {
  637. if (alignments.test(alignment) && !defined(margin[side])) {
  638. // Now we have detected on which side of the chart we should
  639. // reserve space for the legend
  640. chart[marginNames[side]] = Math.max(chart[marginNames[side]], (chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] +
  641. [1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] +
  642. pick(options.margin, 12) +
  643. spacing[side] +
  644. (chart.titleOffset[side] || 0)));
  645. }
  646. });
  647. }
  648. },
  649. /**
  650. * @private
  651. * @function Highcharts.Legend#proximatePositions
  652. * @return {void}
  653. */
  654. proximatePositions: function () {
  655. var chart = this.chart, boxes = [], alignLeft = this.options.align === 'left';
  656. this.allItems.forEach(function (item) {
  657. var lastPoint, height, useFirstPoint = alignLeft, target, top;
  658. if (item.yAxis && item.points) {
  659. if (item.xAxis.options.reversed) {
  660. useFirstPoint = !useFirstPoint;
  661. }
  662. lastPoint = H.find(useFirstPoint ?
  663. item.points :
  664. item.points.slice(0).reverse(), function (item) {
  665. return isNumber(item.plotY);
  666. });
  667. height = this.itemMarginTop +
  668. item.legendItem.getBBox().height +
  669. this.itemMarginBottom;
  670. top = item.yAxis.top - chart.plotTop;
  671. if (item.visible) {
  672. target = lastPoint ?
  673. lastPoint.plotY :
  674. item.yAxis.height;
  675. target += top - 0.3 * height;
  676. }
  677. else {
  678. target = top + item.yAxis.height;
  679. }
  680. boxes.push({
  681. target: target,
  682. size: height,
  683. item: item
  684. });
  685. }
  686. }, this);
  687. H.distribute(boxes, chart.plotHeight);
  688. boxes.forEach(function (box) {
  689. box.item._legendItemPos[1] =
  690. chart.plotTop - chart.spacing[0] + box.pos;
  691. });
  692. },
  693. /**
  694. * Render the legend. This method can be called both before and after
  695. * `chart.render`. If called after, it will only rearrange items instead
  696. * of creating new ones. Called internally on initial render and after
  697. * redraws.
  698. *
  699. * @private
  700. * @function Highcharts.Legend#render
  701. * @return {void}
  702. */
  703. render: function () {
  704. var legend = this, chart = legend.chart, renderer = chart.renderer, legendGroup = legend.group, allItems, display, legendWidth, legendHeight, box = legend.box, options = legend.options, padding = legend.padding, allowedWidth;
  705. legend.itemX = padding;
  706. legend.itemY = legend.initialItemY;
  707. legend.offsetWidth = 0;
  708. legend.lastItemY = 0;
  709. legend.widthOption = relativeLength(options.width, chart.spacingBox.width - padding);
  710. // Compute how wide the legend is allowed to be
  711. allowedWidth =
  712. chart.spacingBox.width - 2 * padding - options.x;
  713. if (['rm', 'lm'].indexOf(legend.getAlignment().substring(0, 2)) > -1) {
  714. allowedWidth /= 2;
  715. }
  716. legend.maxLegendWidth = legend.widthOption || allowedWidth;
  717. if (!legendGroup) {
  718. /**
  719. * SVG group of the legend.
  720. *
  721. * @readonly
  722. * @name Highcharts.Legend#group
  723. * @type {Highcharts.SVGElement}
  724. */
  725. legend.group = legendGroup = renderer.g('legend')
  726. .attr({ zIndex: 7 })
  727. .add();
  728. legend.contentGroup = renderer.g()
  729. .attr({ zIndex: 1 }) // above background
  730. .add(legendGroup);
  731. legend.scrollGroup = renderer.g()
  732. .add(legend.contentGroup);
  733. }
  734. legend.renderTitle();
  735. // add each series or point
  736. allItems = legend.getAllItems();
  737. // sort by legendIndex
  738. stableSort(allItems, function (a, b) {
  739. return ((a.options && a.options.legendIndex) || 0) -
  740. ((b.options && b.options.legendIndex) || 0);
  741. });
  742. // reversed legend
  743. if (options.reversed) {
  744. allItems.reverse();
  745. }
  746. /**
  747. * All items for the legend, which is an array of series for most series
  748. * and an array of points for pie series and its derivatives.
  749. *
  750. * @readonly
  751. * @name Highcharts.Legend#allItems
  752. * @type {Array<(Highcharts.Point|Highcharts.Series)>}
  753. */
  754. legend.allItems = allItems;
  755. legend.display = display = !!allItems.length;
  756. // Render the items. First we run a loop to set the text and properties
  757. // and read all the bounding boxes. The next loop computes the item
  758. // positions based on the bounding boxes.
  759. legend.lastLineHeight = 0;
  760. legend.maxItemWidth = 0;
  761. legend.totalItemWidth = 0;
  762. legend.itemHeight = 0;
  763. allItems.forEach(legend.renderItem, legend);
  764. allItems.forEach(legend.layoutItem, legend);
  765. // Get the box
  766. legendWidth = (legend.widthOption || legend.offsetWidth) + padding;
  767. legendHeight = legend.lastItemY + legend.lastLineHeight +
  768. legend.titleHeight;
  769. legendHeight = legend.handleOverflow(legendHeight);
  770. legendHeight += padding;
  771. // Draw the border and/or background
  772. if (!box) {
  773. /**
  774. * SVG element of the legend box.
  775. *
  776. * @readonly
  777. * @name Highcharts.Legend#box
  778. * @type {Highcharts.SVGElement}
  779. */
  780. legend.box = box = renderer.rect()
  781. .addClass('highcharts-legend-box')
  782. .attr({
  783. r: options.borderRadius
  784. })
  785. .add(legendGroup);
  786. box.isNew = true;
  787. }
  788. // Presentational
  789. if (!chart.styledMode) {
  790. box
  791. .attr({
  792. stroke: options.borderColor,
  793. 'stroke-width': options.borderWidth || 0,
  794. fill: options.backgroundColor || 'none'
  795. })
  796. .shadow(options.shadow);
  797. }
  798. if (legendWidth > 0 && legendHeight > 0) {
  799. box[box.isNew ? 'attr' : 'animate'](box.crisp.call({}, {
  800. x: 0,
  801. y: 0,
  802. width: legendWidth,
  803. height: legendHeight
  804. }, box.strokeWidth()));
  805. box.isNew = false;
  806. }
  807. // hide the border if no items
  808. box[display ? 'show' : 'hide']();
  809. // Open for responsiveness
  810. if (chart.styledMode && legendGroup.getStyle('display') === 'none') {
  811. legendWidth = legendHeight = 0;
  812. }
  813. legend.legendWidth = legendWidth;
  814. legend.legendHeight = legendHeight;
  815. if (display) {
  816. // If aligning to the top and the layout is horizontal, adjust for
  817. // the title (#7428)
  818. var alignTo = chart.spacingBox;
  819. var y = alignTo.y;
  820. if (/(lth|ct|rth)/.test(legend.getAlignment()) &&
  821. chart.titleOffset[0] > 0) {
  822. y += chart.titleOffset[0];
  823. }
  824. else if (/(lbh|cb|rbh)/.test(legend.getAlignment()) &&
  825. chart.titleOffset[2] > 0) {
  826. y -= chart.titleOffset[2];
  827. }
  828. if (y !== alignTo.y) {
  829. alignTo = merge(alignTo, { y: y });
  830. }
  831. legendGroup.align(merge(options, {
  832. width: legendWidth,
  833. height: legendHeight,
  834. verticalAlign: this.proximate ? 'top' : options.verticalAlign
  835. }), true, alignTo);
  836. }
  837. if (!this.proximate) {
  838. this.positionItems();
  839. }
  840. fireEvent(this, 'afterRender');
  841. },
  842. /**
  843. * Set up the overflow handling by adding navigation with up and down arrows
  844. * below the legend.
  845. *
  846. * @private
  847. * @function Highcharts.Legend#handleOverflow
  848. * @param {number} legendHeight
  849. * @return {number}
  850. */
  851. handleOverflow: function (legendHeight) {
  852. var legend = this, chart = this.chart, renderer = chart.renderer, options = this.options, optionsY = options.y, alignTop = options.verticalAlign === 'top', padding = this.padding, spaceHeight = (chart.spacingBox.height +
  853. (alignTop ? -optionsY : optionsY) - padding), maxHeight = options.maxHeight, clipHeight, clipRect = this.clipRect, navOptions = options.navigation, animation = pick(navOptions.animation, true), arrowSize = navOptions.arrowSize || 12, nav = this.nav, pages = this.pages, lastY, allItems = this.allItems, clipToHeight = function (height) {
  854. if (typeof height === 'number') {
  855. clipRect.attr({
  856. height: height
  857. });
  858. }
  859. else if (clipRect) { // Reset (#5912)
  860. legend.clipRect = clipRect.destroy();
  861. legend.contentGroup.clip();
  862. }
  863. // useHTML
  864. if (legend.contentGroup.div) {
  865. legend.contentGroup.div.style.clip = height ?
  866. 'rect(' + padding + 'px,9999px,' +
  867. (padding + height) + 'px,0)' :
  868. 'auto';
  869. }
  870. }, addTracker = function (key) {
  871. legend[key] = renderer
  872. .circle(0, 0, arrowSize * 1.3)
  873. .translate(arrowSize / 2, arrowSize / 2)
  874. .add(nav);
  875. if (!chart.styledMode) {
  876. legend[key].attr('fill', 'rgba(0,0,0,0.0001)');
  877. }
  878. return legend[key];
  879. };
  880. // Adjust the height
  881. if (options.layout === 'horizontal' &&
  882. options.verticalAlign !== 'middle' &&
  883. !options.floating) {
  884. spaceHeight /= 2;
  885. }
  886. if (maxHeight) {
  887. spaceHeight = Math.min(spaceHeight, maxHeight);
  888. }
  889. // Reset the legend height and adjust the clipping rectangle
  890. pages.length = 0;
  891. if (legendHeight > spaceHeight &&
  892. navOptions.enabled !== false) {
  893. this.clipHeight = clipHeight =
  894. Math.max(spaceHeight - 20 - this.titleHeight - padding, 0);
  895. this.currentPage = pick(this.currentPage, 1);
  896. this.fullHeight = legendHeight;
  897. // Fill pages with Y positions so that the top of each a legend item
  898. // defines the scroll top for each page (#2098)
  899. allItems.forEach(function (item, i) {
  900. var y = item._legendItemPos[1], h = Math.round(item.legendItem.getBBox().height), len = pages.length;
  901. if (!len || (y - pages[len - 1] > clipHeight &&
  902. (lastY || y) !== pages[len - 1])) {
  903. pages.push(lastY || y);
  904. len++;
  905. }
  906. // Keep track of which page each item is on
  907. item.pageIx = len - 1;
  908. if (lastY) {
  909. allItems[i - 1].pageIx = len - 1;
  910. }
  911. if (i === allItems.length - 1 &&
  912. y + h - pages[len - 1] > clipHeight &&
  913. y !== lastY // #2617
  914. ) {
  915. pages.push(y);
  916. item.pageIx = len;
  917. }
  918. if (y !== lastY) {
  919. lastY = y;
  920. }
  921. });
  922. // Only apply clipping if needed. Clipping causes blurred legend in
  923. // PDF export (#1787)
  924. if (!clipRect) {
  925. clipRect = legend.clipRect =
  926. renderer.clipRect(0, padding, 9999, 0);
  927. legend.contentGroup.clip(clipRect);
  928. }
  929. clipToHeight(clipHeight);
  930. // Add navigation elements
  931. if (!nav) {
  932. this.nav = nav = renderer.g()
  933. .attr({ zIndex: 1 })
  934. .add(this.group);
  935. this.up = renderer
  936. .symbol('triangle', 0, 0, arrowSize, arrowSize)
  937. .add(nav);
  938. addTracker('upTracker')
  939. .on('click', function () {
  940. legend.scroll(-1, animation);
  941. });
  942. this.pager = renderer.text('', 15, 10)
  943. .addClass('highcharts-legend-navigation');
  944. if (!chart.styledMode) {
  945. this.pager.css(navOptions.style);
  946. }
  947. this.pager.add(nav);
  948. this.down = renderer
  949. .symbol('triangle-down', 0, 0, arrowSize, arrowSize)
  950. .add(nav);
  951. addTracker('downTracker')
  952. .on('click', function () {
  953. legend.scroll(1, animation);
  954. });
  955. }
  956. // Set initial position
  957. legend.scroll(0);
  958. legendHeight = spaceHeight;
  959. // Reset
  960. }
  961. else if (nav) {
  962. clipToHeight();
  963. this.nav = nav.destroy(); // #6322
  964. this.scrollGroup.attr({
  965. translateY: 1
  966. });
  967. this.clipHeight = 0; // #1379
  968. }
  969. return legendHeight;
  970. },
  971. /**
  972. * Scroll the legend by a number of pages.
  973. *
  974. * @private
  975. * @function Highcharts.Legend#scroll
  976. *
  977. * @param {number} scrollBy
  978. * The number of pages to scroll.
  979. *
  980. * @param {boolean|Highcharts.AnimationOptionsObject} [animation]
  981. * Whether and how to apply animation.
  982. *
  983. * @return {void}
  984. */
  985. scroll: function (scrollBy, animation) {
  986. var _this = this;
  987. var chart = this.chart, pages = this.pages, pageCount = pages.length, currentPage = this.currentPage + scrollBy, clipHeight = this.clipHeight, navOptions = this.options.navigation, pager = this.pager, padding = this.padding;
  988. // When resizing while looking at the last page
  989. if (currentPage > pageCount) {
  990. currentPage = pageCount;
  991. }
  992. if (currentPage > 0) {
  993. if (typeof animation !== 'undefined') {
  994. setAnimation(animation, chart);
  995. }
  996. this.nav.attr({
  997. translateX: padding,
  998. translateY: clipHeight + this.padding + 7 + this.titleHeight,
  999. visibility: 'visible'
  1000. });
  1001. [this.up, this.upTracker].forEach(function (elem) {
  1002. elem.attr({
  1003. 'class': currentPage === 1 ?
  1004. 'highcharts-legend-nav-inactive' :
  1005. 'highcharts-legend-nav-active'
  1006. });
  1007. });
  1008. pager.attr({
  1009. text: currentPage + '/' + pageCount
  1010. });
  1011. [this.down, this.downTracker].forEach(function (elem) {
  1012. elem.attr({
  1013. // adjust to text width
  1014. x: 18 + this.pager.getBBox().width,
  1015. 'class': currentPage === pageCount ?
  1016. 'highcharts-legend-nav-inactive' :
  1017. 'highcharts-legend-nav-active'
  1018. });
  1019. }, this);
  1020. if (!chart.styledMode) {
  1021. this.up
  1022. .attr({
  1023. fill: currentPage === 1 ?
  1024. navOptions.inactiveColor :
  1025. navOptions.activeColor
  1026. });
  1027. this.upTracker
  1028. .css({
  1029. cursor: currentPage === 1 ? 'default' : 'pointer'
  1030. });
  1031. this.down
  1032. .attr({
  1033. fill: currentPage === pageCount ?
  1034. navOptions.inactiveColor :
  1035. navOptions.activeColor
  1036. });
  1037. this.downTracker
  1038. .css({
  1039. cursor: currentPage === pageCount ?
  1040. 'default' :
  1041. 'pointer'
  1042. });
  1043. }
  1044. this.scrollOffset = -pages[currentPage - 1] + this.initialItemY;
  1045. this.scrollGroup.animate({
  1046. translateY: this.scrollOffset
  1047. });
  1048. this.currentPage = currentPage;
  1049. this.positionCheckboxes();
  1050. // Fire event after scroll animation is complete
  1051. var animOptions = H.animObject(pick(animation, chart.renderer.globalAnimation, true));
  1052. syncTimeout(function () {
  1053. fireEvent(_this, 'afterScroll', { currentPage: currentPage });
  1054. }, animOptions.duration || 0);
  1055. }
  1056. }
  1057. };
  1058. /**
  1059. * Legend symbol mixin.
  1060. *
  1061. * @private
  1062. * @mixin Highcharts.LegendSymbolMixin
  1063. */
  1064. H.LegendSymbolMixin = {
  1065. /**
  1066. * Get the series' symbol in the legend
  1067. *
  1068. * @private
  1069. * @function Highcharts.LegendSymbolMixin.drawRectangle
  1070. *
  1071. * @param {Highcharts.Legend} legend
  1072. * The legend object
  1073. *
  1074. * @param {Highcharts.Point|Highcharts.Series} item
  1075. * The series (this) or point
  1076. *
  1077. * @return {void}
  1078. */
  1079. drawRectangle: function (legend, item) {
  1080. var options = legend.options, symbolHeight = legend.symbolHeight, square = options.squareSymbol, symbolWidth = square ? symbolHeight : legend.symbolWidth;
  1081. item.legendSymbol = this.chart.renderer.rect(square ? (legend.symbolWidth - symbolHeight) / 2 : 0, legend.baseline - symbolHeight + 1, // #3988
  1082. symbolWidth, symbolHeight, pick(legend.options.symbolRadius, symbolHeight / 2))
  1083. .addClass('highcharts-point')
  1084. .attr({
  1085. zIndex: 3
  1086. }).add(item.legendGroup);
  1087. },
  1088. /**
  1089. * Get the series' symbol in the legend. This method should be overridable
  1090. * to create custom symbols through
  1091. * Highcharts.seriesTypes[type].prototype.drawLegendSymbols.
  1092. *
  1093. * @private
  1094. * @function Highcharts.LegendSymbolMixin.drawLineMarker
  1095. *
  1096. * @param {Highcharts.Legend} legend
  1097. * The legend object.
  1098. *
  1099. * @return {void}
  1100. */
  1101. drawLineMarker: function (legend) {
  1102. var options = this.options, markerOptions = options.marker, radius, legendSymbol, symbolWidth = legend.symbolWidth, symbolHeight = legend.symbolHeight, generalRadius = symbolHeight / 2, renderer = this.chart.renderer, legendItemGroup = this.legendGroup, verticalCenter = legend.baseline -
  1103. Math.round(legend.fontMetrics.b * 0.3), attr = {};
  1104. // Draw the line
  1105. if (!this.chart.styledMode) {
  1106. attr = {
  1107. 'stroke-width': options.lineWidth || 0
  1108. };
  1109. if (options.dashStyle) {
  1110. attr.dashstyle = options.dashStyle;
  1111. }
  1112. }
  1113. this.legendLine = renderer
  1114. .path([
  1115. 'M',
  1116. 0,
  1117. verticalCenter,
  1118. 'L',
  1119. symbolWidth,
  1120. verticalCenter
  1121. ])
  1122. .addClass('highcharts-graph')
  1123. .attr(attr)
  1124. .add(legendItemGroup);
  1125. // Draw the marker
  1126. if (markerOptions && markerOptions.enabled !== false && symbolWidth) {
  1127. // Do not allow the marker to be larger than the symbolHeight
  1128. radius = Math.min(pick(markerOptions.radius, generalRadius), generalRadius);
  1129. // Restrict symbol markers size
  1130. if (this.symbol.indexOf('url') === 0) {
  1131. markerOptions = merge(markerOptions, {
  1132. width: symbolHeight,
  1133. height: symbolHeight
  1134. });
  1135. radius = 0;
  1136. }
  1137. this.legendSymbol = legendSymbol = renderer.symbol(this.symbol, (symbolWidth / 2) - radius, verticalCenter - radius, 2 * radius, 2 * radius, markerOptions)
  1138. .addClass('highcharts-point')
  1139. .add(legendItemGroup);
  1140. legendSymbol.isMarker = true;
  1141. }
  1142. }
  1143. };
  1144. // Workaround for #2030, horizontal legend items not displaying in IE11 Preview,
  1145. // and for #2580, a similar drawing flaw in Firefox 26.
  1146. // Explore if there's a general cause for this. The problem may be related
  1147. // to nested group elements, as the legend item texts are within 4 group
  1148. // elements.
  1149. if (/Trident\/7\.0/.test(win.navigator && win.navigator.userAgent) ||
  1150. isFirefox) {
  1151. wrap(Highcharts.Legend.prototype, 'positionItem', function (proceed, item) {
  1152. var legend = this,
  1153. // If chart destroyed in sync, this is undefined (#2030)
  1154. runPositionItem = function () {
  1155. if (item._legendItemPos) {
  1156. proceed.call(legend, item);
  1157. }
  1158. };
  1159. // Do it now, for export and to get checkbox placement
  1160. runPositionItem();
  1161. // Do it after to work around the core issue
  1162. if (!legend.bubbleLegend) {
  1163. setTimeout(runPositionItem);
  1164. }
  1165. });
  1166. }