@flowmap.gl/data 8.0.0-y.14 → 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 (58) 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 +38 -84
  8. package/dist/FlowmapSelectors.d.ts.map +1 -1
  9. package/dist/FlowmapSelectors.js +125 -141
  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 +20 -1
  17. package/dist/cluster/cluster.d.ts.map +1 -1
  18. package/dist/cluster/cluster.js +108 -52
  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 +2 -2
  23. package/dist/getViewStateForLocations.d.ts.map +1 -1
  24. package/dist/getViewStateForLocations.js +18 -8
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +9 -1
  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 +18 -16
  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 -23
  45. package/src/FlowmapAggregateAccessors.ts +21 -10
  46. package/src/FlowmapSelectors.ts +261 -255
  47. package/src/FlowmapState.ts +13 -5
  48. package/src/cluster/ClusterIndex.ts +23 -28
  49. package/src/cluster/cluster.ts +145 -56
  50. package/src/colors.ts +13 -9
  51. package/src/getViewStateForLocations.ts +6 -0
  52. package/src/index.ts +9 -0
  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 +21 -13
  58. package/src/util.ts +6 -0
@@ -1,25 +1,11 @@
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
- import {WebMercatorViewport} from '@math.gl/web-mercator';
20
- import {ascending, descending, extent, min} from 'd3-array';
21
- import {nest} from 'd3-collection';
22
- import {ScaleLinear, scaleLinear, scaleSqrt} from 'd3-scale';
7
+ import {ascending, descending, extent, min, rollup} from 'd3-array';
8
+ import {ScaleLinear, scaleSqrt} from 'd3-scale';
23
9
  import KDBush from 'kdbush';
