marker-clusters.src.js 80 KB

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