@flowmap.gl/data 8.0.0-alpha.9 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +3 -0
- package/.turbo/turbo-dev.log +6 -0
- package/LICENSE +2 -2
- package/dist/FlowmapAggregateAccessors.d.ts +4 -4
- package/dist/FlowmapAggregateAccessors.d.ts.map +1 -1
- package/dist/FlowmapAggregateAccessors.js +16 -9
- package/dist/FlowmapSelectors.d.ts +41 -87
- package/dist/FlowmapSelectors.d.ts.map +1 -1
- package/dist/FlowmapSelectors.js +174 -161
- package/dist/FlowmapState.d.ts +7 -5
- package/dist/FlowmapState.d.ts.map +1 -1
- package/dist/FlowmapState.js +6 -1
- package/dist/cluster/ClusterIndex.d.ts +4 -4
- package/dist/cluster/ClusterIndex.d.ts.map +1 -1
- package/dist/cluster/ClusterIndex.js +5 -17
- package/dist/cluster/cluster.d.ts +25 -5
- package/dist/cluster/cluster.d.ts.map +1 -1
- package/dist/cluster/cluster.js +115 -57
- package/dist/colors.d.ts +3 -3
- package/dist/colors.d.ts.map +1 -1
- package/dist/colors.js +19 -8
- package/dist/getViewStateForLocations.d.ts +3 -3
- package/dist/getViewStateForLocations.d.ts.map +1 -1
- package/dist/getViewStateForLocations.js +33 -12
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/provider/FlowmapDataProvider.d.ts +7 -2
- package/dist/provider/FlowmapDataProvider.d.ts.map +1 -1
- package/dist/provider/FlowmapDataProvider.js +11 -6
- package/dist/provider/LocalFlowmapDataProvider.d.ts +15 -4
- package/dist/provider/LocalFlowmapDataProvider.d.ts.map +1 -1
- package/dist/provider/LocalFlowmapDataProvider.js +98 -81
- package/dist/selector-functions.d.ts +10 -0
- package/dist/selector-functions.d.ts.map +1 -0
- package/dist/selector-functions.js +65 -0
- package/dist/time.d.ts.map +1 -1
- package/dist/time.js +6 -1
- package/dist/types.d.ts +20 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -4
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +6 -1
- package/package.json +22 -27
- package/src/FlowmapAggregateAccessors.ts +21 -10
- package/src/FlowmapSelectors.ts +304 -280
- package/src/FlowmapState.ts +13 -5
- package/src/cluster/ClusterIndex.ts +23 -28
- package/src/cluster/cluster.ts +165 -73
- package/src/colors.ts +13 -9
- package/src/getViewStateForLocations.ts +23 -7
- package/src/index.ts +9 -3
- package/src/provider/FlowmapDataProvider.ts +23 -7
- package/src/provider/LocalFlowmapDataProvider.ts +68 -5
- package/src/selector-functions.ts +93 -0
- package/src/time.ts +6 -0
- package/src/types.ts +23 -15
- package/src/util.ts +6 -0
- package/dist/provider/WorkerFlowmapDataProvider.d.ts +0 -42
- package/dist/provider/WorkerFlowmapDataProvider.d.ts.map +0 -1
- package/dist/provider/WorkerFlowmapDataProvider.js +0 -80
- package/dist/provider/WorkerFlowmapDataProviderWorker.d.ts +0 -2
- package/dist/provider/WorkerFlowmapDataProviderWorker.d.ts.map +0 -1
- package/dist/provider/WorkerFlowmapDataProviderWorker.js +0 -4
- package/dist/provider/createWorkerDataProvider.d.ts +0 -3
- package/dist/provider/createWorkerDataProvider.d.ts.map +0 -1
- package/dist/provider/createWorkerDataProvider.js +0 -21
- package/src/provider/WorkerFlowmapDataProvider.ts +0 -121
- package/src/provider/WorkerFlowmapDataProviderWorker.ts +0 -4
- package/src/provider/createWorkerDataProvider.ts +0 -18
package/src/FlowmapState.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Flowmap.gl contributors
|
|
3
|
+
* Copyright (c) 2018-2020 Teralytics
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import {LocationFilterMode, ViewportProps} from './types';
|
|
2
8
|
|
|
3
9
|
export interface FilterState {
|
|
4
|
-
selectedLocations
|
|
5
|
-
|
|
6
|
-
|
|
10
|
+
selectedLocations?: (string | number)[];
|
|
11
|
+
locationFilterMode?: LocationFilterMode;
|
|
12
|
+
selectedTimeRange?: [Date, Date];
|
|
7
13
|
}
|
|
8
14
|
|
|
9
15
|
export interface SettingsState {
|
|
10
16
|
animationEnabled: boolean;
|
|
11
17
|
fadeEnabled: boolean;
|
|
12
18
|
fadeOpacityEnabled: boolean;
|
|
19
|
+
locationsEnabled: boolean;
|
|
13
20
|
locationTotalsEnabled: boolean;
|
|
21
|
+
locationLabelsEnabled: boolean;
|
|
14
22
|
adaptiveScalesEnabled: boolean;
|
|
15
23
|
clusteringEnabled: boolean;
|
|
16
24
|
clusteringAuto: boolean;
|
|
@@ -23,7 +31,7 @@ export interface SettingsState {
|
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
export interface FlowmapState {
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
filter?: FilterState;
|
|
35
|
+
settings: SettingsState;
|
|
28
36
|
viewport: ViewportProps;
|
|
29
37
|
}
|
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright
|
|
3
|
-
* Copyright 2018-2020 Teralytics
|
|
4
|
-
*
|
|
5
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
* you may not use this file except in compliance with the License.
|
|
7
|
-
* You may obtain a copy of the License at
|
|
8
|
-
*
|
|
9
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
*
|
|
11
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
-
* See the License for the specific language governing permissions and
|
|
15
|
-
* limitations under the License.
|
|
16
|
-
*
|
|
2
|
+
* Copyright (c) Flowmap.gl contributors
|
|
3
|
+
* Copyright (c) 2018-2020 Teralytics
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
17
5
|
*/
|
|
18
6
|
|
|
19
7
|
import {
|
|
@@ -27,14 +15,14 @@ import {
|
|
|
27
15
|
} from './../types';
|
|
28
16
|
import {ascending, bisectLeft, extent} from 'd3-array';
|
|
29
17
|
|
|
30
|
-
export type LocationWeightGetter = (id: string) => number;
|
|
18
|
+
export type LocationWeightGetter = (id: string | number) => number;
|
|
31
19
|
|
|
32
20
|
/**
|
|
33
21
|
* A data structure representing the cluster levels for efficient flow aggregation.
|
|
34
22
|
*/
|
|
35
23
|
export interface ClusterIndex<F> {
|
|
36
24
|
availableZoomLevels: number[];
|
|
37
|
-
getClusterById: (clusterId: string) => Cluster | undefined;
|
|
25
|
+
getClusterById: (clusterId: string | number) => Cluster | undefined;
|
|
38
26
|
/**
|
|
39
27
|
* List the nodes on the given zoom level.
|
|
40
28
|
*/
|
|
@@ -42,7 +30,7 @@ export interface ClusterIndex<F> {
|
|
|
42
30
|
/**
|
|
43
31
|
* Get the min zoom level on which the location is not clustered.
|
|
44
32
|
*/
|
|
45
|
-
getMinZoomForLocation: (locationId: string) => number;
|
|
33
|
+
getMinZoomForLocation: (locationId: string | number) => number;
|
|
46
34
|
/**
|
|
47
35
|
* List the IDs of all locations in the cluster (leaves of the subtree starting in the cluster).
|
|
48
36
|
*/
|
|
@@ -50,7 +38,10 @@ export interface ClusterIndex<F> {
|
|
|
50
38
|
/**
|
|
51
39
|
* Find the cluster the given location is residing in on the specified zoom level.
|
|
52
40
|
*/
|
|
53
|
-
findClusterFor: (
|
|
41
|
+
findClusterFor: (
|
|
42
|
+
locationId: string | number,
|
|
43
|
+
zoom: number,
|
|
44
|
+
) => string | number | undefined;
|
|
54
45
|
/**
|
|
55
46
|
* Aggregate flows for the specified zoom level.
|
|
56
47
|
*/
|
|
@@ -69,8 +60,8 @@ export interface ClusterIndex<F> {
|
|
|
69
60
|
*/
|
|
70
61
|
export function buildIndex<F>(clusterLevels: ClusterLevels): ClusterIndex<F> {
|
|
71
62
|
const nodesByZoom = new Map<number, ClusterNode[]>();
|
|
72
|
-
const clustersById = new Map<string, Cluster>();
|
|
73
|
-
const minZoomByLocationId = new Map<string, number>();
|
|
63
|
+
const clustersById = new Map<string | number, Cluster>();
|
|
64
|
+
const minZoomByLocationId = new Map<string | number, number>();
|
|
74
65
|
for (const {zoom, nodes} of clusterLevels) {
|
|
75
66
|
nodesByZoom.set(zoom, nodes);
|
|
76
67
|
for (const node of nodes) {
|
|
@@ -91,7 +82,10 @@ export function buildIndex<F>(clusterLevels: ClusterLevels): ClusterIndex<F> {
|
|
|
91
82
|
throw new Error('Could not determine minZoom or maxZoom');
|
|
92
83
|
}
|
|
93
84
|
|
|
94
|
-
const leavesToClustersByZoom = new Map<
|
|
85
|
+
const leavesToClustersByZoom = new Map<
|
|
86
|
+
number,
|
|
87
|
+
Map<string | number, Cluster>
|
|
88
|
+
>();
|
|
95
89
|
|
|
96
90
|
for (const cluster of clustersById.values()) {
|
|
97
91
|
const {zoom} = cluster;
|
|
@@ -118,7 +112,7 @@ export function buildIndex<F>(clusterLevels: ClusterLevels): ClusterIndex<F> {
|
|
|
118
112
|
|
|
119
113
|
const expandCluster = (cluster: Cluster, targetZoom: number = maxZoom) => {
|
|
120
114
|
const ids: string[] = [];
|
|
121
|
-
const visit = (c: Cluster, expandedIds: string[]) => {
|
|
115
|
+
const visit = (c: Cluster, expandedIds: (string | number)[]) => {
|
|
122
116
|
if (targetZoom > c.zoom) {
|
|
123
117
|
for (const childId of c.children) {
|
|
124
118
|
const child = clustersById.get(childId);
|
|
@@ -136,7 +130,7 @@ export function buildIndex<F>(clusterLevels: ClusterLevels): ClusterIndex<F> {
|
|
|
136
130
|
return ids;
|
|
137
131
|
};
|
|
138
132
|
|
|
139
|
-
function findClusterFor(locationId: string, zoom: number) {
|
|
133
|
+
function findClusterFor(locationId: string | number, zoom: number) {
|
|
140
134
|
const leavesToClusters = leavesToClustersByZoom.get(zoom);
|
|
141
135
|
if (!leavesToClusters) {
|
|
142
136
|
return undefined;
|
|
@@ -179,7 +173,8 @@ export function buildIndex<F>(clusterLevels: ClusterLevels): ClusterIndex<F> {
|
|
|
179
173
|
}
|
|
180
174
|
const result: (F | AggregateFlow)[] = [];
|
|
181
175
|
const aggFlowsByKey = new Map<string, AggregateFlow>();
|
|
182
|
-
const makeKey = (origin: string, dest: string) =>
|
|
176
|
+
const makeKey = (origin: string | number, dest: string | number) =>
|
|
177
|
+
`${origin}:${dest}`;
|
|
183
178
|
const {
|
|
184
179
|
flowCountsMapReduce = {
|
|
185
180
|
map: getFlowMagnitude,
|
|
@@ -223,8 +218,8 @@ export function makeLocationWeightGetter<F>(
|
|
|
223
218
|
{getFlowOriginId, getFlowDestId, getFlowMagnitude}: FlowAccessors<F>,
|
|
224
219
|
): LocationWeightGetter {
|
|
225
220
|
const locationTotals = {
|
|
226
|
-
incoming: new Map<string, number>(),
|
|
227
|
-
outgoing: new Map<string, number>(),
|
|
221
|
+
incoming: new Map<string | number, number>(),
|
|
222
|
+
outgoing: new Map<string | number, number>(),
|
|
228
223
|
};
|
|
229
224
|
for (const flow of flows) {
|
|
230
225
|
const origin = getFlowOriginId(flow);
|
|
@@ -239,7 +234,7 @@ export function makeLocationWeightGetter<F>(
|
|
|
239
234
|
(locationTotals.outgoing.get(origin) || 0) + count,
|
|
240
235
|
);
|
|
241
236
|
}
|
|
242
|
-
return (id: string) =>
|
|
237
|
+
return (id: string | number) =>
|
|
243
238
|
Math.max(
|
|
244
239
|
Math.abs(locationTotals.incoming.get(id) || 0),
|
|
245
240
|
Math.abs(locationTotals.outgoing.get(id) || 0),
|
package/src/cluster/cluster.ts
CHANGED
|
@@ -1,46 +1,34 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright
|
|
3
|
-
* Copyright 2018-2020 Teralytics
|
|
4
|
-
*
|
|
5
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
* you may not use this file except in compliance with the License.
|
|
7
|
-
* You may obtain a copy of the License at
|
|
8
|
-
*
|
|
9
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
*
|
|
11
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
-
* See the License for the specific language governing permissions and
|
|
15
|
-
* limitations under the License.
|
|
16
|
-
*
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The code in this file is a based on https://github.com/mapbox/supercluster
|
|
2
|
+
* Copyright (c) Flowmap.gl contributors
|
|
3
|
+
* Copyright (c) 2018-2020 Teralytics
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
21
5
|
*/
|
|
22
6
|
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
// Copyright (c) 2016, Mapbox
|
|
26
|
-
//
|
|
27
|
-
// Permission to use, copy, modify, and/or distribute this software for any purpose
|
|
28
|
-
// with or without fee is hereby granted, provided that the above copyright notice
|
|
29
|
-
// and this permission notice appear in all copies.
|
|
30
|
-
//
|
|
31
|
-
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
32
|
-
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
33
|
-
// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
34
|
-
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
|
35
|
-
// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
36
|
-
// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
37
|
-
// THIS SOFTWARE.
|
|
38
|
-
|
|
39
|
-
import {rollup} from 'd3-array';
|
|
7
|
+
import {min, rollup} from 'd3-array';
|
|
40
8
|
import KDBush from 'kdbush';
|
|
41
9
|
import {LocationWeightGetter} from './ClusterIndex';
|
|
42
10
|
import {Cluster, ClusterLevel, ClusterNode, LocationAccessors} from '../types';
|
|
43
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The code in this file is a based on https://github.com/mapbox/supercluster
|
|
14
|
+
*
|
|
15
|
+
* ISC License
|
|
16
|
+
*
|
|
17
|
+
* Copyright (c) 2016, Mapbox
|
|
18
|
+
*
|
|
19
|
+
* Permission to use, copy, modify, and/or distribute this software for any purpose
|
|
20
|
+
* with or without fee is hereby granted, provided that the above copyright notice
|
|
21
|
+
* and this permission notice appear in all copies.
|
|
22
|
+
*
|
|
23
|
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
24
|
+
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
25
|
+
* FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
26
|
+
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
|
27
|
+
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
28
|
+
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
29
|
+
* THIS SOFTWARE.
|
|
30
|
+
*/
|
|
31
|
+
|
|
44
32
|
export interface Options {
|
|
45
33
|
minZoom: number; // min zoom to generate clusters on
|
|
46
34
|
maxZoom: number; // max zoom level to cluster the points on
|
|
@@ -69,8 +57,9 @@ interface BasePoint {
|
|
|
69
57
|
parentId: number; // parent cluster id
|
|
70
58
|
}
|
|
71
59
|
|
|
72
|
-
interface LeafPoint extends BasePoint {
|
|
60
|
+
interface LeafPoint<L> extends BasePoint {
|
|
73
61
|
index: number; // index of the source feature in the original input array,
|
|
62
|
+
location: L;
|
|
74
63
|
}
|
|
75
64
|
|
|
76
65
|
interface ClusterPoint extends BasePoint {
|
|
@@ -78,14 +67,14 @@ interface ClusterPoint extends BasePoint {
|
|
|
78
67
|
numPoints: number;
|
|
79
68
|
}
|
|
80
69
|
|
|
81
|
-
type Point = LeafPoint | ClusterPoint;
|
|
70
|
+
type Point<L> = LeafPoint<L> | ClusterPoint;
|
|
82
71
|
|
|
83
|
-
export function isLeafPoint(p: Point): p is LeafPoint {
|
|
84
|
-
const {index} = p as LeafPoint
|
|
72
|
+
export function isLeafPoint<L>(p: Point<L>): p is LeafPoint<L> {
|
|
73
|
+
const {index} = p as LeafPoint<L>;
|
|
85
74
|
return index != null;
|
|
86
75
|
}
|
|
87
76
|
|
|
88
|
-
export function isClusterPoint(p: Point): p is ClusterPoint {
|
|
77
|
+
export function isClusterPoint<L>(p: Point<L>): p is ClusterPoint {
|
|
89
78
|
const {id} = p as ClusterPoint;
|
|
90
79
|
return id != null;
|
|
91
80
|
}
|
|
@@ -93,7 +82,7 @@ export function isClusterPoint(p: Point): p is ClusterPoint {
|
|
|
93
82
|
type ZoomLevelKDBush = any;
|
|
94
83
|
|
|
95
84
|
export function clusterLocations<L>(
|
|
96
|
-
locations: L
|
|
85
|
+
locations: Iterable<L>,
|
|
97
86
|
locationAccessors: LocationAccessors<L>,
|
|
98
87
|
getLocationWeight: LocationWeightGetter,
|
|
99
88
|
options?: Partial<Options>,
|
|
@@ -108,51 +97,101 @@ export function clusterLocations<L>(
|
|
|
108
97
|
const trees = new Array<ZoomLevelKDBush>(maxZoom + 1);
|
|
109
98
|
|
|
110
99
|
// generate a cluster object for each point and index input points into a KD-tree
|
|
111
|
-
let clusters = new Array<Point
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
100
|
+
let clusters = new Array<Point<L>>();
|
|
101
|
+
let locationsCount = 0;
|
|
102
|
+
for (const location of locations) {
|
|
103
|
+
const x = getLocationLon(location);
|
|
104
|
+
const y = getLocationLat(location);
|
|
115
105
|
clusters.push({
|
|
116
106
|
x: lngX(x), // projected point coordinates
|
|
117
107
|
y: latY(y),
|
|
118
|
-
weight: getLocationWeight(getLocationId(
|
|
108
|
+
weight: getLocationWeight(getLocationId(location)),
|
|
119
109
|
zoom: Infinity, // the last zoom the point was processed at
|
|
120
|
-
index:
|
|
110
|
+
index: locationsCount, // index of the source feature in the original input array,
|
|
121
111
|
parentId: -1, // parent cluster id
|
|
112
|
+
location,
|
|
122
113
|
});
|
|
114
|
+
locationsCount++;
|
|
123
115
|
}
|
|
124
|
-
|
|
116
|
+
|
|
117
|
+
const makeBush = (points: Point<L>[]) => {
|
|
118
|
+
const bush = new KDBush(points.length, nodeSize, Float32Array);
|
|
119
|
+
for (let i = 0; i < points.length; i++) {
|
|
120
|
+
bush.add(points[i].x, points[i].y);
|
|
121
|
+
}
|
|
122
|
+
bush.finish();
|
|
123
|
+
bush.points = points;
|
|
124
|
+
return bush;
|
|
125
|
+
};
|
|
125
126
|
|
|
126
127
|
// cluster points on max zoom, then cluster the results on previous zoom, etc.;
|
|
127
128
|
// results in a cluster hierarchy across zoom levels
|
|
129
|
+
trees[maxZoom + 1] = makeBush(clusters);
|
|
130
|
+
let prevZoom = maxZoom + 1;
|
|
131
|
+
|
|
128
132
|
for (let z = maxZoom; z >= minZoom; z--) {
|
|
129
133
|
// create a new set of clusters for the zoom and index them with a KD-tree
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
const _clusters = cluster(clusters, z, trees[prevZoom], opts);
|
|
135
|
+
if (_clusters.length === clusters.length) {
|
|
136
|
+
// same number of clusters => move the higher level clusters up
|
|
137
|
+
// no need to keep the same data on multiple levels
|
|
138
|
+
trees[z] = trees[prevZoom];
|
|
139
|
+
trees[prevZoom] = undefined;
|
|
140
|
+
prevZoom = z;
|
|
141
|
+
clusters = _clusters;
|
|
142
|
+
} else {
|
|
143
|
+
prevZoom = z;
|
|
144
|
+
clusters = _clusters;
|
|
145
|
+
trees[z] = makeBush(clusters);
|
|
146
|
+
}
|
|
132
147
|
}
|
|
133
148
|
|
|
134
149
|
if (trees.length === 0) {
|
|
135
150
|
return [];
|
|
136
151
|
}
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
152
|
+
|
|
153
|
+
const numbersOfClusters: number[] = trees.map((d) => d?.points.length);
|
|
154
|
+
const minClusters = min(numbersOfClusters.filter((d) => d > 0));
|
|
155
|
+
|
|
156
|
+
let maxAvailZoom =
|
|
157
|
+
findIndexOfMax(numbersOfClusters) ?? numbersOfClusters.length - 1;
|
|
158
|
+
|
|
159
|
+
const numUniqueLocations = countUniqueLocations(locations, locationAccessors);
|
|
160
|
+
if (numUniqueLocations < locationsCount) {
|
|
161
|
+
// Duplicate locations would be clustered together at any zoom level which can lead to having too many zooms.
|
|
162
|
+
// To avoid that, we need to find the max zoom level that has less or equal clusters than unique locations
|
|
163
|
+
// and drop all zoom levels beyond that (except the unclustered level).
|
|
164
|
+
const maxClustersZoom = findLastIndex(
|
|
165
|
+
numbersOfClusters,
|
|
166
|
+
(d) => d <= numUniqueLocations,
|
|
167
|
+
);
|
|
168
|
+
if (maxClustersZoom >= 0) {
|
|
169
|
+
// Now, move the unclustered points to the next zoom level to avoid having a gap
|
|
170
|
+
if (maxClustersZoom < maxAvailZoom) {
|
|
171
|
+
trees[maxClustersZoom + 1] = trees[maxAvailZoom];
|
|
172
|
+
trees.splice(maxClustersZoom + 2); // Remove all zoom levels beyond maxClustersZoom
|
|
173
|
+
}
|
|
174
|
+
maxAvailZoom = maxClustersZoom + 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
141
178
|
const minAvailZoom = Math.min(
|
|
142
179
|
maxAvailZoom,
|
|
143
|
-
numbersOfClusters.lastIndexOf(
|
|
180
|
+
minClusters ? numbersOfClusters.lastIndexOf(minClusters) : maxAvailZoom,
|
|
144
181
|
);
|
|
145
182
|
|
|
146
183
|
const clusterLevels = new Array<ClusterLevel>();
|
|
147
|
-
|
|
148
|
-
|
|
184
|
+
prevZoom = NaN;
|
|
185
|
+
for (let zoom = maxAvailZoom; zoom >= minAvailZoom; zoom--) {
|
|
186
|
+
let childrenByParent: Map<number, (string | number)[]> | undefined;
|
|
149
187
|
const tree = trees[zoom];
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
188
|
+
if (!tree) continue;
|
|
189
|
+
if (trees[prevZoom] && zoom < maxAvailZoom) {
|
|
190
|
+
childrenByParent = rollup(
|
|
191
|
+
trees[prevZoom].points,
|
|
153
192
|
(points: any[]) =>
|
|
154
193
|
points.map((p: any) =>
|
|
155
|
-
p.id ? makeClusterId(p.id) : getLocationId(
|
|
194
|
+
p.id ? makeClusterId(p.id) : getLocationId(p.location),
|
|
156
195
|
),
|
|
157
196
|
(point: any) => point.parentId,
|
|
158
197
|
);
|
|
@@ -160,9 +199,8 @@ export function clusterLocations<L>(
|
|
|
160
199
|
|
|
161
200
|
const nodes: ClusterNode[] = [];
|
|
162
201
|
for (const point of tree.points) {
|
|
163
|
-
const {x, y, numPoints} = point;
|
|
202
|
+
const {x, y, numPoints, location} = point;
|
|
164
203
|
if (isLeafPoint(point)) {
|
|
165
|
-
const location = locations[point.index];
|
|
166
204
|
nodes.push({
|
|
167
205
|
id: getLocationId(location),
|
|
168
206
|
zoom,
|
|
@@ -173,22 +211,26 @@ export function clusterLocations<L>(
|
|
|
173
211
|
const {id} = point;
|
|
174
212
|
const children = childrenByParent && childrenByParent.get(id);
|
|
175
213
|
if (!children) {
|
|
176
|
-
|
|
214
|
+
// Might happen if there are multiple locations with same coordinates
|
|
215
|
+
console.warn(`Omitting cluster with no children, point:`, point);
|
|
216
|
+
continue;
|
|
177
217
|
}
|
|
178
|
-
|
|
218
|
+
const cluster = {
|
|
179
219
|
id: makeClusterId(id),
|
|
180
220
|
name: makeClusterName(id, numPoints),
|
|
181
221
|
zoom,
|
|
182
222
|
lat: yLat(y),
|
|
183
223
|
lon: xLng(x),
|
|
184
|
-
children,
|
|
185
|
-
} as Cluster
|
|
224
|
+
children: children ?? [],
|
|
225
|
+
} as Cluster;
|
|
226
|
+
nodes.push(cluster);
|
|
186
227
|
}
|
|
187
228
|
}
|
|
188
229
|
clusterLevels.push({
|
|
189
230
|
zoom,
|
|
190
231
|
nodes,
|
|
191
232
|
});
|
|
233
|
+
prevZoom = zoom;
|
|
192
234
|
}
|
|
193
235
|
return clusterLevels;
|
|
194
236
|
}
|
|
@@ -211,13 +253,13 @@ function createCluster(
|
|
|
211
253
|
};
|
|
212
254
|
}
|
|
213
255
|
|
|
214
|
-
function cluster(
|
|
215
|
-
points: Point[],
|
|
256
|
+
function cluster<L>(
|
|
257
|
+
points: Point<L>[],
|
|
216
258
|
zoom: number,
|
|
217
259
|
tree: ZoomLevelKDBush,
|
|
218
260
|
options: Options,
|
|
219
261
|
) {
|
|
220
|
-
const clusters: Point[] = [];
|
|
262
|
+
const clusters: Point<L>[] = [];
|
|
221
263
|
const {radius, extent} = options;
|
|
222
264
|
const r = radius / (extent * Math.pow(2, zoom));
|
|
223
265
|
|
|
@@ -293,10 +335,60 @@ function latY(lat: number) {
|
|
|
293
335
|
return y < 0 ? 0 : y > 1 ? 1 : y;
|
|
294
336
|
}
|
|
295
337
|
|
|
296
|
-
function getX(p: Point) {
|
|
338
|
+
function getX<L>(p: Point<L>) {
|
|
297
339
|
return p.x;
|
|
298
340
|
}
|
|
299
341
|
|
|
300
|
-
function getY(p: Point) {
|
|
342
|
+
function getY<L>(p: Point<L>) {
|
|
301
343
|
return p.y;
|
|
302
344
|
}
|
|
345
|
+
|
|
346
|
+
function countUniqueLocations<L>(
|
|
347
|
+
locations: Iterable<L>,
|
|
348
|
+
locationAccessors: LocationAccessors<L>,
|
|
349
|
+
) {
|
|
350
|
+
const {getLocationLon, getLocationLat} = locationAccessors;
|
|
351
|
+
const countByLatLon = new Map<string, number>();
|
|
352
|
+
let uniqueCnt = 0;
|
|
353
|
+
for (const loc of locations) {
|
|
354
|
+
const lon = getLocationLon(loc);
|
|
355
|
+
const lat = getLocationLat(loc);
|
|
356
|
+
const key = `${lon},${lat}`;
|
|
357
|
+
const prev = countByLatLon.get(key);
|
|
358
|
+
if (!prev) {
|
|
359
|
+
uniqueCnt++;
|
|
360
|
+
}
|
|
361
|
+
countByLatLon.set(key, prev ? prev + 1 : 1);
|
|
362
|
+
}
|
|
363
|
+
return uniqueCnt;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function findIndexOfMax(arr: (number | undefined)[]): number | undefined {
|
|
367
|
+
let max = -Infinity;
|
|
368
|
+
let maxIndex: number | undefined = undefined;
|
|
369
|
+
|
|
370
|
+
for (let i = 0; i < arr.length; i++) {
|
|
371
|
+
const value = arr[i];
|
|
372
|
+
|
|
373
|
+
if (typeof value === 'number') {
|
|
374
|
+
if (value > max) {
|
|
375
|
+
max = value;
|
|
376
|
+
maxIndex = i;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return maxIndex;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function findLastIndex<T>(
|
|
385
|
+
arr: T[],
|
|
386
|
+
predicate: (value: T, index: number, array: T[]) => boolean,
|
|
387
|
+
): number {
|
|
388
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
389
|
+
if (predicate(arr[i], i, arr)) {
|
|
390
|
+
return i;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return -1;
|
|
394
|
+
}
|
package/src/colors.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Flowmap.gl contributors
|
|
3
|
+
* Copyright (c) 2018-2020 Teralytics
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import {
|
|
2
8
|
interpolateCool,
|
|
3
9
|
interpolateInferno,
|
|
@@ -333,17 +339,15 @@ const diffColors: DiffColors = {
|
|
|
333
339
|
outlineColor: 'rgb(230,233,237)',
|
|
334
340
|
};
|
|
335
341
|
|
|
336
|
-
export function getFlowmapColors(
|
|
337
|
-
settingsState: SettingsState,
|
|
338
|
-
): Colors | DiffColors {
|
|
342
|
+
export function getFlowmapColors(settings: SettingsState): Colors | DiffColors {
|
|
339
343
|
return getColors(
|
|
340
344
|
false, // TODO: diffMode
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
settings.colorScheme,
|
|
346
|
+
settings.darkMode,
|
|
347
|
+
settings.fadeEnabled,
|
|
348
|
+
settings.fadeOpacityEnabled,
|
|
349
|
+
settings.fadeAmount,
|
|
350
|
+
settings.animationEnabled,
|
|
347
351
|
);
|
|
348
352
|
}
|
|
349
353
|
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Flowmap.gl contributors
|
|
3
|
+
* Copyright (c) 2018-2020 Teralytics
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import {geoBounds} from 'd3-geo';
|
|
2
8
|
import {fitBounds} from '@math.gl/web-mercator';
|
|
3
9
|
import type {
|
|
@@ -50,19 +56,29 @@ export function getViewStateForFeatures(
|
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
export function getViewStateForLocations(
|
|
54
|
-
locations:
|
|
55
|
-
getLocationCoords: (location:
|
|
59
|
+
export function getViewStateForLocations<L>(
|
|
60
|
+
locations: Iterable<L>,
|
|
61
|
+
getLocationCoords: (location: L) => [number, number],
|
|
56
62
|
size: [number, number],
|
|
57
63
|
opts?: GetViewStateOptions,
|
|
58
64
|
): ViewState & {width: number; height: number} {
|
|
65
|
+
const asGeometry = (location: L) => ({
|
|
66
|
+
type: 'Point',
|
|
67
|
+
coordinates: getLocationCoords(location),
|
|
68
|
+
});
|
|
69
|
+
let geometries;
|
|
70
|
+
if (Array.isArray(locations)) {
|
|
71
|
+
geometries = locations.map(asGeometry);
|
|
72
|
+
} else {
|
|
73
|
+
geometries = [];
|
|
74
|
+
for (const location of locations) {
|
|
75
|
+
geometries.push(asGeometry(location));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
59
78
|
return getViewStateForFeatures(
|
|
60
79
|
{
|
|
61
80
|
type: 'GeometryCollection',
|
|
62
|
-
geometries
|
|
63
|
-
type: 'Point',
|
|
64
|
-
coordinates: getLocationCoords(location),
|
|
65
|
-
})),
|
|
81
|
+
geometries,
|
|
66
82
|
} as any,
|
|
67
83
|
size,
|
|
68
84
|
opts,
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Flowmap.gl contributors
|
|
3
|
+
* Copyright (c) 2018-2020 Teralytics
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
export * from './types';
|
|
2
8
|
export * from './colors';
|
|
3
9
|
export * from './FlowmapState';
|
|
4
10
|
export * from './FlowmapSelectors';
|
|
11
|
+
export * from './selector-functions';
|
|
5
12
|
export * from './time';
|
|
6
13
|
export * from './getViewStateForLocations';
|
|
7
14
|
export * from './provider/FlowmapDataProvider';
|
|
15
|
+
export * from './cluster/cluster';
|
|
16
|
+
export * from './cluster/ClusterIndex';
|
|
8
17
|
export {default as FlowmapAggregateAccessors} from './FlowmapAggregateAccessors';
|
|
9
18
|
export type {default as FlowmapDataProvider} from './provider/FlowmapDataProvider';
|
|
10
19
|
export {default as LocalFlowmapDataProvider} from './provider/LocalFlowmapDataProvider';
|
|
11
|
-
export {default as createWorkerDataProvider} from './provider/createWorkerDataProvider';
|
|
12
|
-
export {default as WorkerFlowmapDataProvider} from './provider/WorkerFlowmapDataProvider';
|
|
13
|
-
export * from './provider/WorkerFlowmapDataProvider';
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Flowmap.gl contributors
|
|
3
|
+
* Copyright (c) 2018-2020 Teralytics
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import {AggregateFlow, Cluster, LocationAccessors, LocationTotals} from '..';
|
|
2
8
|
import {FlowmapState} from '../FlowmapState';
|
|
3
9
|
import {
|
|
@@ -23,11 +29,13 @@ export default interface FlowmapDataProvider<L, F> {
|
|
|
23
29
|
|
|
24
30
|
getFlowByIndex(index: number): Promise<F | AggregateFlow | undefined>;
|
|
25
31
|
|
|
26
|
-
getLocationById(id: string): Promise<L | Cluster | undefined>;
|
|
32
|
+
getLocationById(id: string | number): Promise<L | Cluster | undefined>;
|
|
27
33
|
|
|
28
34
|
getLocationByIndex(idx: number): Promise<L | ClusterNode | undefined>;
|
|
29
35
|
|
|
30
|
-
getTotalsForLocation(
|
|
36
|
+
getTotalsForLocation(
|
|
37
|
+
id: string | number,
|
|
38
|
+
): Promise<LocationTotals | undefined>;
|
|
31
39
|
|
|
32
40
|
// getLocationsInBbox(
|
|
33
41
|
// bbox: [number, number, number, number],
|
|
@@ -36,17 +44,25 @@ export default interface FlowmapDataProvider<L, F> {
|
|
|
36
44
|
// getLocationsForSearchBox(): Promise<(FlowLocation | ClusterNode)[] | undefined>;
|
|
37
45
|
|
|
38
46
|
getLayersData(): Promise<LayersData | undefined>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* This is to give the data provider control over when/how often layersData
|
|
50
|
+
* is updated which leads to the flowmap being redrawn.
|
|
51
|
+
*/
|
|
52
|
+
updateLayersData(
|
|
53
|
+
setLayersData: (layersData: LayersData | undefined) => void,
|
|
54
|
+
changeFlags: Record<string, boolean>,
|
|
55
|
+
): Promise<void>;
|
|
39
56
|
}
|
|
40
57
|
|
|
41
58
|
export function isFlowmapData<L, F>(
|
|
42
59
|
data: Record<string, any>,
|
|
43
60
|
): data is FlowmapData<L, F> {
|
|
44
61
|
return (
|
|
45
|
-
data &&
|
|
46
|
-
|
|
47
|
-
data.
|
|
48
|
-
Array.isArray(data.
|
|
49
|
-
Array.isArray(data.flows)
|
|
62
|
+
data && data.locations && data.flows
|
|
63
|
+
// TODO: test that they are iterable
|
|
64
|
+
// Array.isArray(data.locations) &&
|
|
65
|
+
// Array.isArray(data.flows)
|
|
50
66
|
);
|
|
51
67
|
}
|
|
52
68
|
|