marker-clusters.src.js 75 KB

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