24
10
  import {
25
11
  createSelector,
@@ -33,6 +19,7 @@ import {
33
19
  buildIndex,
34
20
  ClusterIndex,
35
21
  findAppropriateZoomLevel,
22
+ LocationWeightGetter,
36
23
  makeLocationWeightGetter,
37
24
  } from './cluster/ClusterIndex';
38
25
  import getColors, {
@@ -46,6 +33,11 @@ import getColors, {
46
33
  } from './colors';
47
34
  import FlowmapAggregateAccessors from './FlowmapAggregateAccessors';
48
35
  import {FlowmapState} from './FlowmapState';
36
+ import {
37
+ addClusterNames,
38
+ getFlowThicknessScale,
39
+ getViewportBoundingBox,
40
+ } from './selector-functions';
49
41
  import {
50
42
  getTimeGranularityByKey,
51
43
  getTimeGranularityByOrder,
@@ -55,6 +47,7 @@ import {
55
47
  import {
56
48
  AggregateFlow,
57
49
  Cluster,
50
+ ClusterLevels,
58
51
  ClusterNode,
59
52
  CountByTime,
60
53
  FlowAccessors,
@@ -78,7 +71,10 @@ export type Selector<L, F, T> = ParametricSelector<
78
71
  T
79
72
  >;
80
73
 
81
- export default class FlowmapSelectors<L, F> {
74
+ export default class FlowmapSelectors<
75
+ L extends Record<string, any>,
76
+ F extends Record<string, any>,
77
+ > {
82
78
  accessors: FlowmapAggregateAccessors<L, F>;
83
79
 
84
80
  constructor(accessors: FlowmapDataAccessors<L, F>) {
@@ -90,60 +86,71 @@ export default class FlowmapSelectors<L, F> {
90
86
  this.accessors = new FlowmapAggregateAccessors(accessors);
91
87
  }
92
88
 
93
- getFetchedFlows = (state: FlowmapState, props: FlowmapData<L, F>) =>
89
+ getAggregateAccessors(): FlowmapAggregateAccessors<L, F> {
90
+ return this.accessors;
91
+ }
92
+
93
+ getFlowsFromProps = (state: FlowmapState, props: FlowmapData<L, F>) =>
94
94
  props.flows;
95
- getFetchedLocations = (state: FlowmapState, props: FlowmapData<L, F>) =>
95
+ getLocationsFromProps = (state: FlowmapState, props: FlowmapData<L, F>) =>
96
96
  props.locations;
97
+ getClusterLevelsFromProps = (
98
+ state: FlowmapState,
99
+ props: FlowmapData<L, F>,
100
+ ) => {
101
+ return props.clusterLevels;
102
+ };
97
103
  getMaxTopFlowsDisplayNum = (state: FlowmapState, props: FlowmapData<L, F>) =>
98
- state.settingsState.maxTopFlowsDisplayNum;
104
+ state.settings.maxTopFlowsDisplayNum;
99
105
  getSelectedLocations = (state: FlowmapState, props: FlowmapData<L, F>) =>
100
- state.filterState.selectedLocations;
106
+ state.filter?.selectedLocations;
101
107
  getLocationFilterMode = (state: FlowmapState, props: FlowmapData<L, F>) =>
102
- state.filterState.locationFilterMode;
108
+ state.filter?.locationFilterMode;
103
109
  getClusteringEnabled = (state: FlowmapState, props: FlowmapData<L, F>) =>
104
- state.settingsState.clusteringEnabled;
110
+ state.settings.clusteringEnabled;
105
111
  getLocationTotalsEnabled = (state: FlowmapState, props: FlowmapData<L, F>) =>
106
- state.settingsState.locationTotalsEnabled;
112
+ state.settings.locationTotalsEnabled;
113
+ getLocationLabelsEnabled = (state: FlowmapState, props: FlowmapData<L, F>) =>
114
+ state.settings.locationLabelsEnabled;
107
115
  getZoom = (state: FlowmapState, props: FlowmapData<L, F>) =>
108
116
  state.viewport.zoom;
109
117
  getViewport = (state: FlowmapState, props: FlowmapData<L, F>) =>
110
118
  state.viewport;
111
119
  getSelectedTimeRange = (state: FlowmapState, props: FlowmapData<L, F>) =>
112
- state.filterState.selectedTimeRange;
120
+ state.filter?.selectedTimeRange;
113
121
 
114
122
  getColorScheme: Selector<L, F, string | string[] | undefined> = (
115
123
  state: FlowmapState,
116
124
  props: FlowmapData<L, F>,
117
- ) => state.settingsState.colorScheme;
125
+ ) => state.settings.colorScheme;
118
126
 
119
127
  getDarkMode: Selector<L, F, boolean> = (
120
128
  state: FlowmapState,
121
129
  props: FlowmapData<L, F>,
122
- ) => state.settingsState.darkMode;
130
+ ) => state.settings.darkMode;
123
131
 
124
132
  getFadeEnabled: Selector<L, F, boolean> = (
125
133
  state: FlowmapState,
126
134
  props: FlowmapData<L, F>,
127
- ) => state.settingsState.fadeEnabled;
135
+ ) => state.settings.fadeEnabled;
128
136
 
129
137
  getFadeOpacityEnabled: Selector<L, F, boolean> = (
130
138
  state: FlowmapState,
131
139
  props: FlowmapData<L, F>,
132
- ) => state.settingsState.fadeOpacityEnabled;
140
+ ) => state.settings.fadeOpacityEnabled;
133
141
 
134
142
  getFadeAmount: Selector<L, F, number> = (
135
143
  state: FlowmapState,
136
144
  props: FlowmapData<L, F>,
137
- ) => state.settingsState.fadeAmount;
145
+ ) => state.settings.fadeAmount;
138
146
 
139
147
  getAnimate: Selector<L, F, boolean> = (
140
148
  state: FlowmapState,
141
149
  props: FlowmapData<L, F>,
142
- ) => state.settingsState.animationEnabled;
150
+ ) => state.settings.animationEnabled;
143
151
 
144
- getInvalidLocationIds: Selector<L, F, string[] | undefined> = createSelector(
145
- this.getFetchedLocations,
146
- (locations) => {
152
+ getInvalidLocationIds: Selector<L, F, (string | number)[] | undefined> =
153
+ createSelector(this.getLocationsFromProps, (locations) => {
147
154
  if (!locations) return undefined;
148
155
  const invalid = [];
149
156
  for (const location of locations) {
@@ -155,11 +162,10 @@ export default class FlowmapSelectors<L, F> {
155
162
  }
156
163
  }
157
164
  return invalid.length > 0 ? invalid : undefined;
158
- },
159
- );
165
+ });
160
166
 
161
167
  getLocations: Selector<L, F, Iterable<L> | undefined> = createSelector(
162
- this.getFetchedLocations,
168
+ this.getLocationsFromProps,
163
169
  this.getInvalidLocationIds,
164
170
  (locations, invalidIds) => {
165
171
  if (!locations) return undefined;
@@ -176,41 +182,43 @@ export default class FlowmapSelectors<L, F> {
176
182
  },
177
183
  );
178
184
 
179
- getLocationIds: Selector<L, F, Set<string> | undefined> = createSelector(
180
- this.getLocations,
181
- (locations) => {
185
+ getLocationIds: Selector<L, F, Set<string | number> | undefined> =
186
+ createSelector(this.getLocations, (locations) => {
182
187
  if (!locations) return undefined;
183
- const ids = new Set<string>();
188
+ const ids = new Set<string | number>();
184
189
  for (const id of locations) {
185
190
  ids.add(this.accessors.getLocationId(id));
186
191
  }
187
192
  return ids;
188
- },
189
- );
193
+ });
190
194
 
191
- getSelectedLocationsSet: Selector<L, F, Set<string> | undefined> =
195
+ getSelectedLocationsSet: Selector<L, F, Set<string | number> | undefined> =
192
196
  createSelector(this.getSelectedLocations, (ids) =>
193
197
  ids && ids.length > 0 ? new Set(ids) : undefined,
194
198
  );
195
199
 
196
200
  getSortedFlowsForKnownLocations: Selector<L, F, F[] | undefined> =
197
- createSelector(this.getFetchedFlows, this.getLocationIds, (flows, ids) => {
198
- if (!ids || !flows) return undefined;
199
- const filtered = [];
200
- for (const flow of flows) {
201
- const srcId = this.accessors.getFlowOriginId(flow);
202
- const dstId = this.accessors.getFlowDestId(flow);
203
- if (ids.has(srcId) && ids.has(dstId)) {
204
- filtered.push(flow);
201
+ createSelector(
202
+ this.getFlowsFromProps,
203
+ this.getLocationIds,
204
+ (flows, ids) => {
205
+ if (!ids || !flows) return undefined;
206
+ const filtered = [];
207
+ for (const flow of flows) {
208
+ const srcId = this.accessors.getFlowOriginId(flow);
209
+ const dstId = this.accessors.getFlowDestId(flow);
210
+ if (ids.has(srcId) && ids.has(dstId)) {
211
+ filtered.push(flow);
212
+ }
205
213
  }
206
- }
207
- return filtered.sort((a: F, b: F) =>
208
- descending(
209
- Math.abs(this.accessors.getFlowMagnitude(a)),
210
- Math.abs(this.accessors.getFlowMagnitude(b)),
211
- ),
212
- );
213
- });
214
+ return filtered.sort((a: F, b: F) =>
215
+ descending(
216
+ Math.abs(this.accessors.getFlowMagnitude(a)),
217
+ Math.abs(this.accessors.getFlowMagnitude(b)),
218
+ ),
219
+ );
220
+ },
221
+ );
214
222
 
215
223
  getActualTimeExtent: Selector<L, F, [Date, Date] | undefined> =
216
224
  createSelector(this.getSortedFlowsForKnownLocations, (flows) => {
@@ -303,29 +311,33 @@ export default class FlowmapSelectors<L, F> {
303
311
  },
304
312
  );
305
313
 
306
- getLocationsById: Selector<L, F, Map<string, L> | undefined> = createSelector(
307
- this.getLocationsHavingFlows,
308
- (locations) => {
314
+ getLocationsById: Selector<L, F, Map<string | number, L> | undefined> =
315
+ createSelector(this.getLocationsHavingFlows, (locations) => {
309
316
  if (!locations) return undefined;
310
- const locationsById = new Map<string, L>();
317
+ const locationsById = new Map<string | number, L>();
311
318
  for (const location of locations) {
312
319
  locationsById.set(this.accessors.getLocationId(location), location);
313
320
  }
314
321
  return locationsById;
315
- },
316
- );
317
-
318
- getClusterIndex: Selector<L, F, ClusterIndex<F> | undefined> = createSelector(
319
- this.getLocationsHavingFlows,
320
- this.getLocationsById,
321
- this.getSortedFlowsForKnownLocations,
322
- (locations, locationsById, flows) => {
323
- if (!locations || !locationsById || !flows) return undefined;
322
+ });
324
323
 
324
+ getLocationWeightGetter: Selector<L, F, LocationWeightGetter | undefined> =
325
+ createSelector(this.getSortedFlowsForKnownLocations, (flows) => {
326
+ if (!flows) return undefined;
325
327
  const getLocationWeight = makeLocationWeightGetter(
326
328
  flows,
327
329
  this.accessors.getFlowmapDataAccessors(),
328
330
  );
331
+ return getLocationWeight;
332
+ });
333
+
334
+ getClusterLevels: Selector<L, F, ClusterLevels | undefined> = createSelector(
335
+ this.getClusterLevelsFromProps,
336
+ this.getLocationsHavingFlows,
337
+ this.getLocationWeightGetter,
338
+ (clusterLevelsFromProps, locations, getLocationWeight) => {
339
+ if (clusterLevelsFromProps) return clusterLevelsFromProps;
340
+ if (!locations || !getLocationWeight) return undefined;
329
341
  const clusterLevels = clusterLocations(
330
342
  locations,
331
343
  this.accessors.getFlowmapDataAccessors(),
@@ -334,47 +346,27 @@ export default class FlowmapSelectors<L, F> {
334
346
  maxZoom: MAX_CLUSTER_ZOOM_LEVEL,
335
347
  },
336
348
  );
337
- const clusterIndex = buildIndex<F>(clusterLevels);
338
- const {getLocationName, getLocationClusterName} =
339
- this.accessors.getFlowmapDataAccessors();
340
-
341
- // Adding meaningful names
342
- const getName = (id: string) => {
343
- const loc = locationsById.get(id);
344
- if (loc) {
345
- return getLocationName
346
- ? getLocationName(loc)
347
- : this.accessors.getLocationId(loc) || id;
348
- }
349
- return `"${id}"`;
350
- };
351
- for (const level of clusterLevels) {
352
- for (const node of level.nodes) {
353
- // Here mutating the nodes (adding names)
354
- if (isCluster(node)) {
355
- const leaves = clusterIndex.expandCluster(node);
356
-
357
- leaves.sort((a, b) =>
358
- descending(getLocationWeight(a), getLocationWeight(b)),
359
- );
349
+ return clusterLevels;
350
+ },
351
+ );
360
352
 
361
- if (getLocationClusterName) {
362
- node.name = getLocationClusterName(leaves);
363
- } else {
364
- const topId = leaves[0];
365
- const otherId = leaves.length === 2 ? leaves[1] : undefined;
366
- node.name = `"${getName(topId)}" and ${
367
- otherId
368
- ? `"${getName(otherId)}"`
369
- : `${leaves.length - 1} others`
370
- }`;
371
- }
372
- } else {
373
- (node as any).name = getName(node.id);
374
- }
375
- }
376
- }
353
+ getClusterIndex: Selector<L, F, ClusterIndex<F> | undefined> = createSelector(
354
+ this.getLocationsById,
355
+ this.getLocationWeightGetter,
356
+ this.getClusterLevels,
357
+ (locationsById, getLocationWeight, clusterLevels) => {
358
+ if (!locationsById || !getLocationWeight || !clusterLevels)
359
+ return undefined;
377
360
 
361
+ const clusterIndex = buildIndex<F>(clusterLevels);
362
+ // Adding meaningful names
363
+ addClusterNames(
364
+ clusterIndex,
365
+ clusterLevels,
366
+ locationsById,
367
+ this.accessors.getFlowmapDataAccessors(),
368
+ getLocationWeight,
369
+ );
378
370
  return clusterIndex;
379
371
  },
380
372
  );
@@ -390,7 +382,7 @@ export default class FlowmapSelectors<L, F> {
390
382
  let maxZoom = Number.POSITIVE_INFINITY;
391
383
  let minZoom = Number.NEGATIVE_INFINITY;
392
384
 
393
- const adjust = (zoneId: string) => {
385
+ const adjust = (zoneId: string | number) => {
394
386
  const cluster = clusterIndex.getClusterById(zoneId);
395
387
  if (cluster) {
396
388
  minZoom = Math.max(minZoom, cluster.zoom);
@@ -419,7 +411,7 @@ export default class FlowmapSelectors<L, F> {
419
411
  this.getAvailableClusterZoomLevels,
420
412
  (clusterIndex, mapZoom, availableClusterZoomLevels) => {
421
413
  if (!clusterIndex) return undefined;
422
- if (!availableClusterZoomLevels) {
414
+ if (!availableClusterZoomLevels || mapZoom == null) {
423
415
  return undefined;
424
416
  }
425
417
 
@@ -432,12 +424,12 @@ export default class FlowmapSelectors<L, F> {
432
424
  );
433
425
 
434
426
  getClusterZoom = (state: FlowmapState, props: FlowmapData<L, F>) => {
435
- const {settingsState} = state;
436
- if (!settingsState.clusteringEnabled) return undefined;
437
- if (settingsState.clusteringAuto || settingsState.clusteringLevel == null) {
427
+ const {settings} = state;
428
+ if (!settings.clusteringEnabled) return undefined;
429
+ if (settings.clusteringAuto || settings.clusteringLevel == null) {
438
430
  return this._getClusterZoom(state, props);
439
431
  }
440
- return settingsState.clusteringLevel;
432
+ return settings.clusteringLevel;
441
433
  };
442
434
 
443
435
  getLocationsForSearchBox: Selector<L, F, (L | Cluster)[] | undefined> =
@@ -490,7 +482,7 @@ export default class FlowmapSelectors<L, F> {
490
482
  );
491
483
 
492
484
  getDiffMode: Selector<L, F, boolean> = createSelector(
493
- this.getFetchedFlows,
485
+ this.getFlowsFromProps,
494
486
  (flows) => {
495
487
  if (flows) {
496
488
  for (const f of flows) {
@@ -523,27 +515,28 @@ export default class FlowmapSelectors<L, F> {
523
515
  },
524
516
  );
525
517
 
526
- getUnknownLocations: Selector<L, F, Set<string> | undefined> = createSelector(
527
- this.getLocationIds,
528
- this.getFetchedFlows,
529
- this.getSortedFlowsForKnownLocations,
530
- (ids, flows, flowsForKnownLocations) => {
531
- if (!ids || !flows) return undefined;
532
- if (
533
- flowsForKnownLocations
534
- // && flows.length === flowsForKnownLocations.length
535
- )
536
- return undefined;
537
- const missing = new Set<string>();
538
- for (const flow of flows) {
539
- if (!ids.has(this.accessors.getFlowOriginId(flow)))
540
- missing.add(this.accessors.getFlowOriginId(flow));
541
- if (!ids.has(this.accessors.getFlowDestId(flow)))
542
- missing.add(this.accessors.getFlowDestId(flow));
543
- }
544
- return missing;
545
- },
546
- );
518
+ getUnknownLocations: Selector<L, F, Set<string | number> | undefined> =
519
+ createSelector(
520
+ this.getLocationIds,
521
+ this.getFlowsFromProps,
522
+ this.getSortedFlowsForKnownLocations,
523
+ (ids, flows, flowsForKnownLocations) => {
524
+ if (!ids || !flows) return undefined;
525
+ if (
526
+ flowsForKnownLocations
527
+ // && flows.length === flowsForKnownLocations.length
528
+ )
529
+ return undefined;
530
+ const missing = new Set<string | number>();
531
+ for (const flow of flows) {
532
+ if (!ids.has(this.accessors.getFlowOriginId(flow)))
533
+ missing.add(this.accessors.getFlowOriginId(flow));
534
+ if (!ids.has(this.accessors.getFlowDestId(flow)))
535
+ missing.add(this.accessors.getFlowDestId(flow));
536
+ }
537
+ return missing;
538
+ },
539
+ );
547
540
 
548
541
  getSortedAggregatedFilteredFlows: Selector<
549
542
  L,
@@ -584,31 +577,34 @@ export default class FlowmapSelectors<L, F> {
584
577
  },
585
578
  );
586
579
 
587
- getExpandedSelectedLocationsSet: Selector<L, F, Set<string> | undefined> =
588
- createSelector(
589
- this.getClusteringEnabled,
590
- this.getSelectedLocationsSet,
591
- this.getClusterIndex,
592
- (clusteringEnabled, selectedLocations, clusterIndex) => {
593
- if (!selectedLocations || !clusterIndex) {
594
- return selectedLocations;
595
- }
580
+ getExpandedSelectedLocationsSet: Selector<
581
+ L,
582
+ F,
583
+ Set<string | number> | undefined
584
+ > = createSelector(
585
+ this.getClusteringEnabled,
586
+ this.getSelectedLocationsSet,
587
+ this.getClusterIndex,
588
+ (clusteringEnabled, selectedLocations, clusterIndex) => {
589
+ if (!selectedLocations || !clusterIndex) {
590
+ return selectedLocations;
591
+ }
596
592
 
597
- const result = new Set<string>();
598
- for (const locationId of selectedLocations) {
599
- const cluster = clusterIndex.getClusterById(locationId);
600
- if (cluster) {
601
- const expanded = clusterIndex.expandCluster(cluster);
602
- for (const id of expanded) {
603
- result.add(id);
604
- }
605
- } else {
606
- result.add(locationId);
593
+ const result = new Set<string | number>();
594
+ for (const locationId of selectedLocations) {
595
+ const cluster = clusterIndex.getClusterById(locationId);
596
+ if (cluster) {
597
+ const expanded = clusterIndex.expandCluster(cluster);
598
+ for (const id of expanded) {
599
+ result.add(id);
607
600
  }
601
+ } else {
602
+ result.add(locationId);
608
603
  }
609
- return result;
610
- },
611
- );
604
+ }
605
+ return result;
606
+ },
607
+ );
612
608
 
613
609
  getTotalCountsByTime: Selector<L, F, CountByTime[] | undefined> =
614
610
  createSelector(
@@ -663,15 +659,7 @@ export default class FlowmapSelectors<L, F> {
663
659
  createSelector(
664
660
  this.getViewport,
665
661
  this.getMaxLocationCircleSize,
666
- (viewport, maxLocationCircleSize) => {
667
- const pad = maxLocationCircleSize;
668
- const bounds = new WebMercatorViewport({
669
- ...viewport,
670
- width: viewport.width + pad * 2,
671
- height: viewport.height + pad * 2,
672
- }).getBounds();
673
- return [bounds[0][0], bounds[0][1], bounds[1][0], bounds[1][1]];
674
- },
662
+ getViewportBoundingBox,
675
663
  );
676
664
 
677
665
  getLocationsForZoom: Selector<L, F, Iterable<L> | ClusterNode[] | undefined> =
@@ -689,47 +677,50 @@ export default class FlowmapSelectors<L, F> {
689
677
  },
690
678
  );
691
679
 
692
- getLocationTotals: Selector<L, F, Map<string, LocationTotals> | undefined> =
693
- createSelector(
694
- this.getLocationsForZoom,
695
- this.getSortedAggregatedFilteredFlows,
696
- this.getSelectedLocationsSet,
697
- this.getLocationFilterMode,
698
- (locations, flows, selectedLocationsSet, locationFilterMode) => {
699
- if (!flows) return undefined;
700
- const totals = new Map<string, LocationTotals>();
701
- const add = (
702
- id: string,
703
- d: Partial<LocationTotals>,
704
- ): LocationTotals => {
705
- const rv = totals.get(id) ?? {
706
- incomingCount: 0,
707
- outgoingCount: 0,
708
- internalCount: 0,
709
- };
710
- if (d.incomingCount != null) rv.incomingCount += d.incomingCount;
711
- if (d.outgoingCount != null) rv.outgoingCount += d.outgoingCount;
712
- if (d.internalCount != null) rv.internalCount += d.internalCount;
713
- return rv;
680
+ getLocationTotals: Selector<
681
+ L,
682
+ F,
683
+ Map<string | number, LocationTotals> | undefined
684
+ > = createSelector(
685
+ this.getLocationsForZoom,
686
+ this.getSortedAggregatedFilteredFlows,
687
+ this.getSelectedLocationsSet,
688
+ this.getLocationFilterMode,
689
+ (locations, flows, selectedLocationsSet, locationFilterMode) => {
690
+ if (!flows) return undefined;
691
+ const totals = new Map<string | number, LocationTotals>();
692
+ const add = (
693
+ id: string | number,
694
+ d: Partial<LocationTotals>,
695
+ ): LocationTotals => {
696
+ const rv = totals.get(id) ?? {
697
+ incomingCount: 0,
698
+ outgoingCount: 0,
699
+ internalCount: 0,
714
700
  };
715
- for (const f of flows) {
716
- if (
717
- this.isFlowInSelection(f, selectedLocationsSet, locationFilterMode)
718
- ) {
719
- const originId = this.accessors.getFlowOriginId(f);
720
- const destId = this.accessors.getFlowDestId(f);
721
- const count = this.accessors.getFlowMagnitude(f);
722
- if (originId === destId) {
723
- totals.set(originId, add(originId, {internalCount: count}));
724
- } else {
725
- totals.set(originId, add(originId, {outgoingCount: count}));
726
- totals.set(destId, add(destId, {incomingCount: count}));
727
- }
701
+ if (d.incomingCount != null) rv.incomingCount += d.incomingCount;
702
+ if (d.outgoingCount != null) rv.outgoingCount += d.outgoingCount;
703
+ if (d.internalCount != null) rv.internalCount += d.internalCount;
704
+ return rv;
705
+ };
706
+ for (const f of flows) {
707
+ if (
708
+ this.isFlowInSelection(f, selectedLocationsSet, locationFilterMode)
709
+ ) {
710
+ const originId = this.accessors.getFlowOriginId(f);
711
+ const destId = this.accessors.getFlowDestId(f);
712
+ const count = this.accessors.getFlowMagnitude(f);
713
+ if (originId === destId) {
714
+ totals.set(originId, add(originId, {internalCount: count}));
715
+ } else {
716
+ totals.set(originId, add(originId, {outgoingCount: count}));
717
+ totals.set(destId, add(destId, {incomingCount: count}));
728
718
  }
729
719
  }
730
- return totals;
731
- },
732
- );
720
+ }
721
+ return totals;
722
+ },
723
+ );
733
724
 
734
725
  getLocationsTree: Selector<L, F, KDBushTree> = createSelector(
735
726
  this.getLocationsForZoom,
@@ -737,14 +728,20 @@ export default class FlowmapSelectors<L, F> {
737
728
  if (!locations) {
738
729
  return undefined;
739
730
  }
740
- return new KDBush(
741
- // @ts-ignore
742
- locations,
743
- (location: L | ClusterNode) =>
744
- lngX(this.accessors.getLocationLon(location)),
745
- (location: L | ClusterNode) =>
746
- latY(this.accessors.getLocationLat(location)),
747
- );
731
+ const nodes = Array.isArray(locations)
732
+ ? locations
733
+ : Array.from(locations);
734
+ const bush = new KDBush(nodes.length, 64, Float32Array);
735
+ for (let i = 0; i < nodes.length; i++) {
736
+ const node = nodes[i];
737
+ bush.add(
738
+ lngX(this.accessors.getLocationLon(node)),
739
+ latY(this.accessors.getLocationLat(node)),
740
+ );
741
+ }
742
+ bush.finish();
743
+ bush.points = nodes;
744
+ return bush;
748
745
  },
749
746
  );
750
747
 
@@ -763,7 +760,7 @@ export default class FlowmapSelectors<L, F> {
763
760
  },
764
761
  );
765
762
 
766
- getLocationIdsInViewport: Selector<L, F, Set<string> | undefined> =
763
+ getLocationIdsInViewport: Selector<L, F, Set<string | number> | undefined> =
767
764
  createSelectorCreator(
768
765
  defaultMemoize,
769
766
  // @ts-ignore
@@ -835,7 +832,7 @@ export default class FlowmapSelectors<L, F> {
835
832
  state: FlowmapState,
836
833
  props: FlowmapData<L, F>,
837
834
  ): [number, number] | undefined => {
838
- if (state.settingsState.adaptiveScalesEnabled) {
835
+ if (state.settings.adaptiveScalesEnabled) {
839
836
  return this._getLocationTotalsForViewportExtent(state, props);
840
837
  } else {
841
838
  return this._getLocationTotalsExtent(state, props);
@@ -930,7 +927,7 @@ export default class FlowmapSelectors<L, F> {
930
927
  state: FlowmapState,
931
928
  props: FlowmapData<L, F>,
932
929
  ): [number, number] | undefined => {
933
- if (state.settingsState.adaptiveScalesEnabled) {
930
+ if (state.settings.adaptiveScalesEnabled) {
934
931
  return this._getAdaptiveFlowMagnitudeExtent(state, props);
935
932
  } else {
936
933
  return this._getFlowMagnitudeExtent(state, props);
@@ -953,19 +950,7 @@ export default class FlowmapSelectors<L, F> {
953
950
 
954
951
  getFlowThicknessScale = createSelector(
955
952
  this.getFlowMagnitudeExtent,
956
- (magnitudeExtent) => {
957
- if (!magnitudeExtent) return undefined;
958
- return scaleLinear()
959
- .range([0.025, 0.5])
960
- .domain([
961
- 0,
962
- // should support diff mode too
963
- Math.max.apply(
964
- null,
965
- magnitudeExtent.map((x: number | undefined) => Math.abs(x || 0)),
966
- ),
967
- ]);
968
- },
953
+ getFlowThicknessScale,
969
954
  );
970
955
 
971
956
  getCircleSizeScale = createSelector(
@@ -996,7 +981,7 @@ export default class FlowmapSelectors<L, F> {
996
981
  this.getCircleSizeScale,
997
982
  this.getLocationTotals,
998
983
  (circleSizeScale, locationTotals) => {
999
- return (locationId: string) => {
984
+ return (locationId: string | number) => {
1000
985
  const total = locationTotals?.get(locationId);
1001
986
  if (total && circleSizeScale) {
1002
987
  return (
@@ -1014,7 +999,7 @@ export default class FlowmapSelectors<L, F> {
1014
999
  this.getCircleSizeScale,
1015
1000
  this.getLocationTotals,
1016
1001
  (circleSizeScale, locationTotals) => {
1017
- return (locationId: string) => {
1002
+ return (locationId: string | number) => {
1018
1003
  const total = locationTotals?.get(locationId);
1019
1004
  if (total && circleSizeScale) {
1020
1005
  return (
@@ -1089,6 +1074,15 @@ export default class FlowmapSelectors<L, F> {
1089
1074
  );
1090
1075
  });
1091
1076
 
1077
+ getLocationOrClusterByIdGetter = createSelector(
1078
+ this.getClusterIndex,
1079
+ this.getLocationsById,
1080
+ (clusterIndex, locationsById) => {
1081
+ return (id: string | number) =>
1082
+ clusterIndex?.getClusterById(id) ?? locationsById?.get(id);
1083
+ },
1084
+ );
1085
+
1092
1086
  getLayersData: Selector<L, F, LayersData> = createSelector(
1093
1087
  this.getLocationsForFlowmapLayer,
1094
1088
  this.getFlowsForFlowmapLayer,
@@ -1099,6 +1093,7 @@ export default class FlowmapSelectors<L, F> {
1099
1093
  this.getOutCircleSizeGetter,
1100
1094
  this.getFlowThicknessScale,
1101
1095
  this.getAnimate,
1096
+ this.getLocationLabelsEnabled,
1102
1097
  (
1103
1098
  locations,
1104
1099
  flows,
@@ -1109,6 +1104,7 @@ export default class FlowmapSelectors<L, F> {
1109
1104
  getOutCircleSize,
1110
1105
  flowThicknessScale,
1111
1106
  animationEnabled,
1107
+ locationLabelsEnabled,
1112
1108
  ) => {
1113
1109
  return this._prepareLayersData(
1114
1110
  locations,
@@ -1120,6 +1116,7 @@ export default class FlowmapSelectors<L, F> {
1120
1116
  getOutCircleSize,
1121
1117
  flowThicknessScale,
1122
1118
  animationEnabled,
1119
+ locationLabelsEnabled,
1123
1120
  );
1124
1121
  },
1125
1122
  );
@@ -1133,6 +1130,7 @@ export default class FlowmapSelectors<L, F> {
1133
1130
  const getInCircleSize = this.getInCircleSizeGetter(state, props);
1134
1131
  const getOutCircleSize = this.getOutCircleSizeGetter(state, props);
1135
1132
  const flowThicknessScale = this.getFlowThicknessScale(state, props);
1133
+ const locationLabelsEnabled = this.getLocationLabelsEnabled(state, props);
1136
1134
  return this._prepareLayersData(
1137
1135
  locations,
1138
1136
  flows,
@@ -1142,7 +1140,8 @@ export default class FlowmapSelectors<L, F> {
1142
1140
  getInCircleSize,
1143
1141
  getOutCircleSize,
1144
1142
  flowThicknessScale,
1145
- state.settingsState.animationEnabled,
1143
+ state.settings.animationEnabled,
1144
+ locationLabelsEnabled,
1146
1145
  );
1147
1146
  }
1148
1147
 
@@ -1150,12 +1149,13 @@ export default class FlowmapSelectors<L, F> {
1150
1149
  locations: (L | ClusterNode)[] | undefined,
1151
1150
  flows: (F | AggregateFlow)[] | undefined,
1152
1151
  flowmapColors: DiffColorsRGBA | ColorsRGBA,
1153
- locationsById: Map<string, L | ClusterNode> | undefined,
1154
- locationIdsInViewport: Set<string> | undefined,
1155
- getInCircleSize: (locationId: string) => number,
1156
- getOutCircleSize: (locationId: string) => number,
1152
+ locationsById: Map<string | number, L | ClusterNode> | undefined,
1153
+ locationIdsInViewport: Set<string | number> | undefined,
1154
+ getInCircleSize: (locationId: string | number) => number,
1155
+ getOutCircleSize: (locationId: string | number) => number,
1157
1156
  flowThicknessScale: ScaleLinear<number, number, never> | undefined,
1158
1157
  animationEnabled: boolean,
1158
+ locationLabelsEnabled: boolean,
1159
1159
  ): LayersData {
1160
1160
  if (!locations) locations = [];
1161
1161
  if (!flows) flows = [];
@@ -1166,6 +1166,7 @@ export default class FlowmapSelectors<L, F> {
1166
1166
  getLocationId,
1167
1167
  getLocationLon,
1168
1168
  getLocationLat,
1169
+ getLocationName,
1169
1170
  } = this.accessors;
1170
1171
 
1171
1172
  const flowMagnitudeExtent = extent(flows, (f) => getFlowMagnitude(f)) as [
@@ -1297,6 +1298,9 @@ export default class FlowmapSelectors<L, F> {
1297
1298
  : {}),
1298
1299
  },
1299
1300
  },
1301
+ ...(locationLabelsEnabled
1302
+ ? {locationLabels: locations.map(getLocationName)}
1303
+ : undefined),
1300
1304
  };
1301
1305
  }
1302
1306
 
@@ -1327,8 +1331,8 @@ export default class FlowmapSelectors<L, F> {
1327
1331
 
1328
1332
  isFlowInSelection(
1329
1333
  flow: F | AggregateFlow,
1330
- selectedLocationsSet: Set<string> | undefined,
1331
- locationFilterMode: LocationFilterMode,
1334
+ selectedLocationsSet: Set<string | number> | undefined,
1335
+ locationFilterMode?: LocationFilterMode,
1332
1336
  ) {
1333
1337
  const origin = this.accessors.getFlowOriginId(flow);
1334
1338
  const dest = this.accessors.getFlowDestId(flow);
@@ -1374,8 +1378,8 @@ export default class FlowmapSelectors<L, F> {
1374
1378
  }
1375
1379
 
1376
1380
  function calcLocationTotalsExtent(
1377
- locationTotals: Map<string, LocationTotals> | undefined,
1378
- locationIdsInViewport: Set<string> | undefined,
1381
+ locationTotals: Map<string | number, LocationTotals> | undefined,
1382
+ locationIdsInViewport: Set<string | number> | undefined,
1379
1383
  ) {
1380
1384
  if (!locationTotals) return undefined;
1381
1385
  let rv: [number, number] | undefined = undefined;
@@ -1421,10 +1425,9 @@ function aggregateFlows<F>(
1421
1425
  flowAccessors: FlowAccessors<F>,
1422
1426
  ): AggregateFlow[] {
1423
1427
  // Sum up flows with same origin, dest
1424
- const byOriginDest = nest<F, AggregateFlow>()
1425
- .key(flowAccessors.getFlowOriginId)
1426
- .key(flowAccessors.getFlowDestId)
1427
- .rollup((ff: F[]) => {
1428
+ const byOriginDest = rollup(
1429
+ flows,
1430
+ (ff: F[]) => {
1428
1431
  const origin = flowAccessors.getFlowOriginId(ff[0]);
1429
1432
  const dest = flowAccessors.getFlowDestId(ff[0]);
1430
1433
  // const color = ff[0].color;
@@ -1443,11 +1446,14 @@ function aggregateFlows<F>(
1443
1446
  };
1444
1447
  // if (color) rv.color = color;
1445
1448
  return rv;
1446
- })
1447
- .entries(flows);
1449
+ },
1450
+ flowAccessors.getFlowOriginId,
1451
+ flowAccessors.getFlowDestId,
1452
+ );
1453
+
1448
1454
  const rv: AggregateFlow[] = [];
1449
- for (const {values} of byOriginDest) {
1450
- for (const {value} of values) {
1455
+ for (const values of byOriginDest.values()) {
1456
+ for (const value of values.values()) {
1451
1457
  rv.push(value);
1452
1458
  }
1453
1459
  }