@flowmap.gl/data 8.0.0-y.14 → 8.0.2
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 +2 -0
- package/.turbo/turbo-dev.log +670 -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 +39 -85
- package/dist/FlowmapSelectors.d.ts.map +1 -1
- package/dist/FlowmapSelectors.js +128 -144
- 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 +20 -1
- package/dist/cluster/cluster.d.ts.map +1 -1
- package/dist/cluster/cluster.js +108 -52
- 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 +2 -2
- package/dist/getViewStateForLocations.d.ts.map +1 -1
- package/dist/getViewStateForLocations.js +18 -8
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- 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 +18 -16
- 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 -23
- package/src/FlowmapAggregateAccessors.ts +21 -10
- package/src/FlowmapSelectors.ts +271 -264
- package/src/FlowmapState.ts +13 -5
- package/src/cluster/ClusterIndex.ts +23 -28
- package/src/cluster/cluster.ts +145 -56
- package/src/colors.ts +13 -9
- package/src/getViewStateForLocations.ts +6 -0
- package/src/index.ts +9 -0
- 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 +21 -13
- package/src/util.ts +6 -0
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
|
|
@@ -110,7 +98,7 @@ export function clusterLocations<L>(
|
|
|
110
98
|
|
|
111
99
|
// generate a cluster object for each point and index input points into a KD-tree
|
|
112
100
|
let clusters = new Array<Point<L>>();
|
|
113
|
-
let
|
|
101
|
+
let locationsCount = 0;
|
|
114
102
|
for (const location of locations) {
|
|
115
103
|
const x = getLocationLon(location);
|
|
116
104
|
const y = getLocationLat(location);
|
|
@@ -119,41 +107,88 @@ export function clusterLocations<L>(
|
|
|
119
107
|
y: latY(y),
|
|
120
108
|
weight: getLocationWeight(getLocationId(location)),
|
|
121
109
|
zoom: Infinity, // the last zoom the point was processed at
|
|
122
|
-
index:
|
|
110
|
+
index: locationsCount, // index of the source feature in the original input array,
|
|
123
111
|
parentId: -1, // parent cluster id
|
|
124
112
|
location,
|
|
125
113
|
});
|
|
126
|
-
|
|
114
|
+
locationsCount++;
|
|
127
115
|
}
|
|
128
|
-
|
|
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
|
+
};
|
|
129
126
|
|
|
130
127
|
// cluster points on max zoom, then cluster the results on previous zoom, etc.;
|
|
131
128
|
// results in a cluster hierarchy across zoom levels
|
|
129
|
+
trees[maxZoom + 1] = makeBush(clusters);
|
|
130
|
+
let prevZoom = maxZoom + 1;
|
|
131
|
+
|
|
132
132
|
for (let z = maxZoom; z >= minZoom; z--) {
|
|
133
133
|
// create a new set of clusters for the zoom and index them with a KD-tree
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
}
|
|
136
147
|
}
|
|
137
148
|
|
|
138
149
|
if (trees.length === 0) {
|
|
139
150
|
return [];
|
|
140
151
|
}
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
|
|
145
178
|
const minAvailZoom = Math.min(
|
|
146
179
|
maxAvailZoom,
|
|
147
|
-
numbersOfClusters.lastIndexOf(
|
|
180
|
+
minClusters ? numbersOfClusters.lastIndexOf(minClusters) : maxAvailZoom,
|
|
148
181
|
);
|
|
149
182
|
|
|
150
183
|
const clusterLevels = new Array<ClusterLevel>();
|
|
151
|
-
|
|
152
|
-
|
|
184
|
+
prevZoom = NaN;
|
|
185
|
+
for (let zoom = maxAvailZoom; zoom >= minAvailZoom; zoom--) {
|
|
186
|
+
let childrenByParent: Map<number, (string | number)[]> | undefined;
|
|
153
187
|
const tree = trees[zoom];
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
188
|
+
if (!tree) continue;
|
|
189
|
+
if (trees[prevZoom] && zoom < maxAvailZoom) {
|
|
190
|
+
childrenByParent = rollup(
|
|
191
|
+
trees[prevZoom].points,
|
|
157
192
|
(points: any[]) =>
|
|
158
193
|
points.map((p: any) =>
|
|
159
194
|
p.id ? makeClusterId(p.id) : getLocationId(p.location),
|
|
@@ -176,22 +211,26 @@ export function clusterLocations<L>(
|
|
|
176
211
|
const {id} = point;
|
|
177
212
|
const children = childrenByParent && childrenByParent.get(id);
|
|
178
213
|
if (!children) {
|
|
179
|
-
|
|
214
|
+
// Might happen if there are multiple locations with same coordinates
|
|
215
|
+
console.warn(`Omitting cluster with no children, point:`, point);
|
|
216
|
+
continue;
|
|
180
217
|
}
|
|
181
|
-
|
|
218
|
+
const cluster = {
|
|
182
219
|
id: makeClusterId(id),
|
|
183
220
|
name: makeClusterName(id, numPoints),
|
|
184
221
|
zoom,
|
|
185
222
|
lat: yLat(y),
|
|
186
223
|
lon: xLng(x),
|
|
187
|
-
children,
|
|
188
|
-
} as Cluster
|
|
224
|
+
children: children ?? [],
|
|
225
|
+
} as Cluster;
|
|
226
|
+
nodes.push(cluster);
|
|
189
227
|
}
|
|
190
228
|
}
|
|
191
229
|
clusterLevels.push({
|
|
192
230
|
zoom,
|
|
193
231
|
nodes,
|
|
194
232
|
});
|
|
233
|
+
prevZoom = zoom;
|
|
195
234
|
}
|
|
196
235
|
return clusterLevels;
|
|
197
236
|
}
|
|
@@ -303,3 +342,53 @@ function getX<L>(p: Point<L>) {
|
|
|
303
342
|
function getY<L>(p: Point<L>) {
|
|
304
343
|
return p.y;
|
|
305
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
|
|
package/src/index.ts
CHANGED
|
@@ -1,10 +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';
|
|
@@ -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
|
|
|
@@ -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 type FlowmapDataProvider from './FlowmapDataProvider';
|
|
2
8
|
import type {
|
|
3
9
|
Cluster,
|
|
@@ -15,9 +21,12 @@ import {
|
|
|
15
21
|
GetViewStateOptions,
|
|
16
22
|
getViewStateForLocations,
|
|
17
23
|
} from '../getViewStateForLocations';
|
|
24
|
+
import {ClusterIndex} from '../cluster/ClusterIndex';
|
|
18
25
|
|
|
19
|
-
export default class LocalFlowmapDataProvider<
|
|
20
|
-
|
|
26
|
+
export default class LocalFlowmapDataProvider<
|
|
27
|
+
L extends Record<string, any>,
|
|
28
|
+
F extends Record<string, any>,
|
|
29
|
+
> implements FlowmapDataProvider<L, F>
|
|
21
30
|
{
|
|
22
31
|
private selectors: FlowmapSelectors<L, F>;
|
|
23
32
|
private flowmapData: FlowmapData<L, F> | undefined;
|
|
@@ -34,14 +43,26 @@ export default class LocalFlowmapDataProvider<L, F>
|
|
|
34
43
|
this.selectors.setAccessors(accessors);
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
setFlowmapData(flowmapData: FlowmapData<L, F>): void {
|
|
38
47
|
this.flowmapData = flowmapData;
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
getSelectors(): FlowmapSelectors<L, F> {
|
|
51
|
+
return this.selectors;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getFlowmapData(): FlowmapData<L, F> | undefined {
|
|
55
|
+
return this.flowmapData;
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
async setFlowmapState(flowmapState: FlowmapState): Promise<void> {
|
|
42
59
|
this.flowmapState = flowmapState;
|
|
43
60
|
}
|
|
44
61
|
|
|
62
|
+
getFlowmapState(): FlowmapState | undefined {
|
|
63
|
+
return this.flowmapState;
|
|
64
|
+
}
|
|
65
|
+
|
|
45
66
|
async getFlowByIndex(idx: number): Promise<F | AggregateFlow | undefined> {
|
|
46
67
|
if (!this.flowmapState || !this.flowmapData) {
|
|
47
68
|
return undefined;
|
|
@@ -53,6 +74,7 @@ export default class LocalFlowmapDataProvider<L, F>
|
|
|
53
74
|
return flows?.[idx];
|
|
54
75
|
}
|
|
55
76
|
|
|
77
|
+
// TODO: this is unreliable, should replace by unqiue ID
|
|
56
78
|
async getLocationByIndex(idx: number): Promise<L | ClusterNode | undefined> {
|
|
57
79
|
if (!this.flowmapState || !this.flowmapData) {
|
|
58
80
|
return undefined;
|
|
@@ -71,7 +93,7 @@ export default class LocalFlowmapDataProvider<L, F>
|
|
|
71
93
|
return this.selectors.getLayersData(this.flowmapState, this.flowmapData);
|
|
72
94
|
}
|
|
73
95
|
|
|
74
|
-
async getLocationById(id: string): Promise<L | Cluster | undefined> {
|
|
96
|
+
async getLocationById(id: string | number): Promise<L | Cluster | undefined> {
|
|
75
97
|
if (!this.flowmapState || !this.flowmapData) {
|
|
76
98
|
return undefined;
|
|
77
99
|
}
|
|
@@ -92,7 +114,9 @@ export default class LocalFlowmapDataProvider<L, F>
|
|
|
92
114
|
return locationsById?.get(id);
|
|
93
115
|
}
|
|
94
116
|
|
|
95
|
-
async getTotalsForLocation(
|
|
117
|
+
async getTotalsForLocation(
|
|
118
|
+
id: string | number,
|
|
119
|
+
): Promise<LocationTotals | undefined> {
|
|
96
120
|
if (!this.flowmapState || !this.flowmapData) {
|
|
97
121
|
return undefined;
|
|
98
122
|
}
|
|
@@ -119,4 +143,43 @@ export default class LocalFlowmapDataProvider<L, F>
|
|
|
119
143
|
opts,
|
|
120
144
|
);
|
|
121
145
|
}
|
|
146
|
+
|
|
147
|
+
async updateLayersData(
|
|
148
|
+
setLayersData: (layersData: LayersData | undefined) => void,
|
|
149
|
+
) {
|
|
150
|
+
setLayersData(await this.getLayersData());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getClusterZoom(): number | undefined {
|
|
154
|
+
return this.flowmapState && this.flowmapData
|
|
155
|
+
? this.selectors.getClusterZoom(this.flowmapState, this.flowmapData)
|
|
156
|
+
: undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getClusterIndex(): ClusterIndex<F> | undefined {
|
|
160
|
+
return this.flowmapState && this.flowmapData
|
|
161
|
+
? this.selectors.getClusterIndex(this.flowmapState, this.flowmapData)
|
|
162
|
+
: undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getLocationsById(): Map<string | number, L> | undefined {
|
|
166
|
+
return this.flowmapState && this.flowmapData
|
|
167
|
+
? this.selectors.getLocationsById(this.flowmapState, this.flowmapData)
|
|
168
|
+
: undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
getLocationTotals(): Map<string | number, LocationTotals> | undefined {
|
|
172
|
+
return this.flowmapState && this.flowmapData
|
|
173
|
+
? this.selectors.getLocationTotals(this.flowmapState, this.flowmapData)
|
|
174
|
+
: undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
getFlowsForFlowmapLayer(): Array<F | AggregateFlow> | undefined {
|
|
178
|
+
return this.flowmapState && this.flowmapData
|
|
179
|
+
? this.selectors.getFlowsForFlowmapLayer(
|
|
180
|
+
this.flowmapState,
|
|
181
|
+
this.flowmapData,
|
|
182
|
+
)
|
|
183
|
+
: undefined;
|
|
184
|
+
}
|
|
122
185
|
}
|