UNPKG

@amcharts/amcharts5

Version:
425 lines 19.1 kB
import { MapPointSeries } from "./MapPointSeries"; import { Component, DataItem } from "../../core/render/Component"; import { Container } from "../../core/render/Container"; import { Label } from "../../core/render/Label"; import * as $array from "../../core/util/Array"; import * as $object from "../../core/util/Object"; import * as d3hierarchy from "d3-hierarchy"; import * as $math from "../../core/util/Math"; /** * A version of [[MapPointSeries]] which can automatically group closely located * bullets into groups. * * @see {@link https://www.amcharts.com/docs/v5/charts/map-chart/clustered-point-series/} for more info * @since 5.5.6 * @important */ export class ClusteredPointSeries extends MapPointSeries { constructor() { super(...arguments); this._dataItem = this.makeDataItem({}); this._clusterIndex = 0; this._clusters = []; this.clusteredDataItems = []; this._scatterIndex = 0; this._scatters = []; this._packLayout = d3hierarchy.pack(); this._spiral = []; this._previousZL = 0; } _afterNew() { this.fields.push("groupId"); this._setRawDefault("groupIdField", "groupId"); super._afterNew(); } _afterDataChange() { super._afterDataChange(); this._previousZL = 0; } _updateChildren() { super._updateChildren(); if (this.isDirty("scatterRadius")) { this._spiral = $math.spiralPoints(0, 0, 300, 300, 0, 3, 3, 0, 0); } const chart = this.chart; if (chart) { const zoomLevel = chart.get("zoomLevel", 1); if (zoomLevel != this._previousZL) { const clusterDelay = this.get("clusterDelay", 0); if (clusterDelay) { if (this._clusterDP) { this._clusterDP.dispose(); this._clusterDP = this.setTimeout(() => { this._doTheCluster(); }, clusterDelay); } else { // first time without delay this._doTheCluster(); this._clusterDP = this.setTimeout(() => { }, 0); } } else { this._doTheCluster(); } this._previousZL = zoomLevel; } $array.each(this.clusteredDataItems, (dataItem) => { const bullet = dataItem.get("bullet"); const longitude = dataItem.get("longitude", 0); const latitude = dataItem.get("latitude", 0); this._positionBulletReal(bullet, { type: "Point", coordinates: [longitude, latitude] }, [longitude, latitude]); }); } } _doTheCluster() { const groups = {}; // distribute to groups $array.each(this.dataItems, (dataItem) => { const groupId = dataItem.get("groupId", "_default"); if (!groups[groupId]) { groups[groupId] = []; } groups[groupId].push(dataItem); }); this._scatterIndex = -1; this._scatters = []; this._clusterIndex = -1; this._clusters = []; $array.each(this.clusteredDataItems, (dataItem) => { dataItem.setRaw("children", undefined); }); $array.each(this.dataItems, (dataItem) => { dataItem.setRaw("cluster", undefined); }); $object.each(groups, (_key, group) => { this._scatterGroup(group); }); $object.each(groups, (_key, group) => { this._clusterGroup(group); }); $array.each(this.dataItems, (dataItem) => { if (!dataItem.get("cluster")) { const bullets = dataItem.bullets; if (bullets) { $array.each(bullets, (bullet) => { const sprite = bullet.get("sprite"); if (sprite) { sprite.set("forceHidden", false); } }); } } }); } /** * Zooms to the area so that all clustered data items of a cluster would be * visible. * * Pass in `true` as a second parameter to rotate that map so that the group * is in the center. This is especially useful in the maps that use * Orthographic (globe) projection. * * @param dataItem Group data item * @param rotate Rotate the map so that group is in the center? * @see {@link https://www.amcharts.com/docs/v5/charts/map-chart/clustered-point-series/#Drill_down} for more info */ zoomToCluster(dataItem, rotate) { this.zoomToDataItems(dataItem.get("children", []), rotate); } _clusterGroup(dataItems) { const chart = this.chart; if (chart && chart.get("zoomLevel", 1) >= chart.get("maxZoomLevel", 100) * this.get("stopClusterZoom", 0.95)) { // void } else { const minDistance = this.get("minDistance", 20); const minDistSq = minDistance * minDistance; // Pre-filter to candidates with valid points const candidates = []; for (let i = 0; i < dataItems.length; i++) { const di = dataItems[i]; if (!di.get("clipped")) { const pt = di.get("point"); if (pt) { candidates.push({ di: di, x: pt.x, y: pt.y }); } } } // Spatial grid: cell size = minDistance so neighbors are always in adjacent cells const grid = new Map(); for (let i = 0; i < candidates.length; i++) { const c = candidates[i]; const key = Math.floor(c.x / minDistance) + "," + Math.floor(c.y / minDistance); let cell = grid.get(key); if (!cell) { cell = []; grid.set(key, cell); } cell.push(i); } const visited = new Uint8Array(candidates.length); for (let i = 0; i < candidates.length; i++) { if (visited[i]) continue; visited[i] = 1; this._clusterIndex++; this._clusters[this._clusterIndex] = []; const cluster = this._clusters[this._clusterIndex]; const seed = candidates[i]; cluster.push(seed.di); // BFS: only check 9 neighboring cells per expansion const queue = [seed]; let head = 0; while (head < queue.length) { const pt = queue[head++]; const cx = Math.floor(pt.x / minDistance); const cy = Math.floor(pt.y / minDistance); for (let dcx = -1; dcx <= 1; dcx++) { for (let dcy = -1; dcy <= 1; dcy++) { const cell = grid.get((cx + dcx) + "," + (cy + dcy)); if (cell) { for (let k = 0; k < cell.length; k++) { const idx = cell[k]; if (visited[idx]) continue; const c = candidates[idx]; const dx = c.x - pt.x; const dy = c.y - pt.y; if (dx * dx + dy * dy < minDistSq) { visited[idx] = 1; cluster.push(c.di); queue.push(c); } } } } } } } } let i = 0; const bulletMethod = this.get("clusteredBullet"); if (bulletMethod) { $array.each(this._clusters, (cluster) => { let sumX = 0; let sumY = 0; let len = cluster.length; if (len > 1) { let clusteredDataItem = this.clusteredDataItems[i]; if (!clusteredDataItem) { clusteredDataItem = new DataItem(this, undefined, {}); const bullet = clusteredDataItem.set("bullet", bulletMethod(this._root, this, clusteredDataItem)); if (bullet) { const sprite = bullet.get("sprite"); if (sprite) { this.bulletsContainer.children.push(sprite); sprite._setDataItem(clusteredDataItem); this.root.events.once("frameended", () => { if (sprite instanceof Container) { sprite.walkChildren((child) => { if (child instanceof Component) { child.markDirtyValues(); } }); } }); } } this.clusteredDataItems.push(clusteredDataItem); } let groupId; $array.each(cluster, (dataItem) => { dataItem.setRaw("cluster", clusteredDataItem); const point = dataItem.get("point"); if (point) { sumX += point.x; sumY += point.y; } const bullets = dataItem.bullets; if (bullets) { $array.each(bullets, (bullet) => { const sprite = bullet.get("sprite"); if (sprite) { sprite.set("forceHidden", true); } }); } groupId = dataItem.get("groupId"); }); let averageX = sumX / len; let averageY = sumY / len; clusteredDataItem.setRaw("children", cluster); clusteredDataItem.setRaw("groupId", groupId); const prevLen = clusteredDataItem.get("value"); clusteredDataItem.setRaw("value", len); const bullet = clusteredDataItem.get("bullet"); if (bullet) { let geoPoint = this.chart.invert({ x: averageX, y: averageY }); if (geoPoint) { clusteredDataItem.setAll({ longitude: geoPoint.longitude, latitude: geoPoint.latitude, }); } this._positionBullets(clusteredDataItem); const sprite = bullet.get("sprite"); if (sprite) { sprite.set("forceHidden", false); //sprite.setAll({ x: averageX, y: averageY }); if (prevLen != len) { if (sprite instanceof Container) { sprite.walkChildren((child) => { if (child instanceof Label) { child.text.markDirtyText(); } }); } } } } i++; } }); } $array.each(this.clusteredDataItems, (dataItem) => { let children = dataItem.get("children"); if (!children || children.length == 0) { const bullet = dataItem.get("bullet"); if (bullet) { const sprite = bullet.get("sprite"); if (sprite) { sprite.set("forceHidden", true); } } } }); } _onDataClear() { super._onDataClear(); $array.each(this.clusteredDataItems, (dataItem) => { const bullet = dataItem.get("bullet"); if (bullet) { const sprite = bullet.get("sprite"); if (sprite) { sprite.dispose(); } } }); this.clusteredDataItems = []; } _scatterGroup(dataItems) { const chart = this.chart; if (chart && chart.get("zoomLevel", 1) >= chart.get("maxZoomLevel", 100) * this.get("stopClusterZoom", 0.95)) { const scatterDistance = this.get("scatterDistance", 5); const scatterDistSq = scatterDistance * scatterDistance; // Pre-filter to candidates with valid points const candidates = []; for (let i = 0; i < dataItems.length; i++) { const di = dataItems[i]; if (!di.get("clipped")) { const pt = di.get("point"); if (pt) { candidates.push({ di: di, x: pt.x, y: pt.y }); } } } // Spatial grid: cell size = scatterDistance const grid = new Map(); for (let i = 0; i < candidates.length; i++) { const c = candidates[i]; const key = Math.floor(c.x / scatterDistance) + "," + Math.floor(c.y / scatterDistance); let cell = grid.get(key); if (!cell) { cell = []; grid.set(key, cell); } cell.push(i); } const visited = new Uint8Array(candidates.length); for (let i = 0; i < candidates.length; i++) { if (visited[i]) continue; visited[i] = 1; this._scatterIndex++; this._scatters[this._scatterIndex] = []; const scatter = this._scatters[this._scatterIndex]; const seed = candidates[i]; scatter.push(seed.di); const queue = [seed]; let head = 0; while (head < queue.length) { const pt = queue[head++]; const cx = Math.floor(pt.x / scatterDistance); const cy = Math.floor(pt.y / scatterDistance); for (let dcx = -1; dcx <= 1; dcx++) { for (let dcy = -1; dcy <= 1; dcy++) { const cell = grid.get((cx + dcx) + "," + (cy + dcy)); if (cell) { for (let k = 0; k < cell.length; k++) { const idx = cell[k]; if (visited[idx]) continue; const c = candidates[idx]; const dx = c.x - pt.x; const dy = c.y - pt.y; if (dx * dx + dy * dy < scatterDistSq) { visited[idx] = 1; scatter.push(c.di); queue.push(c); } } } } } } } $array.each(this._scatters, (scatter) => { let len = scatter.length; if (len > 1) { let previousCircles = []; let s = 0; let radius = this.get("scatterRadius", 8); $array.each(scatter, (dataItem) => { let spiralPoint = this._spiral[s]; let intersects = true; if (previousCircles.length > 0) { while (intersects) { $array.each(previousCircles, (previousCircle) => { intersects = false; while ($math.circlesOverlap({ x: spiralPoint.x, y: spiralPoint.y, radius: radius }, previousCircle)) { s++; if (this._spiral[s] == undefined) { intersects = false; } else { intersects = true; spiralPoint = this._spiral[s]; } } }); } } const dx = spiralPoint.x; const dy = spiralPoint.y; previousCircles.push({ x: dx, y: dy, radius: radius }); dataItem.set("dx", dx); dataItem.set("dy", dy); const bullets = dataItem.bullets; if (bullets) { $array.each(bullets, (bullet) => { const sprite = bullet.get("sprite"); if (sprite) { sprite.setAll({ dx: dx, dy: dy }); } }); } }); } }); } } } ClusteredPointSeries.className = "ClusteredPointSeries"; ClusteredPointSeries.classNames = MapPointSeries.classNames.concat([ClusteredPointSeries.className]); //# sourceMappingURL=ClusteredPointSeries.js.map