BubbleLegend.js 37 KB

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