PackedBubbleSeries.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. /* *
  2. * (c) 2010-2019 Grzegorz Blachlinski, Sebastian Bochan
  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. var seriesType = H.seriesType,
  14. defined = H.defined;
  15. /**
  16. * Packed bubble series
  17. *
  18. * @private
  19. * @class
  20. * @name Highcharts.seriesTypes.packedbubble
  21. *
  22. * @augments Highcharts.Series
  23. *
  24. * @requires modules:highcharts-more
  25. */
  26. seriesType(
  27. 'packedbubble',
  28. 'bubble',
  29. /**
  30. * A packed bubble series is a two dimensional series type, where each point
  31. * renders a value in X, Y position. Each point is drawn as a bubble where
  32. * the bubbles don't overlap with each other and the radius of the bubble
  33. * related to the value. Requires `highcharts-more.js`.
  34. *
  35. * @sample {highcharts} highcharts/demo/packed-bubble/
  36. * Packed-bubble chart
  37. *
  38. * @extends plotOptions.bubble
  39. * @since 7.0.0
  40. * @product highcharts
  41. * @excluding connectEnds, connectNulls, jitter, keys,
  42. * sizeByAbsoluteValue, step, zMax, zMin
  43. * @optionparent plotOptions.packedbubble
  44. */
  45. {
  46. /**
  47. * Minimum bubble size. Bubbles will automatically size between the
  48. * `minSize` and `maxSize` to reflect the value of each bubble.
  49. * Can be either pixels (when no unit is given), or a percentage of
  50. * the smallest one of the plot width and height.
  51. *
  52. * @sample {highcharts} highcharts/plotoptions/bubble-size/
  53. * Bubble size
  54. *
  55. * @type {number|string}
  56. */
  57. minSize: '10%',
  58. /**
  59. * Maximum bubble size. Bubbles will automatically size between the
  60. * `minSize` and `maxSize` to reflect the value of each bubble.
  61. * Can be either pixels (when no unit is given), or a percentage of
  62. * the smallest one of the plot width and height.
  63. *
  64. * @sample {highcharts} highcharts/plotoptions/bubble-size/
  65. * Bubble size
  66. *
  67. * @type {number|string}
  68. */
  69. maxSize: '100%',
  70. sizeBy: 'radius',
  71. zoneAxis: 'y',
  72. tooltip: {
  73. pointFormat: 'Value: {point.value}'
  74. }
  75. }, {
  76. pointArrayMap: ['value'],
  77. pointValKey: 'value',
  78. isCartesian: false,
  79. axisTypes: [],
  80. /**
  81. * Create a single array of all points from all series
  82. * @private
  83. * @param {Array} Array of all series objects
  84. * @return {Array} Returns the array of all points.
  85. */
  86. accumulateAllPoints: function (series) {
  87. var chart = series.chart,
  88. allDataPoints = [],
  89. i, j;
  90. for (i = 0; i < chart.series.length; i++) {
  91. series = chart.series[i];
  92. if (series.visible || !chart.options.chart.ignoreHiddenSeries) {
  93. // add data to array only if series is visible
  94. for (j = 0; j < series.yData.length; j++) {
  95. allDataPoints.push([
  96. null, null,
  97. series.yData[j],
  98. series.index,
  99. j
  100. ]);
  101. }
  102. }
  103. }
  104. return allDataPoints;
  105. },
  106. // Extend the base translate method to handle bubble size, and correct
  107. // positioning them.
  108. translate: function () {
  109. var positions, // calculated positions of bubbles in bubble array
  110. series = this,
  111. chart = series.chart,
  112. data = series.data,
  113. index = series.index,
  114. point,
  115. radius,
  116. i;
  117. this.processedXData = this.xData;
  118. this.generatePoints();
  119. // merged data is an array with all of the data from all series
  120. if (!defined(chart.allDataPoints)) {
  121. chart.allDataPoints = series.accumulateAllPoints(series);
  122. // calculate radius for all added data
  123. series.getPointRadius();
  124. }
  125. // after getting initial radius, calculate bubble positions
  126. positions = this.placeBubbles(chart.allDataPoints);
  127. // Set the shape type and arguments to be picked up in drawPoints
  128. for (i = 0; i < positions.length; i++) {
  129. if (positions[i][3] === index) {
  130. // update the series points with the values from positions
  131. // array
  132. point = data[positions[i][4]];
  133. radius = positions[i][2];
  134. point.plotX = positions[i][0] - chart.plotLeft +
  135. chart.diffX;
  136. point.plotY = positions[i][1] - chart.plotTop +
  137. chart.diffY;
  138. point.marker = H.extend(point.marker, {
  139. radius: radius,
  140. width: 2 * radius,
  141. height: 2 * radius
  142. });
  143. }
  144. }
  145. },
  146. /**
  147. * Check if two bubbles overlaps.
  148. * @private
  149. * @param {Array} bubble1 first bubble
  150. * @param {Array} bubble2 second bubble
  151. * @return {boolean} overlap or not
  152. */
  153. checkOverlap: function (bubble1, bubble2) {
  154. var diffX = bubble1[0] - bubble2[0], // diff of X center values
  155. diffY = bubble1[1] - bubble2[1], // diff of Y center values
  156. sumRad = bubble1[2] + bubble2[2]; // sum of bubble radius
  157. return (
  158. Math.sqrt(diffX * diffX + diffY * diffY) -
  159. Math.abs(sumRad)
  160. ) < -0.001;
  161. },
  162. /**
  163. * Function that is adding one bubble based on positions and sizes
  164. * of two other bubbles, lastBubble is the last added bubble,
  165. * newOrigin is the bubble for positioning new bubbles.
  166. * nextBubble is the curently added bubble for which we are
  167. * calculating positions
  168. * @private
  169. * @param {Array} lastBubble The closest last bubble
  170. * @param {Array} newOrigin New bubble
  171. * @param {Array} nextBubble The closest next bubble
  172. * @return {Array} Bubble with correct positions
  173. */
  174. positionBubble: function (lastBubble, newOrigin, nextBubble) {
  175. var sqrt = Math.sqrt,
  176. asin = Math.asin,
  177. acos = Math.acos,
  178. pow = Math.pow,
  179. abs = Math.abs,
  180. distance = sqrt( // dist between lastBubble and newOrigin
  181. pow((lastBubble[0] - newOrigin[0]), 2) +
  182. pow((lastBubble[1] - newOrigin[1]), 2)
  183. ),
  184. alfa = acos(
  185. // from cosinus theorem: alfa is an angle used for
  186. // calculating correct position
  187. (
  188. pow(distance, 2) +
  189. pow(nextBubble[2] + newOrigin[2], 2) -
  190. pow(nextBubble[2] + lastBubble[2], 2)
  191. ) / (2 * (nextBubble[2] + newOrigin[2]) * distance)
  192. ),
  193. beta = asin( // from sinus theorem.
  194. abs(lastBubble[0] - newOrigin[0]) /
  195. distance
  196. ),
  197. // providing helping variables, related to angle between
  198. // lastBubble and newOrigin
  199. gamma = (lastBubble[1] - newOrigin[1]) < 0 ? 0 : Math.PI,
  200. // if new origin y is smaller than last bubble y value
  201. // (2 and 3 quarter),
  202. // add Math.PI to final angle
  203. delta = (lastBubble[0] - newOrigin[0]) *
  204. (lastBubble[1] - newOrigin[1]) < 0 ?
  205. 1 : -1, // (1st and 3rd quarter)
  206. finalAngle = gamma + alfa + beta * delta,
  207. cosA = Math.cos(finalAngle),
  208. sinA = Math.sin(finalAngle),
  209. posX = newOrigin[0] + (newOrigin[2] + nextBubble[2]) * sinA,
  210. // center of new origin + (radius1 + radius2) * sinus A
  211. posY = newOrigin[1] - (newOrigin[2] + nextBubble[2]) * cosA;
  212. return [
  213. posX,
  214. posY,
  215. nextBubble[2],
  216. nextBubble[3],
  217. nextBubble[4]
  218. ]; // the same as described before
  219. },
  220. /**
  221. * This is the main function responsible for positioning all of the
  222. * bubbles.
  223. * allDataPoints - bubble array, in format [pixel x value,
  224. * pixel y value, radius, related series index, related point index]
  225. * @private
  226. * @param {Array} allDataPoints All points from all series
  227. * @return {Array} Positions of all bubbles
  228. */
  229. placeBubbles: function (allDataPoints) {
  230. var series = this,
  231. checkOverlap = series.checkOverlap,
  232. positionBubble = series.positionBubble,
  233. bubblePos = [],
  234. stage = 1,
  235. j = 0,
  236. k = 0,
  237. calculatedBubble,
  238. sortedArr,
  239. i;
  240. // sort all points
  241. sortedArr = allDataPoints.sort(function (a, b) {
  242. return b[2] - a[2];
  243. });
  244. // if length is 0, return empty array
  245. if (!sortedArr.length) {
  246. return [];
  247. }
  248. if (sortedArr.length < 2) {
  249. // if length is 1,return only one bubble
  250. return [
  251. 0, 0,
  252. sortedArr[0][0],
  253. sortedArr[0][1],
  254. sortedArr[0][2]
  255. ];
  256. }
  257. // create first bubble in the middle of the chart
  258. bubblePos.push([
  259. [
  260. 0, // starting in 0,0 coordinates
  261. 0,
  262. sortedArr[0][2], // radius
  263. sortedArr[0][3], // series index
  264. sortedArr[0][4]
  265. ] // point index
  266. ]); // 0 level bubble
  267. bubblePos.push([
  268. [
  269. 0,
  270. 0 - sortedArr[1][2] - sortedArr[0][2],
  271. // move bubble above first one
  272. sortedArr[1][2],
  273. sortedArr[1][3],
  274. sortedArr[1][4]
  275. ]
  276. ]); // 1 level 1st bubble
  277. // first two already positioned so starting from 2
  278. for (i = 2; i < sortedArr.length; i++) {
  279. sortedArr[i][2] = sortedArr[i][2] || 1;
  280. // in case if radius is calculated as 0.
  281. calculatedBubble = positionBubble(
  282. bubblePos[stage][j],
  283. bubblePos[stage - 1][k],
  284. sortedArr[i]
  285. ); // calculate initial bubble position
  286. if (checkOverlap(calculatedBubble, bubblePos[stage][0])) {
  287. // if new bubble is overlapping with first bubble in
  288. // current level (stage)
  289. bubblePos.push([]);
  290. k = 0;
  291. // reset index of bubble, used for positioning the bubbles
  292. // around it, we are starting from first bubble in next
  293. // stage because we are changing level to higher
  294. bubblePos[stage + 1].push(
  295. positionBubble(
  296. bubblePos[stage][j],
  297. bubblePos[stage][0],
  298. sortedArr[i]
  299. )
  300. );
  301. // (last added bubble, 1st. bbl from cur stage, new bubble)
  302. stage++; // the new level is created, above current one
  303. j = 0; // set the index of bubble in current level to 0
  304. } else if (
  305. stage > 1 && bubblePos[stage - 1][k + 1] &&
  306. checkOverlap(calculatedBubble, bubblePos[stage - 1][k + 1])
  307. ) {
  308. // If new bubble is overlapping with one of the previous
  309. // stage bubbles, it means that - bubble, used for
  310. // positioning the bubbles around it has changed so we need
  311. // to recalculate it.
  312. k++;
  313. bubblePos[stage].push(
  314. positionBubble(
  315. bubblePos[stage][j],
  316. bubblePos[stage - 1][k],
  317. sortedArr[i]
  318. )
  319. );
  320. // (last added bubble, previous stage bubble, new bubble)
  321. j++;
  322. } else { // simply add calculated bubble
  323. j++;
  324. bubblePos[stage].push(calculatedBubble);
  325. }
  326. }
  327. series.chart.stages = bubblePos;
  328. // it may not be necessary but adding it just in case -
  329. // it is containing all of the bubble levels
  330. series.chart.rawPositions = [].concat.apply([], bubblePos);
  331. // bubble positions merged into one array
  332. series.resizeRadius();
  333. return series.chart.rawPositions;
  334. },
  335. /**
  336. * The function responsible for resizing the bubble radius.
  337. * In shortcut: it is taking the initially
  338. * calculated positions of bubbles. Then it is calculating the min max
  339. * of both dimensions, creating something in shape of bBox.
  340. * The comparison of bBox and the size of plotArea
  341. * (later it may be also the size set by customer) is giving the
  342. * value how to recalculate the radius so it will match the size
  343. * @private
  344. */
  345. resizeRadius: function () {
  346. var chart = this.chart,
  347. positions = chart.rawPositions,
  348. min = Math.min,
  349. max = Math.max,
  350. plotLeft = chart.plotLeft,
  351. plotTop = chart.plotTop,
  352. chartHeight = chart.plotHeight,
  353. chartWidth = chart.plotWidth,
  354. minX, maxX, minY, maxY,
  355. radius,
  356. bBox,
  357. spaceRatio,
  358. smallerDimension,
  359. i;
  360. minX = minY = Number.POSITIVE_INFINITY; // set initial values
  361. maxX = maxY = Number.NEGATIVE_INFINITY;
  362. for (i = 0; i < positions.length; i++) {
  363. radius = positions[i][2];
  364. minX = min(minX, positions[i][0] - radius);
  365. // (x center-radius) is the min x value used by specific bubble
  366. maxX = max(maxX, positions[i][0] + radius);
  367. minY = min(minY, positions[i][1] - radius);
  368. maxY = max(maxY, positions[i][1] + radius);
  369. }
  370. bBox = [maxX - minX, maxY - minY];
  371. spaceRatio = [
  372. (chartWidth - plotLeft) / bBox[0],
  373. (chartHeight - plotTop) / bBox[1]
  374. ];
  375. smallerDimension = min.apply([], spaceRatio);
  376. if (Math.abs(smallerDimension - 1) > 1e-10) {
  377. // if bBox is considered not the same width as possible size
  378. for (i = 0; i < positions.length; i++) {
  379. positions[i][2] *= smallerDimension;
  380. }
  381. this.placeBubbles(positions);
  382. } else {
  383. // If no radius recalculation is needed, we need to position the
  384. // whole bubbles in center of chart plotarea for this, we are
  385. // adding two parameters, diffY and diffX, that are related to
  386. // differences between the initial center and the bounding box.
  387. chart.diffY = chartHeight / 2 +
  388. plotTop - minY - (maxY - minY) / 2;
  389. chart.diffX = chartWidth / 2 +
  390. plotLeft - minX - (maxX - minX) / 2;
  391. }
  392. },
  393. // Calculate radius of bubbles in series.
  394. getPointRadius: function () { // bubbles array
  395. var series = this,
  396. chart = series.chart,
  397. plotWidth = chart.plotWidth,
  398. plotHeight = chart.plotHeight,
  399. seriesOptions = series.options,
  400. smallestSize = Math.min(plotWidth, plotHeight),
  401. extremes = {},
  402. radii = [],
  403. allDataPoints = chart.allDataPoints,
  404. minSize,
  405. maxSize,
  406. value,
  407. radius;
  408. ['minSize', 'maxSize'].forEach(function (prop) {
  409. var length = parseInt(seriesOptions[prop], 10),
  410. isPercent = /%$/.test(length);
  411. extremes[prop] = isPercent ?
  412. smallestSize * length / 100 :
  413. length;
  414. });
  415. chart.minRadius = minSize = extremes.minSize;
  416. chart.maxRadius = maxSize = extremes.maxSize;
  417. (allDataPoints || []).forEach(function (point, i) {
  418. value = point[2];
  419. radius = series.getRadius(
  420. minSize,
  421. maxSize,
  422. minSize,
  423. maxSize,
  424. value
  425. );
  426. if (value === 0) {
  427. radius = null;
  428. }
  429. allDataPoints[i][2] = radius;
  430. radii.push(radius);
  431. });
  432. this.radii = radii;
  433. },
  434. alignDataLabel: H.Series.prototype.alignDataLabel
  435. }
  436. );
  437. // When one series is modified, the others need to be recomputed
  438. H.addEvent(H.seriesTypes.packedbubble, 'updatedData', function () {
  439. var self = this;
  440. this.chart.series.forEach(function (s) {
  441. if (s.type === self.type) {
  442. s.isDirty = true;
  443. }
  444. });
  445. });
  446. // Remove accumulated data points to redistribute all of them again
  447. // (i.e after hiding series by legend)
  448. H.addEvent(H.Chart, 'beforeRedraw', function () {
  449. if (this.allDataPoints) {
  450. delete this.allDataPoints;
  451. }
  452. });
  453. /**
  454. * A `packedbubble` series. If the [type](#series.packedbubble.type) option is
  455. * not specified, it is inherited from [chart.type](#chart.type).
  456. *
  457. * @extends series,plotOptions.packedbubble
  458. * @excluding dataParser, dataURL, stack
  459. * @product highcharts highstock
  460. * @apioption series.packedbubble
  461. */
  462. /**
  463. * An array of data points for the series. For the `packedbubble` series type,
  464. * points can be given in the following ways:
  465. *
  466. * 1. An array of `value` values.
  467. * ```js
  468. * data: [5, 1, 20]
  469. * ```
  470. *
  471. * 2. An array of objects with named values. The objects are point configuration
  472. * objects as seen below. If the total number of data points exceeds the
  473. * series' [turboThreshold](#series.packedbubble.turboThreshold), this option
  474. * is not available.
  475. * ```js
  476. * data: [{
  477. * value: 1,
  478. * name: "Point2",
  479. * color: "#00FF00"
  480. * }, {
  481. * value: 5,
  482. * name: "Point1",
  483. * color: "#FF00FF"
  484. * }]
  485. * ```
  486. *
  487. * @sample {highcharts} highcharts/series/data-array-of-objects/
  488. * Config objects
  489. *
  490. * @type {Array<number|*>}
  491. * @extends series.line.data
  492. * @excluding marker,x,y
  493. * @product highcharts
  494. * @apioption series.packedbubble.data
  495. */
  496. /**
  497. * The value of a bubble. The bubble's size proportional to its `value`.
  498. *
  499. * @type {number}
  500. * @product highcharts
  501. * @apioption series.packedbubble.data.weight
  502. */
  503. /**
  504. * @excluding enabled, enabledThreshold, height, radius, width
  505. * @apioption series.packedbubble.marker
  506. */