@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.
Files changed (70) hide show
  1. package/.turbo/turbo-build.log +3 -0
  2. package/.turbo/turbo-dev.log +6 -0
  3. package/LICENSE +2 -2
  4. package/dist/FlowmapAggregateAccessors.d.ts +4 -4
  5. package/dist/FlowmapAggregateAccessors.d.ts.map +1 -1
  6. package/dist/FlowmapAggregateAccessors.js +16 -9
  7. package/dist/FlowmapSelectors.d.ts +41 -87
  8. package/dist/FlowmapSelectors.d.ts.map +1 -1
  9. package/dist/FlowmapSelectors.js +174 -161
  10. package/dist/FlowmapState.d.ts +7 -5
  11. package/dist/FlowmapState.d.ts.map +1 -1
  12. package/dist/FlowmapState.js +6 -1
  13. package/dist/cluster/ClusterIndex.d.ts +4 -4
  14. package/dist/cluster/ClusterIndex.d.ts.map +1 -1
  15. package/dist/cluster/ClusterIndex.js +5 -17
  16. package/dist/cluster/cluster.d.ts +25 -5
  17. package/dist/cluster/cluster.d.ts.map +1 -1
  18. package/dist/cluster/cluster.js +115 -57
  19. package/dist/colors.d.ts +3 -3
  20. package/dist/colors.d.ts.map +1 -1
  21. package/dist/colors.js +19 -8
  22. package/dist/getViewStateForLocations.d.ts +3 -3
  23. package/dist/getViewStateForLocations.d.ts.map +1 -1
  24. package/dist/getViewStateForLocations.js +33 -12
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +9 -4
  28. package/dist/provider/FlowmapDataProvider.d.ts +7 -2
  29. package/dist/provider/FlowmapDataProvider.d.ts.map +1 -1
  30. package/dist/provider/FlowmapDataProvider.js +11 -6
  31. package/dist/provider/LocalFlowmapDataProvider.d.ts +15 -4
  32. package/dist/provider/LocalFlowmapDataProvider.d.ts.map +1 -1
  33. package/dist/provider/LocalFlowmapDataProvider.js +98 -81
  34. package/dist/selector-functions.d.ts +10 -0
  35. package/dist/selector-functions.d.ts.map +1 -0
  36. package/dist/selector-functions.js +65 -0
  37. package/dist/time.d.ts.map +1 -1
  38. package/dist/time.js +6 -1
  39. package/dist/types.d.ts +20 -18
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/types.js +9 -4
  42. package/dist/util.d.ts.map +1 -1
  43. package/dist/util.js +6 -1
  44. package/package.json +22 -27
  45. package/src/FlowmapAggregateAccessors.ts +21 -10
  46. package/src/FlowmapSelectors.ts +304 -280
  47. package/src/FlowmapState.ts +13 -5
  48. package/src/cluster/ClusterIndex.ts +23 -28
  49. package/src/cluster/cluster.ts +165 -73
  50. package/src/colors.ts +13 -9
  51. package/src/getViewStateForLocations.ts +23 -7
  52. package/src/index.ts +9 -3
  53. package/src/provider/FlowmapDataProvider.ts +23 -7
  54. package/src/provider/LocalFlowmapDataProvider.ts +68 -5
  55. package/src/selector-functions.ts +93 -0
  56. package/src/time.ts +6 -0
  57. package/src/types.ts +23 -15
  58. package/src/util.ts +6 -0
  59. package/dist/provider/WorkerFlowmapDataProvider.d.ts +0 -42
  60. package/dist/provider/WorkerFlowmapDataProvider.d.ts.map +0 -1
  61. package/dist/provider/WorkerFlowmapDataProvider.js +0 -80
  62. package/dist/provider/WorkerFlowmapDataProviderWorker.d.ts +0 -2
  63. package/dist/provider/WorkerFlowmapDataProviderWorker.d.ts.map +0 -1
  64. package/dist/provider/WorkerFlowmapDataProviderWorker.js +0 -4
  65. package/dist/provider/createWorkerDataProvider.d.ts +0 -3
  66. package/dist/provider/createWorkerDataProvider.d.ts.map +0 -1
  67. package/dist/provider/createWorkerDataProvider.js +0 -21
  68. package/src/provider/WorkerFlowmapDataProvider.ts +0 -121
  69. package/src/provider/WorkerFlowmapDataProviderWorker.ts +0 -4
  70. package/src/provider/createWorkerDataProvider.ts +0 -18
@@ -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: string[] | undefined;
5
- selectedTimeRange: [Date, Date] | undefined;
6
- locationFilterMode: LocationFilterMode;
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
- filterState: FilterState;
27
- settingsState: SettingsState;
34
+ filter?: FilterState;
35
+ settings: SettingsState;
28
36
  viewport: ViewportProps;
29
37
  }
@@ -1,19 +1,7 @@
1
1
  /*
2
- * Copyright 2022 FlowmapBlue
3
- * Copyright 2018-2020 Teralytics, modified by FlowmapBlue
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: (locationId: string, zoom: number) => string | undefined;
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<number, Map<string, Cluster>>();
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) => `${origin}:${dest}`;
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),
@@ -1,46 +1,34 @@
1
1
  /*
2
- * Copyright 2022 FlowmapBlue
3
- * Copyright 2018-2020 Teralytics, modified by FlowmapBlue
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
- // ISC License
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
- for (let i = 0; i < locations.length; i++) {
113
- const x = getLocationLon(locations[i]);
114
- const y = getLocationLat(locations[i]);
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(locations[i])),
108
+ weight: getLocationWeight(getLocationId(location)),
119
109
  zoom: Infinity, // the last zoom the point was processed at
120
- index: i, // index of the source feature in the original input array,
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
- trees[maxZoom + 1] = new KDBush(clusters, getX, getY, nodeSize, Float32Array);
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
- clusters = cluster(clusters, z, trees[z + 1], opts);
131
- trees[z] = new KDBush(clusters, getX, getY, nodeSize, Float32Array);
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
- const numbersOfClusters = trees.map((d) => d.points.length);
138
- const maxAvailZoom = numbersOfClusters.indexOf(
139
- numbersOfClusters[numbersOfClusters.length - 1],
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(numbersOfClusters[0]),
180
+ minClusters ? numbersOfClusters.lastIndexOf(minClusters) : maxAvailZoom,
144
181
  );
145
182
 
146
183
  const clusterLevels = new Array<ClusterLevel>();
147
- for (let zoom = minAvailZoom; zoom <= maxAvailZoom; zoom++) {
148
- let childrenByParent: Map<number, string[]> | undefined;
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 (zoom < maxAvailZoom) {
151
- childrenByParent = rollup<Point, string[], number>(
152
- trees[zoom + 1].points,
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(locations[p.index]),
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
- throw new Error(`Cluster ${id} doesn't have children`);
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
- nodes.push({
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
- settingsState.colorScheme,
342
- settingsState.darkMode,
343
- settingsState.fadeEnabled,
344
- settingsState.fadeOpacityEnabled,
345
- settingsState.fadeAmount,
346
- settingsState.animationEnabled,
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: any[],
55
- getLocationCoords: (location: any) => [number, number],
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: locations.map((location) => ({
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(id: string): Promise<LocationTotals | undefined>;
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
- data.locations &&
47
- data.flows &&
48
- Array.isArray(data.locations) &&
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