MarkerClusters.js 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509
  1. /* *
  2. *
  3. * Marker clusters module.
  4. *
  5. * (c) 2010-2020 Torstein Honsi
  6. *
  7. * Author: Wojciech Chmiel
  8. *
  9. * License: www.highcharts.com/license
  10. *
  11. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  12. *
  13. * */
  14. 'use strict';
  15. import Chart from '../Core/Chart/Chart.js';
  16. import H from '../Core/Globals.js';
  17. import O from '../Core/Options.js';
  18. var defaultOptions = O.defaultOptions;
  19. import Point from '../Core/Series/Point.js';
  20. import SVGRenderer from '../Core/Renderer/SVG/SVGRenderer.js';
  21. import U from '../Core/Utilities.js';
  22. var addEvent = U.addEvent, animObject = U.animObject, defined = U.defined, error = U.error, isArray = U.isArray, isFunction = U.isFunction, isObject = U.isObject, isNumber = U.isNumber, merge = U.merge, objectEach = U.objectEach, relativeLength = U.relativeLength, syncTimeout = U.syncTimeout;
  23. /**
  24. * Function callback when a cluster is clicked.
  25. *
  26. * @callback Highcharts.MarkerClusterDrillCallbackFunction
  27. *
  28. * @param {Highcharts.Point} this
  29. * The point where the event occured.
  30. *
  31. * @param {Highcharts.PointClickEventObject} event
  32. * Event arguments.
  33. */
  34. ''; // detach doclets from following code
  35. /* eslint-disable no-invalid-this */
  36. import '../Core/Axis/Axis.js';
  37. import '../Core/Series/Series.js';
  38. var Series = H.Series, Scatter = H.seriesTypes.scatter, baseGeneratePoints = Series.prototype.generatePoints, stateIdCounter = 0,
  39. // Points that ids are included in the oldPointsStateId array
  40. // are hidden before animation. Other ones are destroyed.
  41. oldPointsStateId = [];
  42. /**
  43. * Options for marker clusters, the concept of sampling the data
  44. * values into larger blocks in order to ease readability and
  45. * increase performance of the JavaScript charts.
  46. *
  47. * Note: marker clusters module is not working with `boost`
  48. * and `draggable-points` modules.
  49. *
  50. * The marker clusters feature requires the marker-clusters.js
  51. * file to be loaded, found in the modules directory of the download
  52. * package, or online at [code.highcharts.com/modules/marker-clusters.js
  53. * ](code.highcharts.com/modules/marker-clusters.js).
  54. *
  55. * @sample maps/marker-clusters/europe
  56. * Maps marker clusters
  57. * @sample highcharts/marker-clusters/basic
  58. * Scatter marker clusters
  59. * @sample maps/marker-clusters/optimized-kmeans
  60. * Marker clusters with colorAxis
  61. *
  62. * @product highcharts highmaps
  63. * @since 8.0.0
  64. * @optionparent plotOptions.scatter.cluster
  65. *
  66. * @private
  67. */
  68. var clusterDefaultOptions = {
  69. /**
  70. * Whether to enable the marker-clusters module.
  71. *
  72. * @sample maps/marker-clusters/basic
  73. * Maps marker clusters
  74. * @sample highcharts/marker-clusters/basic
  75. * Scatter marker clusters
  76. */
  77. enabled: false,
  78. /**
  79. * When set to `false` prevent cluster overlapping - this option
  80. * works only when `layoutAlgorithm.type = "grid"`.
  81. *
  82. * @sample highcharts/marker-clusters/grid
  83. * Prevent overlapping
  84. */
  85. allowOverlap: true,
  86. /**
  87. * Options for the cluster marker animation.
  88. * @type {boolean|Partial<Highcharts.AnimationOptionsObject>}
  89. * @default { "duration": 500 }
  90. */
  91. animation: {
  92. /** @ignore-option */
  93. duration: 500
  94. },
  95. /**
  96. * Zoom the plot area to the cluster points range when a cluster is clicked.
  97. */
  98. drillToCluster: true,
  99. /**
  100. * The minimum amount of points to be combined into a cluster.
  101. * This value has to be greater or equal to 2.
  102. *
  103. * @sample highcharts/marker-clusters/basic
  104. * At least three points in the cluster
  105. */
  106. minimumClusterSize: 2,
  107. /**
  108. * Options for layout algorithm. Inside there
  109. * are options to change the type of the algorithm, gridSize,
  110. * distance or iterations.
  111. */
  112. layoutAlgorithm: {
  113. /**
  114. * Type of the algorithm used to combine points into a cluster.
  115. * There are three available algorithms:
  116. *
  117. * 1) `grid` - grid-based clustering technique. Points are assigned
  118. * to squares of set size depending on their position on the plot
  119. * area. Points inside the grid square are combined into a cluster.
  120. * The grid size can be controlled by `gridSize` property
  121. * (grid size changes at certain zoom levels).
  122. *
  123. * 2) `kmeans` - based on K-Means clustering technique. In the
  124. * first step, points are divided using the grid method (distance
  125. * property is a grid size) to find the initial amount of clusters.
  126. * Next, each point is classified by computing the distance between
  127. * each cluster center and that point. When the closest cluster
  128. * distance is lower than distance property set by a user the point
  129. * is added to this cluster otherwise is classified as `noise`. The
  130. * algorithm is repeated until each cluster center not change its
  131. * previous position more than one pixel. This technique is more
  132. * accurate but also more time consuming than the `grid` algorithm,
  133. * especially for big datasets.
  134. *
  135. * 3) `optimizedKmeans` - based on K-Means clustering technique. This
  136. * algorithm uses k-means algorithm only on the chart initialization
  137. * or when chart extremes have greater range than on initialization.
  138. * When a chart is redrawn the algorithm checks only clustered points
  139. * distance from the cluster center and rebuild it when the point is
  140. * spaced enough to be outside the cluster. It provides performance
  141. * improvement and more stable clusters position yet can be used rather
  142. * on small and sparse datasets.
  143. *
  144. * By default, the algorithm depends on visible quantity of points
  145. * and `kmeansThreshold`. When there are more visible points than the
  146. * `kmeansThreshold` the `grid` algorithm is used, otherwise `kmeans`.
  147. *
  148. * The custom clustering algorithm can be added by assigning a callback
  149. * function as the type property. This function takes an array of
  150. * `processedXData`, `processedYData`, `processedXData` indexes and
  151. * `layoutAlgorithm` options as arguments and should return an object
  152. * with grouped data.
  153. *
  154. * The algorithm should return an object like that:
  155. * <pre>{
  156. * clusterId1: [{
  157. * x: 573,
  158. * y: 285,
  159. * index: 1 // point index in the data array
  160. * }, {
  161. * x: 521,
  162. * y: 197,
  163. * index: 2
  164. * }],
  165. * clusterId2: [{
  166. * ...
  167. * }]
  168. * ...
  169. * }</pre>
  170. *
  171. * `clusterId` (example above - unique id of a cluster or noise)
  172. * is an array of points belonging to a cluster. If the
  173. * array has only one point or fewer points than set in
  174. * `cluster.minimumClusterSize` it won't be combined into a cluster.
  175. *
  176. * @sample maps/marker-clusters/optimized-kmeans
  177. * Optimized K-Means algorithm
  178. * @sample highcharts/marker-clusters/kmeans
  179. * K-Means algorithm
  180. * @sample highcharts/marker-clusters/grid
  181. * Grid algorithm
  182. * @sample maps/marker-clusters/custom-alg
  183. * Custom algorithm
  184. *
  185. * @type {string|Function}
  186. * @see [cluster.minimumClusterSize](#plotOptions.scatter.marker.cluster.minimumClusterSize)
  187. * @apioption plotOptions.scatter.cluster.layoutAlgorithm.type
  188. */
  189. /**
  190. * When `type` is set to the `grid`,
  191. * `gridSize` is a size of a grid square element either as a number
  192. * defining pixels, or a percentage defining a percentage
  193. * of the plot area width.
  194. *
  195. * @type {number|string}
  196. */
  197. gridSize: 50,
  198. /**
  199. * When `type` is set to `kmeans`,
  200. * `iterations` are the number of iterations that this algorithm will be
  201. * repeated to find clusters positions.
  202. *
  203. * @type {number}
  204. * @apioption plotOptions.scatter.cluster.layoutAlgorithm.iterations
  205. */
  206. /**
  207. * When `type` is set to `kmeans`,
  208. * `distance` is a maximum distance between point and cluster center
  209. * so that this point will be inside the cluster. The distance
  210. * is either a number defining pixels or a percentage
  211. * defining a percentage of the plot area width.
  212. *
  213. * @type {number|string}
  214. */
  215. distance: 40,
  216. /**
  217. * When `type` is set to `undefined` and there are more visible points
  218. * than the kmeansThreshold the `grid` algorithm is used to find
  219. * clusters, otherwise `kmeans`. It ensures good performance on
  220. * large datasets and better clusters arrangement after the zoom.
  221. */
  222. kmeansThreshold: 100
  223. },
  224. /**
  225. * Options for the cluster marker.
  226. * @extends plotOptions.series.marker
  227. * @excluding enabledThreshold, states
  228. * @type {Highcharts.PointMarkerOptionsObject}
  229. */
  230. marker: {
  231. /** @internal */
  232. symbol: 'cluster',
  233. /** @internal */
  234. radius: 15,
  235. /** @internal */
  236. lineWidth: 0,
  237. /** @internal */
  238. lineColor: '#ffffff'
  239. },
  240. /**
  241. * Fires when the cluster point is clicked and `drillToCluster` is enabled.
  242. * One parameter, `event`, is passed to the function. The default action
  243. * is to zoom to the cluster points range. This can be prevented
  244. * by calling `event.preventDefault()`.
  245. *
  246. * @type {Highcharts.MarkerClusterDrillCallbackFunction}
  247. * @product highcharts highmaps
  248. * @see [cluster.drillToCluster](#plotOptions.scatter.marker.cluster.drillToCluster)
  249. * @apioption plotOptions.scatter.cluster.events.drillToCluster
  250. */
  251. /**
  252. * An array defining zones within marker clusters.
  253. *
  254. * In styled mode, the color zones are styled with the
  255. * `.highcharts-cluster-zone-{n}` class, or custom
  256. * classed from the `className`
  257. * option.
  258. *
  259. * @sample highcharts/marker-clusters/basic
  260. * Marker clusters zones
  261. * @sample maps/marker-clusters/custom-alg
  262. * Zones on maps
  263. *
  264. * @type {Array<*>}
  265. * @product highcharts highmaps
  266. * @apioption plotOptions.scatter.cluster.zones
  267. */
  268. /**
  269. * Styled mode only. A custom class name for the zone.
  270. *
  271. * @sample highcharts/css/color-zones/
  272. * Zones styled by class name
  273. *
  274. * @type {string}
  275. * @apioption plotOptions.scatter.cluster.zones.className
  276. */
  277. /**
  278. * Settings for the cluster marker belonging to the zone.
  279. *
  280. * @see [cluster.marker](#plotOptions.scatter.cluster.marker)
  281. * @extends plotOptions.scatter.cluster.marker
  282. * @product highcharts highmaps
  283. * @apioption plotOptions.scatter.cluster.zones.marker
  284. */
  285. /**
  286. * The value where the zone starts.
  287. *
  288. * @type {number}
  289. * @product highcharts highmaps
  290. * @apioption plotOptions.scatter.cluster.zones.from
  291. */
  292. /**
  293. * The value where the zone ends.
  294. *
  295. * @type {number}
  296. * @product highcharts highmaps
  297. * @apioption plotOptions.scatter.cluster.zones.to
  298. */
  299. /**
  300. * The fill color of the cluster marker in hover state. When
  301. * `undefined`, the series' or point's fillColor for normal
  302. * state is used.
  303. *
  304. * @type {Highcharts.ColorType}
  305. * @apioption plotOptions.scatter.cluster.states.hover.fillColor
  306. */
  307. /**
  308. * Options for the cluster data labels.
  309. * @type {Highcharts.DataLabelsOptions}
  310. */
  311. dataLabels: {
  312. /** @internal */
  313. enabled: true,
  314. /** @internal */
  315. format: '{point.clusterPointsAmount}',
  316. /** @internal */
  317. verticalAlign: 'middle',
  318. /** @internal */
  319. align: 'center',
  320. /** @internal */
  321. style: {
  322. color: 'contrast'
  323. },
  324. /** @internal */
  325. inside: true
  326. }
  327. };
  328. (defaultOptions.plotOptions || {}).series = merge((defaultOptions.plotOptions || {}).series, {
  329. cluster: clusterDefaultOptions,
  330. tooltip: {
  331. /**
  332. * The HTML of the cluster point's in the tooltip. Works only with
  333. * marker-clusters module and analogously to
  334. * [pointFormat](#tooltip.pointFormat).
  335. *
  336. * The cluster tooltip can be also formatted using
  337. * `tooltip.formatter` callback function and `point.isCluster` flag.
  338. *
  339. * @sample highcharts/marker-clusters/grid
  340. * Format tooltip for cluster points.
  341. *
  342. * @sample maps/marker-clusters/europe/
  343. * Format tooltip for clusters using tooltip.formatter
  344. *
  345. * @apioption tooltip.clusterFormat
  346. */
  347. clusterFormat: '<span>Clustered points: ' +
  348. '{point.clusterPointsAmount}</span><br/>'
  349. }
  350. });
  351. // Utils.
  352. /* eslint-disable require-jsdoc */
  353. function getClusterPosition(points) {
  354. var pointsLen = points.length, sumX = 0, sumY = 0, i;
  355. for (i = 0; i < pointsLen; i++) {
  356. sumX += points[i].x;
  357. sumY += points[i].y;
  358. }
  359. return {
  360. x: sumX / pointsLen,
  361. y: sumY / pointsLen
  362. };
  363. }
  364. // Prepare array with sorted data objects to be
  365. // compared in getPointsState method.
  366. function getDataState(clusteredData, stateDataLen) {
  367. var state = [];
  368. state.length = stateDataLen;
  369. clusteredData.clusters.forEach(function (cluster) {
  370. cluster.data.forEach(function (elem) {
  371. state[elem.dataIndex] = elem;
  372. });
  373. });
  374. clusteredData.noise.forEach(function (noise) {
  375. state[noise.data[0].dataIndex] = noise.data[0];
  376. });
  377. return state;
  378. }
  379. function fadeInElement(elem, opacity, animation) {
  380. elem
  381. .attr({
  382. opacity: opacity
  383. })
  384. .animate({
  385. opacity: 1
  386. }, animation);
  387. }
  388. function fadeInStatePoint(stateObj, opacity, animation, fadeinGraphic, fadeinDataLabel) {
  389. if (stateObj.point) {
  390. if (fadeinGraphic && stateObj.point.graphic) {
  391. stateObj.point.graphic.show();
  392. fadeInElement(stateObj.point.graphic, opacity, animation);
  393. }
  394. if (fadeinDataLabel && stateObj.point.dataLabel) {
  395. stateObj.point.dataLabel.show();
  396. fadeInElement(stateObj.point.dataLabel, opacity, animation);
  397. }
  398. }
  399. }
  400. function hideStatePoint(stateObj, hideGraphic, hideDataLabel) {
  401. if (stateObj.point) {
  402. if (hideGraphic && stateObj.point.graphic) {
  403. stateObj.point.graphic.hide();
  404. }
  405. if (hideDataLabel && stateObj.point.dataLabel) {
  406. stateObj.point.dataLabel.hide();
  407. }
  408. }
  409. }
  410. function destroyOldPoints(oldState) {
  411. if (oldState) {
  412. objectEach(oldState, function (state) {
  413. if (state.point && state.point.destroy) {
  414. state.point.destroy();
  415. }
  416. });
  417. }
  418. }
  419. function fadeInNewPointAndDestoryOld(newPointObj, oldPoints, animation, opacity) {
  420. // Fade in new point.
  421. fadeInStatePoint(newPointObj, opacity, animation, true, true);
  422. // Destroy old animated points.
  423. oldPoints.forEach(function (p) {
  424. if (p.point && p.point.destroy) {
  425. p.point.destroy();
  426. }
  427. });
  428. }
  429. // Generate unique stateId for a state element.
  430. function getStateId() {
  431. return Math.random().toString(36).substring(2, 7) + '-' + stateIdCounter++;
  432. }
  433. // Useful for debugging.
  434. // function drawGridLines(
  435. // series: Highcharts.Series,
  436. // options: Highcharts.MarkerClusterLayoutAlgorithmOptions
  437. // ): void {
  438. // var chart = series.chart,
  439. // xAxis = series.xAxis,
  440. // yAxis = series.yAxis,
  441. // xAxisLen = series.xAxis.len,
  442. // yAxisLen = series.yAxis.len,
  443. // i, j, elem, text,
  444. // currentX = 0,
  445. // currentY = 0,
  446. // scaledGridSize = 50,
  447. // gridX = 0,
  448. // gridY = 0,
  449. // gridOffset = series.getGridOffset(),
  450. // mapXSize, mapYSize;
  451. // if (series.debugGridLines && series.debugGridLines.length) {
  452. // series.debugGridLines.forEach(function (gridItem): void {
  453. // if (gridItem && gridItem.destroy) {
  454. // gridItem.destroy();
  455. // }
  456. // });
  457. // }
  458. // series.debugGridLines = [];
  459. // scaledGridSize = series.getScaledGridSize(options);
  460. // mapXSize = Math.abs(
  461. // xAxis.toPixels(xAxis.dataMax || 0) -
  462. // xAxis.toPixels(xAxis.dataMin || 0)
  463. // );
  464. // mapYSize = Math.abs(
  465. // yAxis.toPixels(yAxis.dataMax || 0) -
  466. // yAxis.toPixels(yAxis.dataMin || 0)
  467. // );
  468. // gridX = Math.ceil(mapXSize / scaledGridSize);
  469. // gridY = Math.ceil(mapYSize / scaledGridSize);
  470. // for (i = 0; i < gridX; i++) {
  471. // currentX = i * scaledGridSize;
  472. // if (
  473. // gridOffset.plotLeft + currentX >= 0 &&
  474. // gridOffset.plotLeft + currentX < xAxisLen
  475. // ) {
  476. // for (j = 0; j < gridY; j++) {
  477. // currentY = j * scaledGridSize;
  478. // if (
  479. // gridOffset.plotTop + currentY >= 0 &&
  480. // gridOffset.plotTop + currentY < yAxisLen
  481. // ) {
  482. // if (j % 2 === 0 && i % 2 === 0) {
  483. // var rect = chart.renderer
  484. // .rect(
  485. // gridOffset.plotLeft + currentX,
  486. // gridOffset.plotTop + currentY,
  487. // scaledGridSize * 2,
  488. // scaledGridSize * 2
  489. // )
  490. // .attr({
  491. // stroke: series.color,
  492. // 'stroke-width': '2px'
  493. // })
  494. // .add()
  495. // .toFront();
  496. // series.debugGridLines.push(rect);
  497. // }
  498. // elem = chart.renderer
  499. // .rect(
  500. // gridOffset.plotLeft + currentX,
  501. // gridOffset.plotTop + currentY,
  502. // scaledGridSize,
  503. // scaledGridSize
  504. // )
  505. // .attr({
  506. // stroke: series.color,
  507. // opacity: 0.3,
  508. // 'stroke-width': '1px'
  509. // })
  510. // .add()
  511. // .toFront();
  512. // text = chart.renderer
  513. // .text(
  514. // j + '-' + i,
  515. // gridOffset.plotLeft + currentX + 2,
  516. // gridOffset.plotTop + currentY + 7
  517. // )
  518. // .css({
  519. // fill: 'rgba(0, 0, 0, 0.7)',
  520. // fontSize: '7px'
  521. // })
  522. // .add()
  523. // .toFront();
  524. // series.debugGridLines.push(elem);
  525. // series.debugGridLines.push(text);
  526. // }
  527. // }
  528. // }
  529. // }
  530. // }
  531. /* eslint-enable require-jsdoc */
  532. // Cluster symbol.
  533. SVGRenderer.prototype.symbols.cluster = function (x, y, width, height) {
  534. var w = width / 2, h = height / 2, outerWidth = 1, space = 1, inner, outer1, outer2;
  535. inner = this.arc(x + w, y + h, w - space * 4, h - space * 4, {
  536. start: Math.PI * 0.5,
  537. end: Math.PI * 2.5,
  538. open: false
  539. });
  540. outer1 = this.arc(x + w, y + h, w - space * 3, h - space * 3, {
  541. start: Math.PI * 0.5,
  542. end: Math.PI * 2.5,
  543. innerR: w - outerWidth * 2,
  544. open: false
  545. });
  546. outer2 = this.arc(x + w, y + h, w - space, h - space, {
  547. start: Math.PI * 0.5,
  548. end: Math.PI * 2.5,
  549. innerR: w,
  550. open: false
  551. });
  552. return outer2.concat(outer1, inner);
  553. };
  554. Scatter.prototype.animateClusterPoint = function (clusterObj) {
  555. var series = this, xAxis = series.xAxis, yAxis = series.yAxis, chart = series.chart, clusterOptions = series.options.cluster, animation = animObject((clusterOptions || {}).animation), animDuration = animation.duration || 500, pointsState = (series.markerClusterInfo || {}).pointsState, newState = (pointsState || {}).newState, oldState = (pointsState || {}).oldState, parentId, oldPointObj, newPointObj, oldPoints = [], newPointBBox, offset = 0, newX = 0, newY = 0, isOldPointGrahic = false, isCbHandled = false;
  556. if (oldState && newState) {
  557. newPointObj = newState[clusterObj.stateId];
  558. newX = xAxis.toPixels(newPointObj.x) - chart.plotLeft;
  559. newY = yAxis.toPixels(newPointObj.y) - chart.plotTop;
  560. // Point has one ancestor.
  561. if (newPointObj.parentsId.length === 1) {
  562. parentId = (newState || {})[clusterObj.stateId].parentsId[0];
  563. oldPointObj = oldState[parentId];
  564. // If old and new poistions are the same do not animate.
  565. if (newPointObj.point &&
  566. newPointObj.point.graphic &&
  567. oldPointObj &&
  568. oldPointObj.point &&
  569. oldPointObj.point.plotX &&
  570. oldPointObj.point.plotY &&
  571. oldPointObj.point.plotX !== newPointObj.point.plotX &&
  572. oldPointObj.point.plotY !== newPointObj.point.plotY) {
  573. newPointBBox = newPointObj.point.graphic.getBBox();
  574. offset = newPointBBox.width / 2;
  575. newPointObj.point.graphic.attr({
  576. x: oldPointObj.point.plotX - offset,
  577. y: oldPointObj.point.plotY - offset
  578. });
  579. newPointObj.point.graphic.animate({
  580. x: newX - (newPointObj.point.graphic.radius || 0),
  581. y: newY - (newPointObj.point.graphic.radius || 0)
  582. }, animation, function () {
  583. isCbHandled = true;
  584. // Destroy old point.
  585. if (oldPointObj.point && oldPointObj.point.destroy) {
  586. oldPointObj.point.destroy();
  587. }
  588. });
  589. // Data label animation.
  590. if (newPointObj.point.dataLabel &&
  591. newPointObj.point.dataLabel.alignAttr &&
  592. oldPointObj.point.dataLabel &&
  593. oldPointObj.point.dataLabel.alignAttr) {
  594. newPointObj.point.dataLabel.attr({
  595. x: oldPointObj.point.dataLabel.alignAttr.x,
  596. y: oldPointObj.point.dataLabel.alignAttr.y
  597. });
  598. newPointObj.point.dataLabel.animate({
  599. x: newPointObj.point.dataLabel.alignAttr.x,
  600. y: newPointObj.point.dataLabel.alignAttr.y
  601. }, animation);
  602. }
  603. }
  604. }
  605. else if (newPointObj.parentsId.length === 0) {
  606. // Point has no ancestors - new point.
  607. // Hide new point.
  608. hideStatePoint(newPointObj, true, true);
  609. syncTimeout(function () {
  610. // Fade in new point.
  611. fadeInStatePoint(newPointObj, 0.1, animation, true, true);
  612. }, animDuration / 2);
  613. }
  614. else {
  615. // Point has many ancestors.
  616. // Hide new point before animation.
  617. hideStatePoint(newPointObj, true, true);
  618. newPointObj.parentsId.forEach(function (elem) {
  619. if (oldState && oldState[elem]) {
  620. oldPointObj = oldState[elem];
  621. oldPoints.push(oldPointObj);
  622. if (oldPointObj.point &&
  623. oldPointObj.point.graphic) {
  624. isOldPointGrahic = true;
  625. oldPointObj.point.graphic.show();
  626. oldPointObj.point.graphic.animate({
  627. x: newX - (oldPointObj.point.graphic.radius || 0),
  628. y: newY - (oldPointObj.point.graphic.radius || 0),
  629. opacity: 0.4
  630. }, animation, function () {
  631. isCbHandled = true;
  632. fadeInNewPointAndDestoryOld(newPointObj, oldPoints, animation, 0.7);
  633. });
  634. if (oldPointObj.point.dataLabel &&
  635. oldPointObj.point.dataLabel.y !== -9999 &&
  636. newPointObj.point &&
  637. newPointObj.point.dataLabel &&
  638. newPointObj.point.dataLabel.alignAttr) {
  639. oldPointObj.point.dataLabel.show();
  640. oldPointObj.point.dataLabel.animate({
  641. x: newPointObj.point.dataLabel.alignAttr.x,
  642. y: newPointObj.point.dataLabel.alignAttr.y,
  643. opacity: 0.4
  644. }, animation);
  645. }
  646. }
  647. }
  648. });
  649. // Make sure point is faded in.
  650. syncTimeout(function () {
  651. if (!isCbHandled) {
  652. fadeInNewPointAndDestoryOld(newPointObj, oldPoints, animation, 0.85);
  653. }
  654. }, animDuration);
  655. if (!isOldPointGrahic) {
  656. syncTimeout(function () {
  657. fadeInNewPointAndDestoryOld(newPointObj, oldPoints, animation, 0.1);
  658. }, animDuration / 2);
  659. }
  660. }
  661. }
  662. };
  663. Scatter.prototype.getGridOffset = function () {
  664. var series = this, chart = series.chart, xAxis = series.xAxis, yAxis = series.yAxis, plotLeft = 0, plotTop = 0;
  665. if (series.dataMinX && series.dataMaxX) {
  666. plotLeft = xAxis.reversed ?
  667. xAxis.toPixels(series.dataMaxX) : xAxis.toPixels(series.dataMinX);
  668. }
  669. else {
  670. plotLeft = chart.plotLeft;
  671. }
  672. if (series.dataMinY && series.dataMaxY) {
  673. plotTop = yAxis.reversed ?
  674. yAxis.toPixels(series.dataMinY) : yAxis.toPixels(series.dataMaxY);
  675. }
  676. else {
  677. plotTop = chart.plotTop;
  678. }
  679. return { plotLeft: plotLeft, plotTop: plotTop };
  680. };
  681. Scatter.prototype.getScaledGridSize = function (options) {
  682. var series = this, xAxis = series.xAxis, search = true, k = 1, divider = 1, processedGridSize = options.processedGridSize ||
  683. clusterDefaultOptions.layoutAlgorithm.gridSize, gridSize, scale, level;
  684. if (!series.gridValueSize) {
  685. series.gridValueSize = Math.abs(xAxis.toValue(processedGridSize) - xAxis.toValue(0));
  686. }
  687. gridSize = xAxis.toPixels(series.gridValueSize) - xAxis.toPixels(0);
  688. scale = +(processedGridSize / gridSize).toFixed(14);
  689. // Find the level and its divider.
  690. while (search && scale !== 1) {
  691. level = Math.pow(2, k);
  692. if (scale > 0.75 && scale < 1.25) {
  693. search = false;
  694. }
  695. else if (scale >= (1 / level) && scale < 2 * (1 / level)) {
  696. search = false;
  697. divider = level;
  698. }
  699. else if (scale <= level && scale > level / 2) {
  700. search = false;
  701. divider = 1 / level;
  702. }
  703. k++;
  704. }
  705. return (processedGridSize / divider) / scale;
  706. };
  707. Scatter.prototype.getRealExtremes = function () {
  708. var _a, _b;
  709. var series = this, chart = series.chart, xAxis = series.xAxis, yAxis = series.yAxis, realMinX = xAxis ? xAxis.toValue(chart.plotLeft) : 0, realMaxX = xAxis ?
  710. xAxis.toValue(chart.plotLeft + chart.plotWidth) : 0, realMinY = yAxis ? yAxis.toValue(chart.plotTop) : 0, realMaxY = yAxis ?
  711. yAxis.toValue(chart.plotTop + chart.plotHeight) : 0;
  712. if (realMinX > realMaxX) {
  713. _a = [realMinX, realMaxX], realMaxX = _a[0], realMinX = _a[1];
  714. }
  715. if (realMinY > realMaxY) {
  716. _b = [realMinY, realMaxY], realMaxY = _b[0], realMinY = _b[1];
  717. }
  718. return {
  719. minX: realMinX,
  720. maxX: realMaxX,
  721. minY: realMinY,
  722. maxY: realMaxY
  723. };
  724. };
  725. Scatter.prototype.onDrillToCluster = function (event) {
  726. var point = event.point || event.target;
  727. point.firePointEvent('drillToCluster', event, function (e) {
  728. var _a, _b;
  729. var point = e.point || e.target, series = point.series, xAxis = point.series.xAxis, yAxis = point.series.yAxis, chart = point.series.chart, clusterOptions = series.options.cluster, drillToCluster = (clusterOptions || {}).drillToCluster, offsetX, offsetY, sortedDataX, sortedDataY, minX, minY, maxX, maxY;
  730. if (drillToCluster && point.clusteredData) {
  731. sortedDataX = point.clusteredData.map(function (data) {
  732. return data.x;
  733. }).sort(function (a, b) { return a - b; });
  734. sortedDataY = point.clusteredData.map(function (data) {
  735. return data.y;
  736. }).sort(function (a, b) { return a - b; });
  737. minX = sortedDataX[0];
  738. maxX = sortedDataX[sortedDataX.length - 1];
  739. minY = sortedDataY[0];
  740. maxY = sortedDataY[sortedDataY.length - 1];
  741. offsetX = Math.abs((maxX - minX) * 0.1);
  742. offsetY = Math.abs((maxY - minY) * 0.1);
  743. chart.pointer.zoomX = true;
  744. chart.pointer.zoomY = true;
  745. // Swap when minus values.
  746. if (minX > maxX) {
  747. _a = [maxX, minX], minX = _a[0], maxX = _a[1];
  748. }
  749. if (minY > maxY) {
  750. _b = [maxY, minY], minY = _b[0], maxY = _b[1];
  751. }
  752. chart.zoom({
  753. originalEvent: e,
  754. xAxis: [{
  755. axis: xAxis,
  756. min: minX - offsetX,
  757. max: maxX + offsetX
  758. }],
  759. yAxis: [{
  760. axis: yAxis,
  761. min: minY - offsetY,
  762. max: maxY + offsetY
  763. }]
  764. });
  765. }
  766. });
  767. };
  768. Scatter.prototype.getClusterDistancesFromPoint = function (clusters, pointX, pointY) {
  769. var series = this, xAxis = series.xAxis, yAxis = series.yAxis, pointClusterDistance = [], j, distance;
  770. for (j = 0; j < clusters.length; j++) {
  771. distance = Math.sqrt(Math.pow(xAxis.toPixels(pointX) -
  772. xAxis.toPixels(clusters[j].posX), 2) +
  773. Math.pow(yAxis.toPixels(pointY) -
  774. yAxis.toPixels(clusters[j].posY), 2));
  775. pointClusterDistance.push({
  776. clusterIndex: j,
  777. distance: distance
  778. });
  779. }
  780. return pointClusterDistance.sort(function (a, b) { return a.distance - b.distance; });
  781. };
  782. // Point state used when animation is enabled to compare
  783. // and bind old points with new ones.
  784. Scatter.prototype.getPointsState = function (clusteredData, oldMarkerClusterInfo, dataLength) {
  785. var oldDataStateArr = oldMarkerClusterInfo ?
  786. getDataState(oldMarkerClusterInfo, dataLength) : [], newDataStateArr = getDataState(clusteredData, dataLength), state = {}, newState, oldState, i;
  787. // Clear global array before populate with new ids.
  788. oldPointsStateId = [];
  789. // Build points state structure.
  790. clusteredData.clusters.forEach(function (cluster) {
  791. state[cluster.stateId] = {
  792. x: cluster.x,
  793. y: cluster.y,
  794. id: cluster.stateId,
  795. point: cluster.point,
  796. parentsId: []
  797. };
  798. });
  799. clusteredData.noise.forEach(function (noise) {
  800. state[noise.stateId] = {
  801. x: noise.x,
  802. y: noise.y,
  803. id: noise.stateId,
  804. point: noise.point,
  805. parentsId: []
  806. };
  807. });
  808. // Bind new and old state.
  809. for (i = 0; i < newDataStateArr.length; i++) {
  810. newState = newDataStateArr[i];
  811. oldState = oldDataStateArr[i];
  812. if (newState &&
  813. oldState &&
  814. newState.parentStateId &&
  815. oldState.parentStateId &&
  816. state[newState.parentStateId] &&
  817. state[newState.parentStateId].parentsId.indexOf(oldState.parentStateId) === -1) {
  818. state[newState.parentStateId].parentsId.push(oldState.parentStateId);
  819. if (oldPointsStateId.indexOf(oldState.parentStateId) === -1) {
  820. oldPointsStateId.push(oldState.parentStateId);
  821. }
  822. }
  823. }
  824. return state;
  825. };
  826. Scatter.prototype.markerClusterAlgorithms = {
  827. grid: function (dataX, dataY, dataIndexes, options) {
  828. var series = this, xAxis = series.xAxis, yAxis = series.yAxis, grid = {}, gridOffset = series.getGridOffset(), scaledGridSize, x, y, gridX, gridY, key, i;
  829. // drawGridLines(series, options);
  830. scaledGridSize = series.getScaledGridSize(options);
  831. for (i = 0; i < dataX.length; i++) {
  832. x = xAxis.toPixels(dataX[i]) - gridOffset.plotLeft;
  833. y = yAxis.toPixels(dataY[i]) - gridOffset.plotTop;
  834. gridX = Math.floor(x / scaledGridSize);
  835. gridY = Math.floor(y / scaledGridSize);
  836. key = gridY + '-' + gridX;
  837. if (!grid[key]) {
  838. grid[key] = [];
  839. }
  840. grid[key].push({
  841. dataIndex: dataIndexes[i],
  842. x: dataX[i],
  843. y: dataY[i]
  844. });
  845. }
  846. return grid;
  847. },
  848. kmeans: function (dataX, dataY, dataIndexes, options) {
  849. var series = this, clusters = [], noise = [], group = {}, pointMaxDistance = options.processedDistance ||
  850. clusterDefaultOptions.layoutAlgorithm.distance, iterations = options.iterations,
  851. // Max pixel difference beetwen new and old cluster position.
  852. maxClusterShift = 1, currentIteration = 0, repeat = true, pointX = 0, pointY = 0, tempPos, pointClusterDistance = [], groupedData, key, i, j;
  853. options.processedGridSize = options.processedDistance;
  854. // Use grid method to get groupedData object.
  855. groupedData = series.markerClusterAlgorithms ?
  856. series.markerClusterAlgorithms.grid.call(series, dataX, dataY, dataIndexes, options) : {};
  857. // Find clusters amount and its start positions
  858. // based on grid grouped data.
  859. for (key in groupedData) {
  860. if (groupedData[key].length > 1) {
  861. tempPos = getClusterPosition(groupedData[key]);
  862. clusters.push({
  863. posX: tempPos.x,
  864. posY: tempPos.y,
  865. oldX: 0,
  866. oldY: 0,
  867. startPointsLen: groupedData[key].length,
  868. points: []
  869. });
  870. }
  871. }
  872. // Start kmeans iteration process.
  873. while (repeat) {
  874. clusters.map(function (c) {
  875. c.points.length = 0;
  876. return c;
  877. });
  878. noise.length = 0;
  879. for (i = 0; i < dataX.length; i++) {
  880. pointX = dataX[i];
  881. pointY = dataY[i];
  882. pointClusterDistance = series.getClusterDistancesFromPoint(clusters, pointX, pointY);
  883. if (pointClusterDistance.length &&
  884. pointClusterDistance[0].distance < pointMaxDistance) {
  885. clusters[pointClusterDistance[0].clusterIndex].points.push({
  886. x: pointX,
  887. y: pointY,
  888. dataIndex: dataIndexes[i]
  889. });
  890. }
  891. else {
  892. noise.push({
  893. x: pointX,
  894. y: pointY,
  895. dataIndex: dataIndexes[i]
  896. });
  897. }
  898. }
  899. // When cluster points array has only one point the
  900. // point should be classified again.
  901. for (j = 0; j < clusters.length; j++) {
  902. if (clusters[j].points.length === 1) {
  903. pointClusterDistance = series.getClusterDistancesFromPoint(clusters, clusters[j].points[0].x, clusters[j].points[0].y);
  904. if (pointClusterDistance[1].distance < pointMaxDistance) {
  905. // Add point to the next closest cluster.
  906. clusters[pointClusterDistance[1].clusterIndex].points
  907. .push(clusters[j].points[0]);
  908. // Clear points array.
  909. clusters[pointClusterDistance[0].clusterIndex]
  910. .points.length = 0;
  911. }
  912. }
  913. }
  914. // Compute a new clusters position and check if it
  915. // is different than the old one.
  916. repeat = false;
  917. for (j = 0; j < clusters.length; j++) {
  918. tempPos = getClusterPosition(clusters[j].points);
  919. clusters[j].oldX = clusters[j].posX;
  920. clusters[j].oldY = clusters[j].posY;
  921. clusters[j].posX = tempPos.x;
  922. clusters[j].posY = tempPos.y;
  923. // Repeat the algorithm if at least one cluster
  924. // is shifted more than maxClusterShift property.
  925. if (clusters[j].posX > clusters[j].oldX + maxClusterShift ||
  926. clusters[j].posX < clusters[j].oldX - maxClusterShift ||
  927. clusters[j].posY > clusters[j].oldY + maxClusterShift ||
  928. clusters[j].posY < clusters[j].oldY - maxClusterShift) {
  929. repeat = true;
  930. }
  931. }
  932. // If iterations property is set repeat the algorithm
  933. // specified amount of times.
  934. if (iterations) {
  935. repeat = currentIteration < iterations - 1;
  936. }
  937. currentIteration++;
  938. }
  939. clusters.forEach(function (cluster, i) {
  940. group['cluster' + i] = cluster.points;
  941. });
  942. noise.forEach(function (noise, i) {
  943. group['noise' + i] = [noise];
  944. });
  945. return group;
  946. },
  947. optimizedKmeans: function (processedXData, processedYData, dataIndexes, options) {
  948. var series = this, xAxis = series.xAxis, yAxis = series.yAxis, pointMaxDistance = options.processedDistance ||
  949. clusterDefaultOptions.layoutAlgorithm.gridSize, group = {}, extremes = series.getRealExtremes(), clusterMarkerOptions = (series.options.cluster || {}).marker, offset, distance, radius;
  950. if (!series.markerClusterInfo || (series.initMaxX && series.initMaxX < extremes.maxX ||
  951. series.initMinX && series.initMinX > extremes.minX ||
  952. series.initMaxY && series.initMaxY < extremes.maxY ||
  953. series.initMinY && series.initMinY > extremes.minY)) {
  954. series.initMaxX = extremes.maxX;
  955. series.initMinX = extremes.minX;
  956. series.initMaxY = extremes.maxY;
  957. series.initMinY = extremes.minY;
  958. group = series.markerClusterAlgorithms ?
  959. series.markerClusterAlgorithms.kmeans.call(series, processedXData, processedYData, dataIndexes, options) : {};
  960. series.baseClusters = null;
  961. }
  962. else {
  963. if (!series.baseClusters) {
  964. series.baseClusters = {
  965. clusters: series.markerClusterInfo.clusters,
  966. noise: series.markerClusterInfo.noise
  967. };
  968. }
  969. series.baseClusters.clusters.forEach(function (cluster) {
  970. cluster.pointsOutside = [];
  971. cluster.pointsInside = [];
  972. cluster.data.forEach(function (dataPoint) {
  973. distance = Math.sqrt(Math.pow(xAxis.toPixels(dataPoint.x) -
  974. xAxis.toPixels(cluster.x), 2) +
  975. Math.pow(yAxis.toPixels(dataPoint.y) -
  976. yAxis.toPixels(cluster.y), 2));
  977. if (cluster.clusterZone &&
  978. cluster.clusterZone.marker &&
  979. cluster.clusterZone.marker.radius) {
  980. radius = cluster.clusterZone.marker.radius;
  981. }
  982. else if (clusterMarkerOptions &&
  983. clusterMarkerOptions.radius) {
  984. radius = clusterMarkerOptions.radius;
  985. }
  986. else {
  987. radius = clusterDefaultOptions.marker.radius;
  988. }
  989. offset = pointMaxDistance - radius >= 0 ?
  990. pointMaxDistance - radius : radius;
  991. if (distance > radius + offset &&
  992. defined(cluster.pointsOutside)) {
  993. cluster.pointsOutside.push(dataPoint);
  994. }
  995. else if (defined(cluster.pointsInside)) {
  996. cluster.pointsInside.push(dataPoint);
  997. }
  998. });
  999. if (cluster.pointsInside.length) {
  1000. group[cluster.id] = cluster.pointsInside;
  1001. }
  1002. cluster.pointsOutside.forEach(function (p, i) {
  1003. group[cluster.id + '_noise' + i] = [p];
  1004. });
  1005. });
  1006. series.baseClusters.noise.forEach(function (noise) {
  1007. group[noise.id] = noise.data;
  1008. });
  1009. }
  1010. return group;
  1011. }
  1012. };
  1013. Scatter.prototype.preventClusterCollisions = function (props) {
  1014. var series = this, xAxis = series.xAxis, yAxis = series.yAxis, _a = props.key.split('-').map(parseFloat), gridY = _a[0], gridX = _a[1], gridSize = props.gridSize, groupedData = props.groupedData, defaultRadius = props.defaultRadius, clusterRadius = props.clusterRadius, gridXPx = gridX * gridSize, gridYPx = gridY * gridSize, xPixel = xAxis.toPixels(props.x), yPixel = yAxis.toPixels(props.y), gridsToCheckCollision = [], pointsLen = 0, radius = 0, clusterMarkerOptions = (series.options.cluster || {}).marker, zoneOptions = (series.options.cluster || {}).zones, gridOffset = series.getGridOffset(), nextXPixel, nextYPixel, signX, signY, cornerGridX, cornerGridY, i, j, itemX, itemY, nextClusterPos, maxDist, keys, x, y;
  1015. // Distance to the grid start.
  1016. xPixel -= gridOffset.plotLeft;
  1017. yPixel -= gridOffset.plotTop;
  1018. for (i = 1; i < 5; i++) {
  1019. signX = i % 2 ? -1 : 1;
  1020. signY = i < 3 ? -1 : 1;
  1021. cornerGridX = Math.floor((xPixel + signX * clusterRadius) / gridSize);
  1022. cornerGridY = Math.floor((yPixel + signY * clusterRadius) / gridSize);
  1023. keys = [
  1024. cornerGridY + '-' + cornerGridX,
  1025. cornerGridY + '-' + gridX,
  1026. gridY + '-' + cornerGridX
  1027. ];
  1028. for (j = 0; j < keys.length; j++) {
  1029. if (gridsToCheckCollision.indexOf(keys[j]) === -1 &&
  1030. keys[j] !== props.key) {
  1031. gridsToCheckCollision.push(keys[j]);
  1032. }
  1033. }
  1034. }
  1035. gridsToCheckCollision.forEach(function (item) {
  1036. var _a;
  1037. if (groupedData[item]) {
  1038. // Cluster or noise position is already computed.
  1039. if (!groupedData[item].posX) {
  1040. nextClusterPos = getClusterPosition(groupedData[item]);
  1041. groupedData[item].posX = nextClusterPos.x;
  1042. groupedData[item].posY = nextClusterPos.y;
  1043. }
  1044. nextXPixel = xAxis.toPixels(groupedData[item].posX || 0) -
  1045. gridOffset.plotLeft;
  1046. nextYPixel = yAxis.toPixels(groupedData[item].posY || 0) -
  1047. gridOffset.plotTop;
  1048. _a = item.split('-').map(parseFloat), itemY = _a[0], itemX = _a[1];
  1049. if (zoneOptions) {
  1050. pointsLen = groupedData[item].length;
  1051. for (i = 0; i < zoneOptions.length; i++) {
  1052. if (pointsLen >= zoneOptions[i].from &&
  1053. pointsLen <= zoneOptions[i].to) {
  1054. if (defined((zoneOptions[i].marker || {}).radius)) {
  1055. radius = zoneOptions[i].marker.radius || 0;
  1056. }
  1057. else if (clusterMarkerOptions &&
  1058. clusterMarkerOptions.radius) {
  1059. radius = clusterMarkerOptions.radius;
  1060. }
  1061. else {
  1062. radius = clusterDefaultOptions.marker.radius;
  1063. }
  1064. }
  1065. }
  1066. }
  1067. if (groupedData[item].length > 1 &&
  1068. radius === 0 &&
  1069. clusterMarkerOptions &&
  1070. clusterMarkerOptions.radius) {
  1071. radius = clusterMarkerOptions.radius;
  1072. }
  1073. else if (groupedData[item].length === 1) {
  1074. radius = defaultRadius;
  1075. }
  1076. maxDist = clusterRadius + radius;
  1077. radius = 0;
  1078. if (itemX !== gridX &&
  1079. Math.abs(xPixel - nextXPixel) < maxDist) {
  1080. xPixel = itemX - gridX < 0 ? gridXPx + clusterRadius :
  1081. gridXPx + gridSize - clusterRadius;
  1082. }
  1083. if (itemY !== gridY &&
  1084. Math.abs(yPixel - nextYPixel) < maxDist) {
  1085. yPixel = itemY - gridY < 0 ? gridYPx + clusterRadius :
  1086. gridYPx + gridSize - clusterRadius;
  1087. }
  1088. }
  1089. });
  1090. x = xAxis.toValue(xPixel + gridOffset.plotLeft);
  1091. y = yAxis.toValue(yPixel + gridOffset.plotTop);
  1092. groupedData[props.key].posX = x;
  1093. groupedData[props.key].posY = y;
  1094. return { x: x, y: y };
  1095. };
  1096. // Check if user algorithm result is valid groupedDataObject.
  1097. Scatter.prototype.isValidGroupedDataObject = function (groupedData) {
  1098. var result = false, i;
  1099. if (!isObject(groupedData)) {
  1100. return false;
  1101. }
  1102. objectEach(groupedData, function (elem) {
  1103. result = true;
  1104. if (!isArray(elem) || !elem.length) {
  1105. result = false;
  1106. return;
  1107. }
  1108. for (i = 0; i < elem.length; i++) {
  1109. if (!isObject(elem[i]) || (!elem[i].x || !elem[i].y)) {
  1110. result = false;
  1111. return;
  1112. }
  1113. }
  1114. });
  1115. return result;
  1116. };
  1117. Scatter.prototype.getClusteredData = function (groupedData, options) {
  1118. var series = this, groupedXData = [], groupedYData = [], clusters = [], // Container for clusters.
  1119. noise = [], // Container for points not belonging to any cluster.
  1120. groupMap = [], index = 0,
  1121. // Prevent minimumClusterSize lower than 2.
  1122. minimumClusterSize = Math.max(2, options.minimumClusterSize || 2), stateId, point, points, pointUserOptions, pointsLen, marker, clusterPos, pointOptions, clusterTempPos, zoneOptions, clusterZone, clusterZoneClassName, i, k;
  1123. // Check if groupedData is valid when user uses a custom algorithm.
  1124. if (isFunction(options.layoutAlgorithm.type) &&
  1125. !series.isValidGroupedDataObject(groupedData)) {
  1126. error('Highcharts marker-clusters module: ' +
  1127. 'The custom algorithm result is not valid!', false, series.chart);
  1128. return false;
  1129. }
  1130. for (k in groupedData) {
  1131. if (groupedData[k].length >= minimumClusterSize) {
  1132. points = groupedData[k];
  1133. stateId = getStateId();
  1134. pointsLen = points.length;
  1135. // Get zone options for cluster.
  1136. if (options.zones) {
  1137. for (i = 0; i < options.zones.length; i++) {
  1138. if (pointsLen >= options.zones[i].from &&
  1139. pointsLen <= options.zones[i].to) {
  1140. clusterZone = options.zones[i];
  1141. clusterZone.zoneIndex = i;
  1142. zoneOptions = options.zones[i].marker;
  1143. clusterZoneClassName = options.zones[i].className;
  1144. }
  1145. }
  1146. }
  1147. clusterTempPos = getClusterPosition(points);
  1148. if (options.layoutAlgorithm.type === 'grid' &&
  1149. !options.allowOverlap) {
  1150. marker = series.options.marker || {};
  1151. clusterPos = series.preventClusterCollisions({
  1152. x: clusterTempPos.x,
  1153. y: clusterTempPos.y,
  1154. key: k,
  1155. groupedData: groupedData,
  1156. gridSize: series.getScaledGridSize(options.layoutAlgorithm),
  1157. defaultRadius: marker.radius || 3 + (marker.lineWidth || 0),
  1158. clusterRadius: (zoneOptions && zoneOptions.radius) ?
  1159. zoneOptions.radius :
  1160. (options.marker || {}).radius ||
  1161. clusterDefaultOptions.marker.radius
  1162. });
  1163. }
  1164. else {
  1165. clusterPos = {
  1166. x: clusterTempPos.x,
  1167. y: clusterTempPos.y
  1168. };
  1169. }
  1170. for (i = 0; i < pointsLen; i++) {
  1171. points[i].parentStateId = stateId;
  1172. }
  1173. clusters.push({
  1174. x: clusterPos.x,
  1175. y: clusterPos.y,
  1176. id: k,
  1177. stateId: stateId,
  1178. index: index,
  1179. data: points,
  1180. clusterZone: clusterZone,
  1181. clusterZoneClassName: clusterZoneClassName
  1182. });
  1183. groupedXData.push(clusterPos.x);
  1184. groupedYData.push(clusterPos.y);
  1185. groupMap.push({
  1186. options: {
  1187. formatPrefix: 'cluster',
  1188. dataLabels: options.dataLabels,
  1189. marker: merge(options.marker, {
  1190. states: options.states
  1191. }, zoneOptions || {})
  1192. }
  1193. });
  1194. // Save cluster data points options.
  1195. if (series.options.data && series.options.data.length) {
  1196. for (i = 0; i < pointsLen; i++) {
  1197. if (isObject(series.options.data[points[i].dataIndex])) {
  1198. points[i].options =
  1199. series.options.data[points[i].dataIndex];
  1200. }
  1201. }
  1202. }
  1203. index++;
  1204. zoneOptions = null;
  1205. }
  1206. else {
  1207. for (i = 0; i < groupedData[k].length; i++) {
  1208. // Points not belonging to any cluster.
  1209. point = groupedData[k][i];
  1210. stateId = getStateId();
  1211. pointOptions = null;
  1212. pointUserOptions =
  1213. ((series.options || {}).data || [])[point.dataIndex];
  1214. groupedXData.push(point.x);
  1215. groupedYData.push(point.y);
  1216. point.parentStateId = stateId;
  1217. noise.push({
  1218. x: point.x,
  1219. y: point.y,
  1220. id: k,
  1221. stateId: stateId,
  1222. index: index,
  1223. data: groupedData[k]
  1224. });
  1225. if (pointUserOptions &&
  1226. typeof pointUserOptions === 'object' &&
  1227. !isArray(pointUserOptions)) {
  1228. pointOptions = merge(pointUserOptions, { x: point.x, y: point.y });
  1229. }
  1230. else {
  1231. pointOptions = {
  1232. userOptions: pointUserOptions,
  1233. x: point.x,
  1234. y: point.y
  1235. };
  1236. }
  1237. groupMap.push({ options: pointOptions });
  1238. index++;
  1239. }
  1240. }
  1241. }
  1242. return {
  1243. clusters: clusters,
  1244. noise: noise,
  1245. groupedXData: groupedXData,
  1246. groupedYData: groupedYData,
  1247. groupMap: groupMap
  1248. };
  1249. };
  1250. // Destroy clustered data points.
  1251. Scatter.prototype.destroyClusteredData = function () {
  1252. var clusteredSeriesData = this.markerClusterSeriesData;
  1253. // Clear previous groups.
  1254. (clusteredSeriesData || []).forEach(function (point) {
  1255. if (point && point.destroy) {
  1256. point.destroy();
  1257. }
  1258. });
  1259. this.markerClusterSeriesData = null;
  1260. };
  1261. // Hide clustered data points.
  1262. Scatter.prototype.hideClusteredData = function () {
  1263. var series = this, clusteredSeriesData = this.markerClusterSeriesData, oldState = ((series.markerClusterInfo || {}).pointsState || {}).oldState || {}, oldPointsId = oldPointsStateId.map(function (elem) {
  1264. return (oldState[elem].point || {}).id || '';
  1265. });
  1266. (clusteredSeriesData || []).forEach(function (point) {
  1267. // If an old point is used in animation hide it, otherwise destroy.
  1268. if (point &&
  1269. oldPointsId.indexOf(point.id) !== -1) {
  1270. if (point.graphic) {
  1271. point.graphic.hide();
  1272. }
  1273. if (point.dataLabel) {
  1274. point.dataLabel.hide();
  1275. }
  1276. }
  1277. else {
  1278. if (point && point.destroy) {
  1279. point.destroy();
  1280. }
  1281. }
  1282. });
  1283. };
  1284. // Override the generatePoints method by adding a reference to grouped data.
  1285. Scatter.prototype.generatePoints = function () {
  1286. var series = this, chart = series.chart, xAxis = series.xAxis, yAxis = series.yAxis, clusterOptions = series.options.cluster, realExtremes = series.getRealExtremes(), visibleXData = [], visibleYData = [], visibleDataIndexes = [], oldPointsState, oldDataLen, oldMarkerClusterInfo, kmeansThreshold, cropDataOffsetX, cropDataOffsetY, seriesMinX, seriesMaxX, seriesMinY, seriesMaxY, type, algorithm, clusteredData, groupedData, layoutAlgOptions, point, i;
  1287. if (clusterOptions &&
  1288. clusterOptions.enabled &&
  1289. series.xData &&
  1290. series.yData &&
  1291. !chart.polar) {
  1292. type = clusterOptions.layoutAlgorithm.type;
  1293. layoutAlgOptions = clusterOptions.layoutAlgorithm;
  1294. // Get processed algorithm properties.
  1295. layoutAlgOptions.processedGridSize = relativeLength(layoutAlgOptions.gridSize ||
  1296. clusterDefaultOptions.layoutAlgorithm.gridSize, chart.plotWidth);
  1297. layoutAlgOptions.processedDistance = relativeLength(layoutAlgOptions.distance ||
  1298. clusterDefaultOptions.layoutAlgorithm.distance, chart.plotWidth);
  1299. kmeansThreshold = layoutAlgOptions.kmeansThreshold ||
  1300. clusterDefaultOptions.layoutAlgorithm.kmeansThreshold;
  1301. // Offset to prevent cluster size changes.
  1302. cropDataOffsetX = Math.abs(xAxis.toValue(layoutAlgOptions.processedGridSize / 2) -
  1303. xAxis.toValue(0));
  1304. cropDataOffsetY = Math.abs(yAxis.toValue(layoutAlgOptions.processedGridSize / 2) -
  1305. yAxis.toValue(0));
  1306. // Get only visible data.
  1307. for (i = 0; i < series.xData.length; i++) {
  1308. if (!series.dataMaxX) {
  1309. if (!defined(seriesMaxX) ||
  1310. !defined(seriesMinX) ||
  1311. !defined(seriesMaxY) ||
  1312. !defined(seriesMinY)) {
  1313. seriesMaxX = seriesMinX = series.xData[i];
  1314. seriesMaxY = seriesMinY = series.yData[i];
  1315. }
  1316. else if (isNumber(series.yData[i]) &&
  1317. isNumber(seriesMaxY) &&
  1318. isNumber(seriesMinY)) {
  1319. seriesMaxX = Math.max(series.xData[i], seriesMaxX);
  1320. seriesMinX = Math.min(series.xData[i], seriesMinX);
  1321. seriesMaxY = Math.max(series.yData[i] || seriesMaxY, seriesMaxY);
  1322. seriesMinY = Math.min(series.yData[i] || seriesMinY, seriesMinY);
  1323. }
  1324. }
  1325. // Crop data to visible ones with appropriate offset to prevent
  1326. // cluster size changes on the edge of the plot area.
  1327. if (series.xData[i] >= (realExtremes.minX - cropDataOffsetX) &&
  1328. series.xData[i] <= (realExtremes.maxX + cropDataOffsetX) &&
  1329. (series.yData[i] || realExtremes.minY) >=
  1330. (realExtremes.minY - cropDataOffsetY) &&
  1331. (series.yData[i] || realExtremes.maxY) <=
  1332. (realExtremes.maxY + cropDataOffsetY)) {
  1333. visibleXData.push(series.xData[i]);
  1334. visibleYData.push(series.yData[i]);
  1335. visibleDataIndexes.push(i);
  1336. }
  1337. }
  1338. // Save data max values.
  1339. if (defined(seriesMaxX) && defined(seriesMinX) &&
  1340. isNumber(seriesMaxY) && isNumber(seriesMinY)) {
  1341. series.dataMaxX = seriesMaxX;
  1342. series.dataMinX = seriesMinX;
  1343. series.dataMaxY = seriesMaxY;
  1344. series.dataMinY = seriesMinY;
  1345. }
  1346. if (isFunction(type)) {
  1347. algorithm = type;
  1348. }
  1349. else if (series.markerClusterAlgorithms) {
  1350. if (type && series.markerClusterAlgorithms[type]) {
  1351. algorithm = series.markerClusterAlgorithms[type];
  1352. }
  1353. else {
  1354. algorithm = visibleXData.length < kmeansThreshold ?
  1355. series.markerClusterAlgorithms.kmeans :
  1356. series.markerClusterAlgorithms.grid;
  1357. }
  1358. }
  1359. else {
  1360. algorithm = function () {
  1361. return false;
  1362. };
  1363. }
  1364. groupedData = algorithm.call(this, visibleXData, visibleYData, visibleDataIndexes, layoutAlgOptions);
  1365. clusteredData = groupedData ? series.getClusteredData(groupedData, clusterOptions) : groupedData;
  1366. // When animation is enabled get old points state.
  1367. if (clusterOptions.animation &&
  1368. series.markerClusterInfo &&
  1369. series.markerClusterInfo.pointsState &&
  1370. series.markerClusterInfo.pointsState.oldState) {
  1371. // Destroy old points.
  1372. destroyOldPoints(series.markerClusterInfo.pointsState.oldState);
  1373. oldPointsState = series.markerClusterInfo.pointsState.newState;
  1374. }
  1375. else {
  1376. oldPointsState = {};
  1377. }
  1378. // Save points old state info.
  1379. oldDataLen = series.xData.length;
  1380. oldMarkerClusterInfo = series.markerClusterInfo;
  1381. if (clusteredData) {
  1382. series.processedXData = clusteredData.groupedXData;
  1383. series.processedYData = clusteredData.groupedYData;
  1384. series.hasGroupedData = true;
  1385. series.markerClusterInfo = clusteredData;
  1386. series.groupMap = clusteredData.groupMap;
  1387. }
  1388. baseGeneratePoints.apply(this);
  1389. if (clusteredData && series.markerClusterInfo) {
  1390. // Mark cluster points. Safe point reference in the cluster object.
  1391. (series.markerClusterInfo.clusters || []).forEach(function (cluster) {
  1392. point = series.points[cluster.index];
  1393. point.isCluster = true;
  1394. point.clusteredData = cluster.data;
  1395. point.clusterPointsAmount = cluster.data.length;
  1396. cluster.point = point;
  1397. // Add zoom to cluster range.
  1398. addEvent(point, 'click', series.onDrillToCluster);
  1399. });
  1400. // Safe point reference in the noise object.
  1401. (series.markerClusterInfo.noise || []).forEach(function (noise) {
  1402. noise.point = series.points[noise.index];
  1403. });
  1404. // When animation is enabled save points state.
  1405. if (clusterOptions.animation &&
  1406. series.markerClusterInfo) {
  1407. series.markerClusterInfo.pointsState = {
  1408. oldState: oldPointsState,
  1409. newState: series.getPointsState(clusteredData, oldMarkerClusterInfo, oldDataLen)
  1410. };
  1411. }
  1412. // Record grouped data in order to let it be destroyed the next time
  1413. // processData runs.
  1414. if (!clusterOptions.animation) {
  1415. this.destroyClusteredData();
  1416. }
  1417. else {
  1418. this.hideClusteredData();
  1419. }
  1420. this.markerClusterSeriesData =
  1421. this.hasGroupedData ? this.points : null;
  1422. }
  1423. }
  1424. else {
  1425. baseGeneratePoints.apply(this);
  1426. }
  1427. };
  1428. // Handle animation.
  1429. addEvent(Chart, 'render', function () {
  1430. var chart = this;
  1431. (chart.series || []).forEach(function (series) {
  1432. if (series.markerClusterInfo) {
  1433. var options = series.options.cluster, pointsState = (series.markerClusterInfo || {}).pointsState, oldState = (pointsState || {}).oldState;
  1434. if ((options || {}).animation &&
  1435. series.markerClusterInfo &&
  1436. series.chart.pointer.pinchDown.length === 0 &&
  1437. (series.xAxis.eventArgs || {}).trigger !== 'pan' &&
  1438. oldState &&
  1439. Object.keys(oldState).length) {
  1440. series.markerClusterInfo.clusters.forEach(function (cluster) {
  1441. series.animateClusterPoint(cluster);
  1442. });
  1443. series.markerClusterInfo.noise.forEach(function (noise) {
  1444. series.animateClusterPoint(noise);
  1445. });
  1446. }
  1447. }
  1448. });
  1449. });
  1450. // Override point prototype to throw a warning when trying to update
  1451. // clustered point.
  1452. addEvent(Point, 'update', function () {
  1453. if (this.dataGroup) {
  1454. error('Highcharts marker-clusters module: ' +
  1455. 'Running `Point.update` when point belongs to clustered series' +
  1456. ' is not supported.', false, this.series.chart);
  1457. return false;
  1458. }
  1459. });
  1460. // Destroy grouped data on series destroy.
  1461. addEvent(Series, 'destroy', Scatter.prototype.destroyClusteredData);
  1462. // Add classes, change mouse cursor.
  1463. addEvent(Series, 'afterRender', function () {
  1464. var series = this, clusterZoomEnabled = (series.options.cluster || {}).drillToCluster;
  1465. if (series.markerClusterInfo && series.markerClusterInfo.clusters) {
  1466. series.markerClusterInfo.clusters.forEach(function (cluster) {
  1467. if (cluster.point && cluster.point.graphic) {
  1468. cluster.point.graphic.addClass('highcharts-cluster-point');
  1469. // Change cursor to pointer when drillToCluster is enabled.
  1470. if (clusterZoomEnabled && cluster.point) {
  1471. cluster.point.graphic.css({
  1472. cursor: 'pointer'
  1473. });
  1474. if (cluster.point.dataLabel) {
  1475. cluster.point.dataLabel.css({
  1476. cursor: 'pointer'
  1477. });
  1478. }
  1479. }
  1480. if (defined(cluster.clusterZone)) {
  1481. cluster.point.graphic.addClass(cluster.clusterZoneClassName ||
  1482. 'highcharts-cluster-zone-' +
  1483. cluster.clusterZone.zoneIndex);
  1484. }
  1485. }
  1486. });
  1487. }
  1488. });
  1489. addEvent(Point, 'drillToCluster', function (event) {
  1490. var point = event.point || event.target, series = point.series, clusterOptions = series.options.cluster, onDrillToCluster = ((clusterOptions || {}).events || {}).drillToCluster;
  1491. if (isFunction(onDrillToCluster)) {
  1492. onDrillToCluster.call(this, event);
  1493. }
  1494. });
  1495. // Destroy the old tooltip after zoom.
  1496. addEvent(H.Axis, 'setExtremes', function () {
  1497. var chart = this.chart, animationDuration = 0, animation;
  1498. chart.series.forEach(function (series) {
  1499. if (series.markerClusterInfo) {
  1500. animation = animObject((series.options.cluster || {}).animation);
  1501. animationDuration = animation.duration || 0;
  1502. }
  1503. });
  1504. syncTimeout(function () {
  1505. if (chart.tooltip) {
  1506. chart.tooltip.destroy();
  1507. }
  1508. }, animationDuration);
  1509. });