BubbleLegend.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893
  1. /* *
  2. *
  3. * (c) 2010-2021 Highsoft AS
  4. *
  5. * Author: Paweł Potaczek
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. 'use strict';
  13. import Chart from '../../Core/Chart/Chart.js';
  14. import Color from '../../Core/Color/Color.js';
  15. var color = Color.parse;
  16. import F from '../../Core/FormatUtilities.js';
  17. import H from '../../Core/Globals.js';
  18. var noop = H.noop;
  19. import Legend from '../../Core/Legend.js';
  20. import O from '../../Core/Options.js';
  21. var setOptions = O.setOptions;
  22. import palette from '../../Core/Color/Palette.js';
  23. import Series from '../../Core/Series/Series.js';
  24. import U from '../../Core/Utilities.js';
  25. var addEvent = U.addEvent, arrayMax = U.arrayMax, arrayMin = U.arrayMin, isNumber = U.isNumber, merge = U.merge, objectEach = U.objectEach, pick = U.pick, stableSort = U.stableSort, wrap = U.wrap;
  26. /**
  27. * @interface Highcharts.BubbleLegendFormatterContextObject
  28. */ /**
  29. * The center y position of the range.
  30. * @name Highcharts.BubbleLegendFormatterContextObject#center
  31. * @type {number}
  32. */ /**
  33. * The radius of the bubble range.
  34. * @name Highcharts.BubbleLegendFormatterContextObject#radius
  35. * @type {number}
  36. */ /**
  37. * The bubble value.
  38. * @name Highcharts.BubbleLegendFormatterContextObject#value
  39. * @type {number}
  40. */
  41. ''; // detach doclets above
  42. import './BubbleSeries.js';
  43. setOptions({
  44. legend: {
  45. /**
  46. * The bubble legend is an additional element in legend which
  47. * presents the scale of the bubble series. Individual bubble ranges
  48. * can be defined by user or calculated from series. In the case of
  49. * automatically calculated ranges, a 1px margin of error is
  50. * permitted.
  51. *
  52. * @since 7.0.0
  53. * @product highcharts highstock highmaps
  54. * @requires highcharts-more
  55. * @optionparent legend.bubbleLegend
  56. */
  57. bubbleLegend: {
  58. /**
  59. * The color of the ranges borders, can be also defined for an
  60. * individual range.
  61. *
  62. * @sample highcharts/bubble-legend/similartoseries/
  63. * Similar look to the bubble series
  64. * @sample highcharts/bubble-legend/bordercolor/
  65. * Individual bubble border color
  66. *
  67. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
  68. */
  69. borderColor: void 0,
  70. /**
  71. * The width of the ranges borders in pixels, can be also
  72. * defined for an individual range.
  73. */
  74. borderWidth: 2,
  75. /**
  76. * An additional class name to apply to the bubble legend'
  77. * circle graphical elements. This option does not replace
  78. * default class names of the graphical element.
  79. *
  80. * @sample {highcharts} highcharts/css/bubble-legend/
  81. * Styling by CSS
  82. *
  83. * @type {string}
  84. */
  85. className: void 0,
  86. /**
  87. * The main color of the bubble legend. Applies to ranges, if
  88. * individual color is not defined.
  89. *
  90. * @sample highcharts/bubble-legend/similartoseries/
  91. * Similar look to the bubble series
  92. * @sample highcharts/bubble-legend/color/
  93. * Individual bubble color
  94. *
  95. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
  96. */
  97. color: void 0,
  98. /**
  99. * An additional class name to apply to the bubble legend's
  100. * connector graphical elements. This option does not replace
  101. * default class names of the graphical element.
  102. *
  103. * @sample {highcharts} highcharts/css/bubble-legend/
  104. * Styling by CSS
  105. *
  106. * @type {string}
  107. */
  108. connectorClassName: void 0,
  109. /**
  110. * The color of the connector, can be also defined
  111. * for an individual range.
  112. *
  113. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
  114. */
  115. connectorColor: void 0,
  116. /**
  117. * The length of the connectors in pixels. If labels are
  118. * centered, the distance is reduced to 0.
  119. *
  120. * @sample highcharts/bubble-legend/connectorandlabels/
  121. * Increased connector length
  122. */
  123. connectorDistance: 60,
  124. /**
  125. * The width of the connectors in pixels.
  126. *
  127. * @sample highcharts/bubble-legend/connectorandlabels/
  128. * Increased connector width
  129. */
  130. connectorWidth: 1,
  131. /**
  132. * Enable or disable the bubble legend.
  133. */
  134. enabled: false,
  135. /**
  136. * Options for the bubble legend labels.
  137. */
  138. labels: {
  139. /**
  140. * An additional class name to apply to the bubble legend
  141. * label graphical elements. This option does not replace
  142. * default class names of the graphical element.
  143. *
  144. * @sample {highcharts} highcharts/css/bubble-legend/
  145. * Styling by CSS
  146. *
  147. * @type {string}
  148. */
  149. className: void 0,
  150. /**
  151. * Whether to allow data labels to overlap.
  152. */
  153. allowOverlap: false,
  154. /**
  155. * A format string for the bubble legend labels. Available
  156. * variables are the same as for `formatter`.
  157. *
  158. * @sample highcharts/bubble-legend/format/
  159. * Add a unit
  160. *
  161. * @type {string}
  162. */
  163. format: '',
  164. /**
  165. * Available `this` properties are:
  166. *
  167. * - `this.value`: The bubble value.
  168. *
  169. * - `this.radius`: The radius of the bubble range.
  170. *
  171. * - `this.center`: The center y position of the range.
  172. *
  173. * @type {Highcharts.FormatterCallbackFunction<Highcharts.BubbleLegendFormatterContextObject>}
  174. */
  175. formatter: void 0,
  176. /**
  177. * The alignment of the labels compared to the bubble
  178. * legend. Can be one of `left`, `center` or `right`.
  179. *
  180. * @sample highcharts/bubble-legend/connectorandlabels/
  181. * Labels on left
  182. *
  183. * @type {Highcharts.AlignValue}
  184. */
  185. align: 'right',
  186. /**
  187. * CSS styles for the labels.
  188. *
  189. * @type {Highcharts.CSSObject}
  190. */
  191. style: {
  192. /** @ignore-option */
  193. fontSize: '10px',
  194. /** @ignore-option */
  195. color: palette.neutralColor100
  196. },
  197. /**
  198. * The x position offset of the label relative to the
  199. * connector.
  200. */
  201. x: 0,
  202. /**
  203. * The y position offset of the label relative to the
  204. * connector.
  205. */
  206. y: 0
  207. },
  208. /**
  209. * Miximum bubble legend range size. If values for ranges are
  210. * not specified, the `minSize` and the `maxSize` are calculated
  211. * from bubble series.
  212. */
  213. maxSize: 60,
  214. /**
  215. * Minimum bubble legend range size. If values for ranges are
  216. * not specified, the `minSize` and the `maxSize` are calculated
  217. * from bubble series.
  218. */
  219. minSize: 10,
  220. /**
  221. * The position of the bubble legend in the legend.
  222. * @sample highcharts/bubble-legend/connectorandlabels/
  223. * Bubble legend as last item in legend
  224. */
  225. legendIndex: 0,
  226. /**
  227. * Options for specific range. One range consists of bubble,
  228. * label and connector.
  229. *
  230. * @sample highcharts/bubble-legend/ranges/
  231. * Manually defined ranges
  232. * @sample highcharts/bubble-legend/autoranges/
  233. * Auto calculated ranges
  234. *
  235. * @type {Array<*>}
  236. */
  237. ranges: {
  238. /**
  239. * Range size value, similar to bubble Z data.
  240. * @type {number}
  241. */
  242. value: void 0,
  243. /**
  244. * The color of the border for individual range.
  245. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
  246. */
  247. borderColor: void 0,
  248. /**
  249. * The color of the bubble for individual range.
  250. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
  251. */
  252. color: void 0,
  253. /**
  254. * The color of the connector for individual range.
  255. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
  256. */
  257. connectorColor: void 0
  258. },
  259. /**
  260. * Whether the bubble legend range value should be represented
  261. * by the area or the width of the bubble. The default, area,
  262. * corresponds best to the human perception of the size of each
  263. * bubble.
  264. *
  265. * @sample highcharts/bubble-legend/ranges/
  266. * Size by width
  267. *
  268. * @type {Highcharts.BubbleSizeByValue}
  269. */
  270. sizeBy: 'area',
  271. /**
  272. * When this is true, the absolute value of z determines the
  273. * size of the bubble. This means that with the default
  274. * zThreshold of 0, a bubble of value -1 will have the same size
  275. * as a bubble of value 1, while a bubble of value 0 will have a
  276. * smaller size according to minSize.
  277. */
  278. sizeByAbsoluteValue: false,
  279. /**
  280. * Define the visual z index of the bubble legend.
  281. */
  282. zIndex: 1,
  283. /**
  284. * Ranges with with lower value than zThreshold, are skipped.
  285. */
  286. zThreshold: 0
  287. }
  288. }
  289. });
  290. /* eslint-disable no-invalid-this, valid-jsdoc */
  291. /**
  292. * BubbleLegend class.
  293. *
  294. * @private
  295. * @class
  296. * @name Highcharts.BubbleLegend
  297. * @param {Highcharts.LegendBubbleLegendOptions} options
  298. * Bubble legend options
  299. * @param {Highcharts.Legend} legend
  300. * Legend
  301. */
  302. var BubbleLegend = /** @class */ (function () {
  303. function BubbleLegend(options, legend) {
  304. this.chart = void 0;
  305. this.fontMetrics = void 0;
  306. this.legend = void 0;
  307. this.legendGroup = void 0;
  308. this.legendItem = void 0;
  309. this.legendItemHeight = void 0;
  310. this.legendItemWidth = void 0;
  311. this.legendSymbol = void 0;
  312. this.maxLabel = void 0;
  313. this.movementX = void 0;
  314. this.ranges = void 0;
  315. this.visible = void 0;
  316. this.symbols = void 0;
  317. this.options = void 0;
  318. this.setState = noop;
  319. this.init(options, legend);
  320. }
  321. /**
  322. * Create basic bubbleLegend properties similar to item in legend.
  323. *
  324. * @private
  325. * @function Highcharts.BubbleLegend#init
  326. * @param {Highcharts.LegendBubbleLegendOptions} options
  327. * Bubble legend options
  328. * @param {Highcharts.Legend} legend
  329. * Legend
  330. * @return {void}
  331. */
  332. BubbleLegend.prototype.init = function (options, legend) {
  333. this.options = options;
  334. this.visible = true;
  335. this.chart = legend.chart;
  336. this.legend = legend;
  337. };
  338. /**
  339. * Depending on the position option, add bubbleLegend to legend items.
  340. *
  341. * @private
  342. * @function Highcharts.BubbleLegend#addToLegend
  343. * @param {Array<(Highcharts.Point|Highcharts.Series)>}
  344. * All legend items
  345. * @return {void}
  346. */
  347. BubbleLegend.prototype.addToLegend = function (items) {
  348. // Insert bubbleLegend into legend items
  349. items.splice(this.options.legendIndex, 0, this);
  350. };
  351. /**
  352. * Calculate ranges, sizes and call the next steps of bubbleLegend
  353. * creation.
  354. *
  355. * @private
  356. * @function Highcharts.BubbleLegend#drawLegendSymbol
  357. * @param {Highcharts.Legend} legend
  358. * Legend instance
  359. * @return {void}
  360. */
  361. BubbleLegend.prototype.drawLegendSymbol = function (legend) {
  362. var chart = this.chart, options = this.options, size, itemDistance = pick(legend.options.itemDistance, 20), connectorSpace, ranges = options.ranges, radius, maxLabel, connectorDistance = options.connectorDistance;
  363. // Predict label dimensions
  364. this.fontMetrics = chart.renderer.fontMetrics(options.labels.style.fontSize);
  365. // Do not create bubbleLegend now if ranges or ranges valeus are not
  366. // specified or if are empty array.
  367. if (!ranges || !ranges.length || !isNumber(ranges[0].value)) {
  368. legend.options.bubbleLegend.autoRanges = true;
  369. return;
  370. }
  371. // Sort ranges to right render order
  372. stableSort(ranges, function (a, b) {
  373. return b.value - a.value;
  374. });
  375. this.ranges = ranges;
  376. this.setOptions();
  377. this.render();
  378. // Get max label size
  379. maxLabel = this.getMaxLabelSize();
  380. radius = this.ranges[0].radius;
  381. size = radius * 2;
  382. // Space for connectors and labels.
  383. connectorSpace =
  384. connectorDistance - radius + maxLabel.width;
  385. connectorSpace = connectorSpace > 0 ? connectorSpace : 0;
  386. this.maxLabel = maxLabel;
  387. this.movementX = options.labels.align === 'left' ?
  388. connectorSpace : 0;
  389. this.legendItemWidth = size + connectorSpace + itemDistance;
  390. this.legendItemHeight = size + this.fontMetrics.h / 2;
  391. };
  392. /**
  393. * Set style options for each bubbleLegend range.
  394. *
  395. * @private
  396. * @function Highcharts.BubbleLegend#setOptions
  397. * @return {void}
  398. */
  399. BubbleLegend.prototype.setOptions = function () {
  400. var ranges = this.ranges, options = this.options, series = this.chart.series[options.seriesIndex], baseline = this.legend.baseline, bubbleAttribs = {
  401. zIndex: options.zIndex,
  402. 'stroke-width': options.borderWidth
  403. }, connectorAttribs = {
  404. zIndex: options.zIndex,
  405. 'stroke-width': options.connectorWidth
  406. }, labelAttribs = {
  407. align: (this.legend.options.rtl ||
  408. options.labels.align === 'left') ? 'right' : 'left',
  409. zIndex: options.zIndex
  410. }, fillOpacity = series.options.marker.fillOpacity, styledMode = this.chart.styledMode;
  411. // Allow to parts of styles be used individually for range
  412. ranges.forEach(function (range, i) {
  413. if (!styledMode) {
  414. bubbleAttribs.stroke = pick(range.borderColor, options.borderColor, series.color);
  415. bubbleAttribs.fill = pick(range.color, options.color, fillOpacity !== 1 ?
  416. color(series.color).setOpacity(fillOpacity)
  417. .get('rgba') :
  418. series.color);
  419. connectorAttribs.stroke = pick(range.connectorColor, options.connectorColor, series.color);
  420. }
  421. // Set options needed for rendering each range
  422. ranges[i].radius = this.getRangeRadius(range.value);
  423. ranges[i] = merge(ranges[i], {
  424. center: (ranges[0].radius - ranges[i].radius +
  425. baseline)
  426. });
  427. if (!styledMode) {
  428. merge(true, ranges[i], {
  429. bubbleAttribs: merge(bubbleAttribs),
  430. connectorAttribs: merge(connectorAttribs),
  431. labelAttribs: labelAttribs
  432. });
  433. }
  434. }, this);
  435. };
  436. /**
  437. * Calculate radius for each bubble range,
  438. * used code from BubbleSeries.js 'getRadius' method.
  439. *
  440. * @private
  441. * @function Highcharts.BubbleLegend#getRangeRadius
  442. * @param {number} value
  443. * Range value
  444. * @return {number|null}
  445. * Radius for one range
  446. */
  447. BubbleLegend.prototype.getRangeRadius = function (value) {
  448. var options = this.options, seriesIndex = this.options.seriesIndex, bubbleSeries = this.chart.series[seriesIndex], zMax = options.ranges[0].value, zMin = options.ranges[options.ranges.length - 1].value, minSize = options.minSize, maxSize = options.maxSize;
  449. return bubbleSeries.getRadius.call(this, zMin, zMax, minSize, maxSize, value);
  450. };
  451. /**
  452. * Render the legendSymbol group.
  453. *
  454. * @private
  455. * @function Highcharts.BubbleLegend#render
  456. * @return {void}
  457. */
  458. BubbleLegend.prototype.render = function () {
  459. var renderer = this.chart.renderer, zThreshold = this.options.zThreshold;
  460. if (!this.symbols) {
  461. this.symbols = {
  462. connectors: [],
  463. bubbleItems: [],
  464. labels: []
  465. };
  466. }
  467. // Nesting SVG groups to enable handleOverflow
  468. this.legendSymbol = renderer.g('bubble-legend');
  469. this.legendItem = renderer.g('bubble-legend-item');
  470. // To enable default 'hideOverlappingLabels' method
  471. this.legendSymbol.translateX = 0;
  472. this.legendSymbol.translateY = 0;
  473. this.ranges.forEach(function (range) {
  474. if (range.value >= zThreshold) {
  475. this.renderRange(range);
  476. }
  477. }, this);
  478. // To use handleOverflow method
  479. this.legendSymbol.add(this.legendItem);
  480. this.legendItem.add(this.legendGroup);
  481. this.hideOverlappingLabels();
  482. };
  483. /**
  484. * Render one range, consisting of bubble symbol, connector and label.
  485. *
  486. * @private
  487. * @function Highcharts.BubbleLegend#renderRange
  488. * @param {Highcharts.LegendBubbleLegendRangesOptions} range
  489. * Range options
  490. * @return {void}
  491. */
  492. BubbleLegend.prototype.renderRange = function (range) {
  493. var mainRange = this.ranges[0], legend = this.legend, options = this.options, labelsOptions = options.labels, chart = this.chart, bubbleSeries = chart.series[options.seriesIndex], renderer = chart.renderer, symbols = this.symbols, labels = symbols.labels, label, elementCenter = range.center, absoluteRadius = Math.abs(range.radius), connectorDistance = options.connectorDistance || 0, labelsAlign = labelsOptions.align, rtl = legend.options.rtl, connectorLength = rtl || labelsAlign === 'left' ?
  494. -connectorDistance : connectorDistance, borderWidth = options.borderWidth, connectorWidth = options.connectorWidth, posX = mainRange.radius || 0, posY = elementCenter - absoluteRadius -
  495. borderWidth / 2 + connectorWidth / 2, labelY, labelX, fontMetrics = this.fontMetrics, labelMovement = fontMetrics.f / 2 -
  496. (fontMetrics.h - fontMetrics.f) / 2, crispMovement = (posY % 1 ? 1 : 0.5) -
  497. (connectorWidth % 2 ? 0 : 0.5), styledMode = renderer.styledMode;
  498. // Set options for centered labels
  499. if (labelsAlign === 'center') {
  500. connectorLength = 0; // do not use connector
  501. options.connectorDistance = 0;
  502. range.labelAttribs.align = 'center';
  503. }
  504. labelY = posY + options.labels.y;
  505. labelX = posX + connectorLength + options.labels.x;
  506. // Render bubble symbol
  507. symbols.bubbleItems.push(renderer
  508. .circle(posX, elementCenter + crispMovement, absoluteRadius)
  509. .attr(styledMode ? {} : range.bubbleAttribs)
  510. .addClass((styledMode ?
  511. 'highcharts-color-' +
  512. bubbleSeries.colorIndex + ' ' :
  513. '') +
  514. 'highcharts-bubble-legend-symbol ' +
  515. (options.className || '')).add(this.legendSymbol));
  516. // Render connector
  517. symbols.connectors.push(renderer
  518. .path(renderer.crispLine([
  519. ['M', posX, posY],
  520. ['L', posX + connectorLength, posY]
  521. ], options.connectorWidth))
  522. .attr((styledMode ? {} : range.connectorAttribs))
  523. .addClass((styledMode ?
  524. 'highcharts-color-' +
  525. this.options.seriesIndex + ' ' : '') +
  526. 'highcharts-bubble-legend-connectors ' +
  527. (options.connectorClassName || '')).add(this.legendSymbol));
  528. // Render label
  529. label = renderer
  530. .text(this.formatLabel(range), labelX, labelY + labelMovement)
  531. .attr((styledMode ? {} : range.labelAttribs))
  532. .css(styledMode ? {} : labelsOptions.style)
  533. .addClass('highcharts-bubble-legend-labels ' +
  534. (options.labels.className || '')).add(this.legendSymbol);
  535. labels.push(label);
  536. // To enable default 'hideOverlappingLabels' method
  537. label.placed = true;
  538. label.alignAttr = {
  539. x: labelX,
  540. y: labelY + labelMovement
  541. };
  542. };
  543. /**
  544. * Get the label which takes up the most space.
  545. *
  546. * @private
  547. * @function Highcharts.BubbleLegend#getMaxLabelSize
  548. * @return {Highcharts.BBoxObject}
  549. */
  550. BubbleLegend.prototype.getMaxLabelSize = function () {
  551. var labels = this.symbols.labels, maxLabel, labelSize;
  552. labels.forEach(function (label) {
  553. labelSize = label.getBBox(true);
  554. if (maxLabel) {
  555. maxLabel = labelSize.width > maxLabel.width ?
  556. labelSize : maxLabel;
  557. }
  558. else {
  559. maxLabel = labelSize;
  560. }
  561. });
  562. return maxLabel || {};
  563. };
  564. /**
  565. * Get formatted label for range.
  566. *
  567. * @private
  568. * @function Highcharts.BubbleLegend#formatLabel
  569. * @param {Highcharts.LegendBubbleLegendRangesOptions} range
  570. * Range options
  571. * @return {string}
  572. * Range label text
  573. */
  574. BubbleLegend.prototype.formatLabel = function (range) {
  575. var options = this.options, formatter = options.labels.formatter, format = options.labels.format;
  576. var numberFormatter = this.chart.numberFormatter;
  577. return format ? F.format(format, range) :
  578. formatter ? formatter.call(range) :
  579. numberFormatter(range.value, 1);
  580. };
  581. /**
  582. * By using default chart 'hideOverlappingLabels' method, hide or show
  583. * labels and connectors.
  584. *
  585. * @private
  586. * @function Highcharts.BubbleLegend#hideOverlappingLabels
  587. * @return {void}
  588. */
  589. BubbleLegend.prototype.hideOverlappingLabels = function () {
  590. var chart = this.chart, allowOverlap = this.options.labels.allowOverlap, symbols = this.symbols;
  591. if (!allowOverlap && symbols) {
  592. chart.hideOverlappingLabels(symbols.labels);
  593. // Hide or show connectors
  594. symbols.labels.forEach(function (label, index) {
  595. if (!label.newOpacity) {
  596. symbols.connectors[index].hide();
  597. }
  598. else if (label.newOpacity !== label.oldOpacity) {
  599. symbols.connectors[index].show();
  600. }
  601. });
  602. }
  603. };
  604. /**
  605. * Calculate ranges from created series.
  606. *
  607. * @private
  608. * @function Highcharts.BubbleLegend#getRanges
  609. * @return {Array<Highcharts.LegendBubbleLegendRangesOptions>}
  610. * Array of range objects
  611. */
  612. BubbleLegend.prototype.getRanges = function () {
  613. var bubbleLegend = this.legend.bubbleLegend, series = bubbleLegend.chart.series, ranges, rangesOptions = bubbleLegend.options.ranges, zData, minZ = Number.MAX_VALUE, maxZ = -Number.MAX_VALUE;
  614. series.forEach(function (s) {
  615. // Find the min and max Z, like in bubble series
  616. if (s.isBubble && !s.ignoreSeries) {
  617. zData = s.zData.filter(isNumber);
  618. if (zData.length) {
  619. minZ = pick(s.options.zMin, Math.min(minZ, Math.max(arrayMin(zData), s.options.displayNegative === false ?
  620. s.options.zThreshold :
  621. -Number.MAX_VALUE)));
  622. maxZ = pick(s.options.zMax, Math.max(maxZ, arrayMax(zData)));
  623. }
  624. }
  625. });
  626. // Set values for ranges
  627. if (minZ === maxZ) {
  628. // Only one range if min and max values are the same.
  629. ranges = [{ value: maxZ }];
  630. }
  631. else {
  632. ranges = [
  633. { value: minZ },
  634. { value: (minZ + maxZ) / 2 },
  635. { value: maxZ, autoRanges: true }
  636. ];
  637. }
  638. // Prevent reverse order of ranges after redraw
  639. if (rangesOptions.length && rangesOptions[0].radius) {
  640. ranges.reverse();
  641. }
  642. // Merge ranges values with user options
  643. ranges.forEach(function (range, i) {
  644. if (rangesOptions && rangesOptions[i]) {
  645. ranges[i] = merge(rangesOptions[i], range);
  646. }
  647. });
  648. return ranges;
  649. };
  650. /**
  651. * Calculate bubble legend sizes from rendered series.
  652. *
  653. * @private
  654. * @function Highcharts.BubbleLegend#predictBubbleSizes
  655. * @return {Array<number,number>}
  656. * Calculated min and max bubble sizes
  657. */
  658. BubbleLegend.prototype.predictBubbleSizes = function () {
  659. var chart = this.chart, fontMetrics = this.fontMetrics, legendOptions = chart.legend.options, floating = legendOptions.floating, horizontal = legendOptions.layout === 'horizontal', lastLineHeight = horizontal ? chart.legend.lastLineHeight : 0, plotSizeX = chart.plotSizeX, plotSizeY = chart.plotSizeY, bubbleSeries = chart.series[this.options.seriesIndex], minSize = Math.ceil(bubbleSeries.minPxSize), maxPxSize = Math.ceil(bubbleSeries.maxPxSize), maxSize = bubbleSeries.options.maxSize, plotSize = Math.min(plotSizeY, plotSizeX), calculatedSize;
  660. // Calculate prediceted max size of bubble
  661. if (floating || !(/%$/.test(maxSize))) {
  662. calculatedSize = maxPxSize;
  663. }
  664. else {
  665. maxSize = parseFloat(maxSize);
  666. calculatedSize = ((plotSize + lastLineHeight -
  667. fontMetrics.h / 2) * maxSize / 100) / (maxSize / 100 + 1);
  668. // Get maxPxSize from bubble series if calculated bubble legend
  669. // size will not affect to bubbles series.
  670. if ((horizontal && plotSizeY - calculatedSize >=
  671. plotSizeX) || (!horizontal && plotSizeX -
  672. calculatedSize >= plotSizeY)) {
  673. calculatedSize = maxPxSize;
  674. }
  675. }
  676. return [minSize, Math.ceil(calculatedSize)];
  677. };
  678. /**
  679. * Correct ranges with calculated sizes.
  680. *
  681. * @private
  682. * @function Highcharts.BubbleLegend#updateRanges
  683. * @param {number} min
  684. * @param {number} max
  685. * @return {void}
  686. */
  687. BubbleLegend.prototype.updateRanges = function (min, max) {
  688. var bubbleLegendOptions = this.legend.options.bubbleLegend;
  689. bubbleLegendOptions.minSize = min;
  690. bubbleLegendOptions.maxSize = max;
  691. bubbleLegendOptions.ranges = this.getRanges();
  692. };
  693. /**
  694. * Because of the possibility of creating another legend line, predicted
  695. * bubble legend sizes may differ by a few pixels, so it is necessary to
  696. * correct them.
  697. *
  698. * @private
  699. * @function Highcharts.BubbleLegend#correctSizes
  700. * @return {void}
  701. */
  702. BubbleLegend.prototype.correctSizes = function () {
  703. var legend = this.legend, chart = this.chart, bubbleSeries = chart.series[this.options.seriesIndex], bubbleSeriesSize = bubbleSeries.maxPxSize, bubbleLegendSize = this.options.maxSize;
  704. if (Math.abs(Math.ceil(bubbleSeriesSize) - bubbleLegendSize) >
  705. 1) {
  706. this.updateRanges(this.options.minSize, bubbleSeries.maxPxSize);
  707. legend.render();
  708. }
  709. };
  710. return BubbleLegend;
  711. }());
  712. // Start the bubble legend creation process.
  713. addEvent(Legend, 'afterGetAllItems', function (e) {
  714. var legend = this, bubbleLegend = legend.bubbleLegend, legendOptions = legend.options, options = legendOptions.bubbleLegend, bubbleSeriesIndex = legend.chart.getVisibleBubbleSeriesIndex();
  715. // Remove unnecessary element
  716. if (bubbleLegend && bubbleLegend.ranges && bubbleLegend.ranges.length) {
  717. // Allow change the way of calculating ranges in update
  718. if (options.ranges.length) {
  719. options.autoRanges =
  720. !!options.ranges[0].autoRanges;
  721. }
  722. // Update bubbleLegend dimensions in each redraw
  723. legend.destroyItem(bubbleLegend);
  724. }
  725. // Create bubble legend
  726. if (bubbleSeriesIndex >= 0 &&
  727. legendOptions.enabled &&
  728. options.enabled) {
  729. options.seriesIndex = bubbleSeriesIndex;
  730. legend.bubbleLegend = new H.BubbleLegend(options, legend);
  731. legend.bubbleLegend.addToLegend(e.allItems);
  732. }
  733. });
  734. /**
  735. * Check if there is at least one visible bubble series.
  736. *
  737. * @private
  738. * @function Highcharts.Chart#getVisibleBubbleSeriesIndex
  739. * @return {number}
  740. * First visible bubble series index
  741. */
  742. Chart.prototype.getVisibleBubbleSeriesIndex = function () {
  743. var series = this.series, i = 0;
  744. while (i < series.length) {
  745. if (series[i] &&
  746. series[i].isBubble &&
  747. series[i].visible &&
  748. series[i].zData.length) {
  749. return i;
  750. }
  751. i++;
  752. }
  753. return -1;
  754. };
  755. /**
  756. * Calculate height for each row in legend.
  757. *
  758. * @private
  759. * @function Highcharts.Legend#getLinesHeights
  760. * @return {Array<Highcharts.Dictionary<number>>}
  761. * Informations about line height and items amount
  762. */
  763. Legend.prototype.getLinesHeights = function () {
  764. var items = this.allItems, lines = [], lastLine, length = items.length, i = 0, j = 0;
  765. for (i = 0; i < length; i++) {
  766. if (items[i].legendItemHeight) {
  767. // for bubbleLegend
  768. items[i].itemHeight = items[i].legendItemHeight;
  769. }
  770. if ( // Line break
  771. items[i] === items[length - 1] ||
  772. items[i + 1] &&
  773. items[i]._legendItemPos[1] !==
  774. items[i + 1]._legendItemPos[1]) {
  775. lines.push({ height: 0 });
  776. lastLine = lines[lines.length - 1];
  777. // Find the highest item in line
  778. for (j; j <= i; j++) {
  779. if (items[j].itemHeight > lastLine.height) {
  780. lastLine.height = items[j].itemHeight;
  781. }
  782. }
  783. lastLine.step = i;
  784. }
  785. }
  786. return lines;
  787. };
  788. /**
  789. * Correct legend items translation in case of different elements heights.
  790. *
  791. * @private
  792. * @function Highcharts.Legend#retranslateItems
  793. * @param {Array<Highcharts.Dictionary<number>>} lines
  794. * Informations about line height and items amount
  795. * @return {void}
  796. */
  797. Legend.prototype.retranslateItems = function (lines) {
  798. var items = this.allItems, orgTranslateX, orgTranslateY, movementX, rtl = this.options.rtl, actualLine = 0;
  799. items.forEach(function (item, index) {
  800. orgTranslateX = item.legendGroup.translateX;
  801. orgTranslateY = item._legendItemPos[1];
  802. movementX = item.movementX;
  803. if (movementX || (rtl && item.ranges)) {
  804. movementX = rtl ?
  805. orgTranslateX - item.options.maxSize / 2 :
  806. orgTranslateX + movementX;
  807. item.legendGroup.attr({ translateX: movementX });
  808. }
  809. if (index > lines[actualLine].step) {
  810. actualLine++;
  811. }
  812. item.legendGroup.attr({
  813. translateY: Math.round(orgTranslateY + lines[actualLine].height / 2)
  814. });
  815. item._legendItemPos[1] = orgTranslateY +
  816. lines[actualLine].height / 2;
  817. });
  818. };
  819. // Toggle bubble legend depending on the visible status of bubble series.
  820. addEvent(Series, 'legendItemClick', function () {
  821. var series = this, chart = series.chart, visible = series.visible, legend = series.chart.legend, status;
  822. if (legend && legend.bubbleLegend) {
  823. // Temporary correct 'visible' property
  824. series.visible = !visible;
  825. // Save future status for getRanges method
  826. series.ignoreSeries = visible;
  827. // Check if at lest one bubble series is visible
  828. status = chart.getVisibleBubbleSeriesIndex() >= 0;
  829. // Hide bubble legend if all bubble series are disabled
  830. if (legend.bubbleLegend.visible !== status) {
  831. // Show or hide bubble legend
  832. legend.update({
  833. bubbleLegend: { enabled: status }
  834. });
  835. legend.bubbleLegend.visible = status; // Restore default status
  836. }
  837. series.visible = visible;
  838. }
  839. });
  840. // If ranges are not specified, determine ranges from rendered bubble series
  841. // and render legend again.
  842. wrap(Chart.prototype, 'drawChartBox', function (proceed, options, callback) {
  843. var chart = this, legend = chart.legend, bubbleSeries = chart.getVisibleBubbleSeriesIndex() >= 0, bubbleLegendOptions, bubbleSizes;
  844. if (legend && legend.options.enabled && legend.bubbleLegend &&
  845. legend.options.bubbleLegend.autoRanges && bubbleSeries) {
  846. bubbleLegendOptions = legend.bubbleLegend.options;
  847. bubbleSizes = legend.bubbleLegend.predictBubbleSizes();
  848. legend.bubbleLegend.updateRanges(bubbleSizes[0], bubbleSizes[1]);
  849. // Disable animation on init
  850. if (!bubbleLegendOptions.placed) {
  851. legend.group.placed = false;
  852. legend.allItems.forEach(function (item) {
  853. item.legendGroup.translateY = null;
  854. });
  855. }
  856. // Create legend with bubbleLegend
  857. legend.render();
  858. chart.getMargins();
  859. chart.axes.forEach(function (axis) {
  860. if (axis.visible) { // #11448
  861. axis.render();
  862. }
  863. if (!bubbleLegendOptions.placed) {
  864. axis.setScale();
  865. axis.updateNames();
  866. // Disable axis animation on init
  867. objectEach(axis.ticks, function (tick) {
  868. tick.isNew = true;
  869. tick.isNewLabel = true;
  870. });
  871. }
  872. });
  873. bubbleLegendOptions.placed = true;
  874. // After recalculate axes, calculate margins again.
  875. chart.getMargins();
  876. // Call default 'drawChartBox' method.
  877. proceed.call(chart, options, callback);
  878. // Check bubble legend sizes and correct them if necessary.
  879. legend.bubbleLegend.correctSizes();
  880. // Correct items positions with different dimensions in legend.
  881. legend.retranslateItems(legend.getLinesHeights());
  882. }
  883. else {
  884. proceed.call(chart, options, callback);
  885. // Allow color change on static bubble legend after click on legend
  886. if (legend && legend.options.enabled && legend.bubbleLegend) {
  887. legend.render();
  888. legend.retranslateItems(legend.getLinesHeights());
  889. }
  890. }
  891. });
  892. H.BubbleLegend = BubbleLegend;
  893. export default H.BubbleLegend;