BubbleSeries.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. /* *
  2. * (c) 2010-2019 Torstein Honsi
  3. *
  4. * License: www.highcharts.com/license
  5. */
  6. 'use strict';
  7. import H from '../parts/Globals.js';
  8. import '../parts/Utilities.js';
  9. import '../parts/Axis.js';
  10. import '../parts/Color.js';
  11. import '../parts/Point.js';
  12. import '../parts/Series.js';
  13. import '../parts/ScatterSeries.js';
  14. import './BubbleLegend.js';
  15. var arrayMax = H.arrayMax,
  16. arrayMin = H.arrayMin,
  17. Axis = H.Axis,
  18. color = H.color,
  19. isNumber = H.isNumber,
  20. noop = H.noop,
  21. pick = H.pick,
  22. pInt = H.pInt,
  23. Point = H.Point,
  24. Series = H.Series,
  25. seriesType = H.seriesType,
  26. seriesTypes = H.seriesTypes;
  27. /**
  28. * A bubble series is a three dimensional series type where each point renders
  29. * an X, Y and Z value. Each points is drawn as a bubble where the position
  30. * along the X and Y axes mark the X and Y values, and the size of the bubble
  31. * relates to the Z value. Requires `highcharts-more.js`.
  32. *
  33. * @sample {highcharts} highcharts/demo/bubble/
  34. * Bubble chart
  35. *
  36. * @extends plotOptions.scatter
  37. * @product highcharts highstock
  38. * @optionparent plotOptions.bubble
  39. */
  40. seriesType('bubble', 'scatter', {
  41. dataLabels: {
  42. formatter: function () { // #2945
  43. return this.point.z;
  44. },
  45. inside: true,
  46. verticalAlign: 'middle'
  47. },
  48. /**
  49. * If there are more points in the series than the `animationLimit`, the
  50. * animation won't run. Animation affects overall performance and doesn't
  51. * work well with heavy data series.
  52. *
  53. * @since 6.1.0
  54. */
  55. animationLimit: 250,
  56. /**
  57. * Whether to display negative sized bubbles. The threshold is given
  58. * by the [zThreshold](#plotOptions.bubble.zThreshold) option, and negative
  59. * bubbles can be visualized by setting
  60. * [negativeColor](#plotOptions.bubble.negativeColor).
  61. *
  62. * @sample {highcharts} highcharts/plotoptions/bubble-negative/
  63. * Negative bubbles
  64. *
  65. * @type {boolean}
  66. * @default true
  67. * @since 3.0
  68. * @apioption plotOptions.bubble.displayNegative
  69. */
  70. /**
  71. * @extends plotOptions.series.marker
  72. * @excluding enabled, enabledThreshold, height, radius, width
  73. */
  74. marker: {
  75. lineColor: null, // inherit from series.color
  76. lineWidth: 1,
  77. /**
  78. * The fill opacity of the bubble markers.
  79. */
  80. fillOpacity: 0.5,
  81. /**
  82. * In bubble charts, the radius is overridden and determined based on
  83. * the point's data value.
  84. *
  85. * @ignore
  86. */
  87. radius: null,
  88. states: {
  89. hover: {
  90. radiusPlus: 0
  91. }
  92. },
  93. /**
  94. * A predefined shape or symbol for the marker. Possible values are
  95. * "circle", "square", "diamond", "triangle" and "triangle-down".
  96. *
  97. * Additionally, the URL to a graphic can be given on the form
  98. * `url(graphic.png)`. Note that for the image to be applied to exported
  99. * charts, its URL needs to be accessible by the export server.
  100. *
  101. * Custom callbacks for symbol path generation can also be added to
  102. * `Highcharts.SVGRenderer.prototype.symbols`. The callback is then
  103. * used by its method name, as shown in the demo.
  104. *
  105. * @sample {highcharts} highcharts/plotoptions/bubble-symbol/
  106. * Bubble chart with various symbols
  107. * @sample {highcharts} highcharts/plotoptions/series-marker-symbol/
  108. * General chart with predefined, graphic and custom markers
  109. *
  110. * @since 5.0.11
  111. * @validvalue ["circle", "square", "diamond", "triangle",
  112. * "triangle-down"]
  113. */
  114. symbol: 'circle'
  115. },
  116. /**
  117. * Minimum bubble size. Bubbles will automatically size between the
  118. * `minSize` and `maxSize` to reflect the `z` value of each bubble.
  119. * Can be either pixels (when no unit is given), or a percentage of
  120. * the smallest one of the plot width and height.
  121. *
  122. * @sample {highcharts} highcharts/plotoptions/bubble-size/
  123. * Bubble size
  124. *
  125. * @type {number|string}
  126. * @since 3.0
  127. * @product highcharts highstock
  128. */
  129. minSize: 8,
  130. /**
  131. * Maximum bubble size. Bubbles will automatically size between the
  132. * `minSize` and `maxSize` to reflect the `z` value of each bubble.
  133. * Can be either pixels (when no unit is given), or a percentage of
  134. * the smallest one of the plot width and height.
  135. *
  136. * @sample {highcharts} highcharts/plotoptions/bubble-size/
  137. * Bubble size
  138. *
  139. * @type {number|string}
  140. * @since 3.0
  141. * @product highcharts highstock
  142. */
  143. maxSize: '20%',
  144. /**
  145. * When a point's Z value is below the
  146. * [zThreshold](#plotOptions.bubble.zThreshold) setting, this color is used.
  147. *
  148. * @sample {highcharts} highcharts/plotoptions/bubble-negative/
  149. * Negative bubbles
  150. *
  151. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
  152. * @since 3.0
  153. * @product highcharts
  154. * @apioption plotOptions.bubble.negativeColor
  155. */
  156. /**
  157. * Whether the bubble's value should be represented by the area or the
  158. * width of the bubble. The default, `area`, corresponds best to the
  159. * human perception of the size of each bubble.
  160. *
  161. * @sample {highcharts} highcharts/plotoptions/bubble-sizeby/
  162. * Comparison of area and size
  163. *
  164. * @type {string}
  165. * @default area
  166. * @since 3.0.7
  167. * @validvalue ["area", "width"]
  168. * @apioption plotOptions.bubble.sizeBy
  169. */
  170. /**
  171. * When this is true, the absolute value of z determines the size of
  172. * the bubble. This means that with the default `zThreshold` of 0, a
  173. * bubble of value -1 will have the same size as a bubble of value 1,
  174. * while a bubble of value 0 will have a smaller size according to
  175. * `minSize`.
  176. *
  177. * @sample {highcharts} highcharts/plotoptions/bubble-sizebyabsolutevalue/
  178. * Size by absolute value, various thresholds
  179. *
  180. * @type {boolean}
  181. * @default false
  182. * @since 4.1.9
  183. * @product highcharts
  184. * @apioption plotOptions.bubble.sizeByAbsoluteValue
  185. */
  186. /**
  187. * When this is true, the series will not cause the Y axis to cross
  188. * the zero plane (or [threshold](#plotOptions.series.threshold) option)
  189. * unless the data actually crosses the plane.
  190. *
  191. * For example, if `softThreshold` is `false`, a series of 0, 1, 2,
  192. * 3 will make the Y axis show negative values according to the `minPadding`
  193. * option. If `softThreshold` is `true`, the Y axis starts at 0.
  194. *
  195. * @since 4.1.9
  196. * @product highcharts
  197. */
  198. softThreshold: false,
  199. states: {
  200. hover: {
  201. halo: {
  202. size: 5
  203. }
  204. }
  205. },
  206. tooltip: {
  207. pointFormat: '({point.x}, {point.y}), Size: {point.z}'
  208. },
  209. turboThreshold: 0,
  210. /**
  211. * The minimum for the Z value range. Defaults to the highest Z value
  212. * in the data.
  213. *
  214. * @see [zMin](#plotOptions.bubble.zMin)
  215. *
  216. * @sample {highcharts} highcharts/plotoptions/bubble-zmin-zmax/
  217. * Z has a possible range of 0-100
  218. *
  219. * @type {number}
  220. * @since 4.0.3
  221. * @product highcharts
  222. * @apioption plotOptions.bubble.zMax
  223. */
  224. /**
  225. * The minimum for the Z value range. Defaults to the lowest Z value
  226. * in the data.
  227. *
  228. * @see [zMax](#plotOptions.bubble.zMax)
  229. *
  230. * @sample {highcharts} highcharts/plotoptions/bubble-zmin-zmax/
  231. * Z has a possible range of 0-100
  232. *
  233. * @type {number}
  234. * @since 4.0.3
  235. * @product highcharts
  236. * @apioption plotOptions.bubble.zMin
  237. */
  238. /**
  239. * When [displayNegative](#plotOptions.bubble.displayNegative) is `false`,
  240. * bubbles with lower Z values are skipped. When `displayNegative`
  241. * is `true` and a [negativeColor](#plotOptions.bubble.negativeColor)
  242. * is given, points with lower Z is colored.
  243. *
  244. * @sample {highcharts} highcharts/plotoptions/bubble-negative/
  245. * Negative bubbles
  246. *
  247. * @since 3.0
  248. * @product highcharts
  249. */
  250. zThreshold: 0,
  251. zoneAxis: 'z'
  252. // Prototype members
  253. }, {
  254. pointArrayMap: ['y', 'z'],
  255. parallelArrays: ['x', 'y', 'z'],
  256. trackerGroups: ['group', 'dataLabelsGroup'],
  257. specialGroup: 'group', // To allow clipping (#6296)
  258. bubblePadding: true,
  259. zoneAxis: 'z',
  260. directTouch: true,
  261. isBubble: true,
  262. pointAttribs: function (point, state) {
  263. var markerOptions = this.options.marker,
  264. fillOpacity = markerOptions.fillOpacity,
  265. attr = Series.prototype.pointAttribs.call(this, point, state);
  266. if (fillOpacity !== 1) {
  267. attr.fill = color(attr.fill).setOpacity(fillOpacity).get('rgba');
  268. }
  269. return attr;
  270. },
  271. // Get the radius for each point based on the minSize, maxSize and each
  272. // point's Z value. This must be done prior to Series.translate because
  273. // the axis needs to add padding in accordance with the point sizes.
  274. getRadii: function (zMin, zMax, series) {
  275. var len,
  276. i,
  277. zData = this.zData,
  278. minSize = series.minPxSize,
  279. maxSize = series.maxPxSize,
  280. radii = [],
  281. value;
  282. // Set the shape type and arguments to be picked up in drawPoints
  283. for (i = 0, len = zData.length; i < len; i++) {
  284. value = zData[i];
  285. // Separate method to get individual radius for bubbleLegend
  286. radii.push(this.getRadius(zMin, zMax, minSize, maxSize, value));
  287. }
  288. this.radii = radii;
  289. },
  290. // Get the individual radius for one point.
  291. getRadius: function (zMin, zMax, minSize, maxSize, value) {
  292. var options = this.options,
  293. sizeByArea = options.sizeBy !== 'width',
  294. zThreshold = options.zThreshold,
  295. pos,
  296. zRange = zMax - zMin,
  297. radius;
  298. // When sizing by threshold, the absolute value of z determines
  299. // the size of the bubble.
  300. if (options.sizeByAbsoluteValue && value !== null) {
  301. value = Math.abs(value - zThreshold);
  302. zMax = zRange = Math.max(
  303. zMax - zThreshold,
  304. Math.abs(zMin - zThreshold)
  305. );
  306. zMin = 0;
  307. }
  308. if (!isNumber(value)) {
  309. radius = null;
  310. // Issue #4419 - if value is less than zMin, push a radius that's
  311. // always smaller than the minimum size
  312. } else if (value < zMin) {
  313. radius = minSize / 2 - 1;
  314. } else {
  315. // Relative size, a number between 0 and 1
  316. pos = zRange > 0 ? (value - zMin) / zRange : 0.5;
  317. if (sizeByArea && pos >= 0) {
  318. pos = Math.sqrt(pos);
  319. }
  320. radius = Math.ceil(minSize + pos * (maxSize - minSize)) / 2;
  321. }
  322. return radius;
  323. },
  324. // Perform animation on the bubbles
  325. animate: function (init) {
  326. if (
  327. !init &&
  328. this.points.length < this.options.animationLimit // #8099
  329. ) {
  330. this.points.forEach(function (point) {
  331. var graphic = point.graphic,
  332. animationTarget;
  333. if (graphic && graphic.width) { // URL symbols don't have width
  334. animationTarget = {
  335. x: graphic.x,
  336. y: graphic.y,
  337. width: graphic.width,
  338. height: graphic.height
  339. };
  340. // Start values
  341. graphic.attr({
  342. x: point.plotX,
  343. y: point.plotY,
  344. width: 1,
  345. height: 1
  346. });
  347. // Run animation
  348. graphic.animate(animationTarget, this.options.animation);
  349. }
  350. }, this);
  351. // delete this function to allow it only once
  352. this.animate = null;
  353. }
  354. },
  355. // Extend the base translate method to handle bubble size
  356. translate: function () {
  357. var i,
  358. data = this.data,
  359. point,
  360. radius,
  361. radii = this.radii;
  362. // Run the parent method
  363. seriesTypes.scatter.prototype.translate.call(this);
  364. // Set the shape type and arguments to be picked up in drawPoints
  365. i = data.length;
  366. while (i--) {
  367. point = data[i];
  368. radius = radii ? radii[i] : 0; // #1737
  369. if (isNumber(radius) && radius >= this.minPxSize / 2) {
  370. // Shape arguments
  371. point.marker = H.extend(point.marker, {
  372. radius: radius,
  373. width: 2 * radius,
  374. height: 2 * radius
  375. });
  376. // Alignment box for the data label
  377. point.dlBox = {
  378. x: point.plotX - radius,
  379. y: point.plotY - radius,
  380. width: 2 * radius,
  381. height: 2 * radius
  382. };
  383. } else { // below zThreshold
  384. // #1691
  385. point.shapeArgs = point.plotY = point.dlBox = undefined;
  386. }
  387. }
  388. },
  389. alignDataLabel: seriesTypes.column.prototype.alignDataLabel,
  390. buildKDTree: noop,
  391. applyZones: noop
  392. // Point class
  393. }, {
  394. haloPath: function (size) {
  395. return Point.prototype.haloPath.call(
  396. this,
  397. // #6067
  398. size === 0 ? 0 : (this.marker ? this.marker.radius || 0 : 0) + size
  399. );
  400. },
  401. ttBelow: false
  402. });
  403. // Add logic to pad each axis with the amount of pixels necessary to avoid the
  404. // bubbles to overflow.
  405. Axis.prototype.beforePadding = function () {
  406. var axis = this,
  407. axisLength = this.len,
  408. chart = this.chart,
  409. pxMin = 0,
  410. pxMax = axisLength,
  411. isXAxis = this.isXAxis,
  412. dataKey = isXAxis ? 'xData' : 'yData',
  413. min = this.min,
  414. extremes = {},
  415. smallestSize = Math.min(chart.plotWidth, chart.plotHeight),
  416. zMin = Number.MAX_VALUE,
  417. zMax = -Number.MAX_VALUE,
  418. range = this.max - min,
  419. transA = axisLength / range,
  420. activeSeries = [];
  421. // Handle padding on the second pass, or on redraw
  422. this.series.forEach(function (series) {
  423. var seriesOptions = series.options,
  424. zData;
  425. if (
  426. series.bubblePadding &&
  427. (series.visible || !chart.options.chart.ignoreHiddenSeries)
  428. ) {
  429. // Correction for #1673
  430. axis.allowZoomOutside = true;
  431. // Cache it
  432. activeSeries.push(series);
  433. if (isXAxis) { // because X axis is evaluated first
  434. // For each series, translate the size extremes to pixel values
  435. ['minSize', 'maxSize'].forEach(function (prop) {
  436. var length = seriesOptions[prop],
  437. isPercent = /%$/.test(length);
  438. length = pInt(length);
  439. extremes[prop] = isPercent ?
  440. smallestSize * length / 100 :
  441. length;
  442. });
  443. series.minPxSize = extremes.minSize;
  444. // Prioritize min size if conflict to make sure bubbles are
  445. // always visible. #5873
  446. series.maxPxSize = Math.max(extremes.maxSize, extremes.minSize);
  447. // Find the min and max Z
  448. zData = series.zData.filter(H.isNumber);
  449. if (zData.length) { // #1735
  450. zMin = pick(seriesOptions.zMin, Math.min(
  451. zMin,
  452. Math.max(
  453. arrayMin(zData),
  454. seriesOptions.displayNegative === false ?
  455. seriesOptions.zThreshold :
  456. -Number.MAX_VALUE
  457. )
  458. ));
  459. zMax = pick(
  460. seriesOptions.zMax,
  461. Math.max(zMax, arrayMax(zData))
  462. );
  463. }
  464. }
  465. }
  466. });
  467. activeSeries.forEach(function (series) {
  468. var data = series[dataKey],
  469. i = data.length,
  470. radius;
  471. if (isXAxis) {
  472. series.getRadii(zMin, zMax, series);
  473. }
  474. if (range > 0) {
  475. while (i--) {
  476. if (
  477. isNumber(data[i]) &&
  478. axis.dataMin <= data[i] &&
  479. data[i] <= axis.dataMax
  480. ) {
  481. radius = series.radii[i];
  482. pxMin = Math.min(
  483. ((data[i] - min) * transA) - radius,
  484. pxMin
  485. );
  486. pxMax = Math.max(
  487. ((data[i] - min) * transA) + radius,
  488. pxMax
  489. );
  490. }
  491. }
  492. }
  493. });
  494. // Apply the padding to the min and max properties
  495. if (activeSeries.length && range > 0 && !this.isLog) {
  496. pxMax -= axisLength;
  497. transA *= (
  498. axisLength +
  499. Math.max(0, pxMin) - // #8901
  500. Math.min(pxMax, axisLength)
  501. ) / axisLength;
  502. [['min', 'userMin', pxMin], ['max', 'userMax', pxMax]].forEach(
  503. function (keys) {
  504. if (pick(axis.options[keys[0]], axis[keys[1]]) === undefined) {
  505. axis[keys[0]] += keys[2] / transA;
  506. }
  507. }
  508. );
  509. }
  510. };
  511. /**
  512. * A `bubble` series. If the [type](#series.bubble.type) option is
  513. * not specified, it is inherited from [chart.type](#chart.type).
  514. *
  515. * @extends series,plotOptions.bubble
  516. * @excluding dataParser, dataURL, stack
  517. * @product highcharts highstock
  518. * @apioption series.bubble
  519. */
  520. /**
  521. * An array of data points for the series. For the `bubble` series type,
  522. * points can be given in the following ways:
  523. *
  524. * 1. An array of arrays with 3 or 2 values. In this case, the values correspond
  525. * to `x,y,z`. If the first value is a string, it is applied as the name of
  526. * the point, and the `x` value is inferred. The `x` value can also be
  527. * omitted, in which case the inner arrays should be of length 2\. Then the
  528. * `x` value is automatically calculated, either starting at 0 and
  529. * incremented by 1, or from `pointStart` and `pointInterval` given in the
  530. * series options.
  531. * ```js
  532. * data: [
  533. * [0, 1, 2],
  534. * [1, 5, 5],
  535. * [2, 0, 2]
  536. * ]
  537. * ```
  538. *
  539. * 2. An array of objects with named values. The following snippet shows only a
  540. * few settings, see the complete options set below. If the total number of
  541. * data points exceeds the series'
  542. * [turboThreshold](#series.bubble.turboThreshold), this option is not
  543. * available.
  544. * ```js
  545. * data: [{
  546. * x: 1,
  547. * y: 1,
  548. * z: 1,
  549. * name: "Point2",
  550. * color: "#00FF00"
  551. * }, {
  552. * x: 1,
  553. * y: 5,
  554. * z: 4,
  555. * name: "Point1",
  556. * color: "#FF00FF"
  557. * }]
  558. * ```
  559. *
  560. * @sample {highcharts} highcharts/series/data-array-of-arrays/
  561. * Arrays of numeric x and y
  562. * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
  563. * Arrays of datetime x and y
  564. * @sample {highcharts} highcharts/series/data-array-of-name-value/
  565. * Arrays of point.name and y
  566. * @sample {highcharts} highcharts/series/data-array-of-objects/
  567. * Config objects
  568. *
  569. * @type {Array<Array<(number|string),number>|Array<(number|string),number,number>|*>}
  570. * @extends series.line.data
  571. * @excluding marker
  572. * @product highcharts
  573. * @apioption series.bubble.data
  574. */
  575. /**
  576. * The size value for each bubble. The bubbles' diameters are computed
  577. * based on the `z`, and controlled by series options like `minSize`,
  578. * `maxSize`, `sizeBy`, `zMin` and `zMax`.
  579. *
  580. * @type {number}
  581. * @product highcharts
  582. * @apioption series.bubble.data.z
  583. */
  584. /**
  585. * @excluding enabled, enabledThreshold, height, radius, width
  586. * @apioption series.bubble.marker
  587. */