BubbleLegend.js 36 KB

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