@flowmap.gl/data 8.0.0-alpha.18 → 8.0.0-alpha.21

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 (44) hide show
  1. package/dist/FlowmapAggregateAccessors.d.ts +3 -3
  2. package/dist/FlowmapAggregateAccessors.d.ts.map +1 -1
  3. package/dist/FlowmapAggregateAccessors.js +2 -2
  4. package/dist/FlowmapSelectors.d.ts +44 -33
  5. package/dist/FlowmapSelectors.d.ts.map +1 -1
  6. package/dist/FlowmapSelectors.js +50 -49
  7. package/dist/FlowmapState.d.ts +5 -5
  8. package/dist/FlowmapState.d.ts.map +1 -1
  9. package/dist/FlowmapState.js +1 -1
  10. package/dist/cluster/ClusterIndex.d.ts +3 -3
  11. package/dist/cluster/ClusterIndex.d.ts.map +1 -1
  12. package/dist/cluster/ClusterIndex.js +1 -1
  13. package/dist/cluster/cluster.d.ts.map +1 -1
  14. package/dist/cluster/cluster.js +64 -13
  15. package/dist/colors.d.ts +1 -1
  16. package/dist/colors.d.ts.map +1 -1
  17. package/dist/colors.js +3 -3
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +4 -1
  21. package/dist/provider/FlowmapDataProvider.d.ts +7 -2
  22. package/dist/provider/FlowmapDataProvider.d.ts.map +1 -1
  23. package/dist/provider/FlowmapDataProvider.js +1 -1
  24. package/dist/provider/LocalFlowmapDataProvider.d.ts +3 -2
  25. package/dist/provider/LocalFlowmapDataProvider.d.ts.map +1 -1
  26. package/dist/provider/LocalFlowmapDataProvider.js +6 -1
  27. package/dist/selector-functions.d.ts +4 -0
  28. package/dist/selector-functions.d.ts.map +1 -0
  29. package/dist/selector-functions.js +20 -0
  30. package/dist/types.d.ts +12 -11
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/types.js +1 -1
  33. package/package.json +2 -3
  34. package/src/FlowmapAggregateAccessors.ts +2 -2
  35. package/src/FlowmapSelectors.ts +171 -160
  36. package/src/FlowmapState.ts +5 -5
  37. package/src/cluster/ClusterIndex.ts +19 -12
  38. package/src/cluster/cluster.ts +71 -16
  39. package/src/colors.ts +7 -9
  40. package/src/index.ts +3 -0
  41. package/src/provider/FlowmapDataProvider.ts +13 -2
  42. package/src/provider/LocalFlowmapDataProvider.ts +10 -2
  43. package/src/selector-functions.ts +34 -0
  44. package/src/types.ts +12 -11
@@ -27,14 +27,14 @@ import {
27
27
  } from './../types';
28
28
  import {ascending, bisectLeft, extent} from 'd3-array';
29
29
 
30
- export type LocationWeightGetter = (id: string) => number;
30
+ export type LocationWeightGetter = (id: string | number) => number;
31
31
 
32
32
  /**
33
33
  * A data structure representing the cluster levels for efficient flow aggregation.
34
34
  */
35
35
  export interface ClusterIndex<F> {
36
36
  availableZoomLevels: number[];
37
- getClusterById: (clusterId: string) => Cluster | undefined;
37
+ getClusterById: (clusterId: string | number) => Cluster | undefined;
38
38
  /**
39
39
  * List the nodes on the given zoom level.
40
40
  */
@@ -50,7 +50,10 @@ export interface ClusterIndex<F> {
50
50
  /**
51
51
  * Find the cluster the given location is residing in on the specified zoom level.
52
52
  */
53
- findClusterFor: (locationId: string, zoom: number) => string | undefined;
53
+ findClusterFor: (
54
+ locationId: string | number,
55
+ zoom: number,
56
+ ) => string | number | undefined;
54
57
  /**
55
58
  * Aggregate flows for the specified zoom level.
56
59
  */
@@ -69,8 +72,8 @@ export interface ClusterIndex<F> {
69
72
  */
70
73
  export function buildIndex<F>(clusterLevels: ClusterLevels): ClusterIndex<F> {
71
74
  const nodesByZoom = new Map<number, ClusterNode[]>();
72
- const clustersById = new Map<string, Cluster>();
73
- const minZoomByLocationId = new Map<string, number>();
75
+ const clustersById = new Map<string | number, Cluster>();
76
+ const minZoomByLocationId = new Map<string | number, number>();
74
77
  for (const {zoom, nodes} of clusterLevels) {
75
78
  nodesByZoom.set(zoom, nodes);
76
79
  for (const node of nodes) {
@@ -91,7 +94,10 @@ export function buildIndex<F>(clusterLevels: ClusterLevels): ClusterIndex<F> {
91
94
  throw new Error('Could not determine minZoom or maxZoom');
92
95
  }
93
96
 
94
- const leavesToClustersByZoom = new Map<number, Map<string, Cluster>>();
97
+ const leavesToClustersByZoom = new Map<
98
+ number,
99
+ Map<string | number, Cluster>
100
+ >();
95
101
 
96
102
  for (const cluster of clustersById.values()) {
97
103
  const {zoom} = cluster;
@@ -118,7 +124,7 @@ export function buildIndex<F>(clusterLevels: ClusterLevels): ClusterIndex<F> {
118
124
 
119
125
  const expandCluster = (cluster: Cluster, targetZoom: number = maxZoom) => {
120
126
  const ids: string[] = [];
121
- const visit = (c: Cluster, expandedIds: string[]) => {
127
+ const visit = (c: Cluster, expandedIds: (string | number)[]) => {
122
128
  if (targetZoom > c.zoom) {
123
129
  for (const childId of c.children) {
124
130
  const child = clustersById.get(childId);
@@ -136,7 +142,7 @@ export function buildIndex<F>(clusterLevels: ClusterLevels): ClusterIndex<F> {
136
142
  return ids;
137
143
  };
138
144
 
139
- function findClusterFor(locationId: string, zoom: number) {
145
+ function findClusterFor(locationId: string | number, zoom: number) {
140
146
  const leavesToClusters = leavesToClustersByZoom.get(zoom);
141
147
  if (!leavesToClusters) {
142
148
  return undefined;
@@ -179,7 +185,8 @@ export function buildIndex<F>(clusterLevels: ClusterLevels): ClusterIndex<F> {
179
185
  }
180
186
  const result: (F | AggregateFlow)[] = [];
181
187
  const aggFlowsByKey = new Map<string, AggregateFlow>();
182
- const makeKey = (origin: string, dest: string) => `${origin}:${dest}`;
188
+ const makeKey = (origin: string | number, dest: string | number) =>
189
+ `${origin}:${dest}`;
183
190
  const {
184
191
  flowCountsMapReduce = {
185
192
  map: getFlowMagnitude,
@@ -223,8 +230,8 @@ export function makeLocationWeightGetter<F>(
223
230
  {getFlowOriginId, getFlowDestId, getFlowMagnitude}: FlowAccessors<F>,
224
231
  ): LocationWeightGetter {
225
232
  const locationTotals = {
226
- incoming: new Map<string, number>(),
227
- outgoing: new Map<string, number>(),
233
+ incoming: new Map<string | number, number>(),
234
+ outgoing: new Map<string | number, number>(),
228
235
  };
229
236
  for (const flow of flows) {
230
237
  const origin = getFlowOriginId(flow);
@@ -239,7 +246,7 @@ export function makeLocationWeightGetter<F>(
239
246
  (locationTotals.outgoing.get(origin) || 0) + count,
240
247
  );
241
248
  }
242
- return (id: string) =>
249
+ return (id: string | number) =>
243
250
  Math.max(
244
251
  Math.abs(locationTotals.incoming.get(id) || 0),
245
252
  Math.abs(locationTotals.outgoing.get(id) || 0),
@@ -36,7 +36,7 @@
36
36
  // TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
37
37
  // THIS SOFTWARE.
38
38
 
39
- import {rollup} from 'd3-array';
39
+ import {min, rollup} from 'd3-array';
40
40
  import KDBush from 'kdbush';
41
41
  import {LocationWeightGetter} from './ClusterIndex';
42
42
  import {Cluster, ClusterLevel, ClusterNode, LocationAccessors} from '../types';
@@ -110,7 +110,7 @@ export function clusterLocations<L>(
110
110
 
111
111
  // generate a cluster object for each point and index input points into a KD-tree
112
112
  let clusters = new Array<Point<L>>();
113
- let i = 0;
113
+ let locationsCount = 0;
114
114
  for (const location of locations) {
115
115
  const x = getLocationLon(location);
116
116
  const y = getLocationLat(location);
@@ -119,41 +119,65 @@ export function clusterLocations<L>(
119
119
  y: latY(y),
120
120
  weight: getLocationWeight(getLocationId(location)),
121
121
  zoom: Infinity, // the last zoom the point was processed at
122
- index: i, // index of the source feature in the original input array,
122
+ index: locationsCount, // index of the source feature in the original input array,
123
123
  parentId: -1, // parent cluster id
124
124
  location,
125
125
  });
126
- i++;
126
+ locationsCount++;
127
127
  }
128
- trees[maxZoom + 1] = new KDBush(clusters, getX, getY, nodeSize, Float32Array);
129
128
 
130
129
  // cluster points on max zoom, then cluster the results on previous zoom, etc.;
131
130
  // results in a cluster hierarchy across zoom levels
131
+ trees[maxZoom + 1] = new KDBush(clusters, getX, getY, nodeSize, Float32Array);
132
+ let prevZoom = maxZoom + 1;
133
+
132
134
  for (let z = maxZoom; z >= minZoom; z--) {
133
135
  // create a new set of clusters for the zoom and index them with a KD-tree
134
- clusters = cluster(clusters, z, trees[z + 1], opts);
135
- trees[z] = new KDBush(clusters, getX, getY, nodeSize, Float32Array);
136
+ const _clusters = cluster(clusters, z, trees[prevZoom], opts);
137
+ if (_clusters.length === clusters.length) {
138
+ // same number of clusters => move the higher level clusters up
139
+ // no need to keep the same data on multiple levels
140
+ trees[z] = trees[prevZoom];
141
+ trees[prevZoom] = undefined;
142
+ prevZoom = z;
143
+ clusters = _clusters;
144
+ } else {
145
+ prevZoom = z;
146
+ clusters = _clusters;
147
+ trees[z] = new KDBush(clusters, getX, getY, nodeSize, Float32Array);
148
+ }
136
149
  }
137
150
 
138
151
  if (trees.length === 0) {
139
152
  return [];
140
153
  }
141
- const numbersOfClusters = trees.map((d) => d.points.length);
142
- const maxAvailZoom = numbersOfClusters.indexOf(
143
- numbersOfClusters[numbersOfClusters.length - 1],
144
- );
154
+
155
+ const numbersOfClusters = trees.map((d) => d?.points.length);
156
+ const minClusters = min(numbersOfClusters.filter((d) => d > 0));
157
+ const maxClusters = getMaxNumberOfClusters(locations, locationAccessors);
158
+
159
+ let maxAvailZoom = numbersOfClusters.indexOf(maxClusters);
160
+ if (maxClusters < locationsCount) {
161
+ maxAvailZoom++;
162
+ if (maxAvailZoom < maxZoom + 1) {
163
+ trees[maxAvailZoom] = trees[maxZoom + 1];
164
+ trees[maxZoom + 1] = undefined;
165
+ }
166
+ }
145
167
  const minAvailZoom = Math.min(
146
168
  maxAvailZoom,
147
- numbersOfClusters.lastIndexOf(numbersOfClusters[0]),
169
+ numbersOfClusters.lastIndexOf(minClusters),
148
170
  );
149
171
 
150
172
  const clusterLevels = new Array<ClusterLevel>();
151
- for (let zoom = minAvailZoom; zoom <= maxAvailZoom; zoom++) {
152
- let childrenByParent: Map<number, string[]> | undefined;
173
+ prevZoom = NaN;
174
+ for (let zoom = maxAvailZoom; zoom >= minAvailZoom; zoom--) {
175
+ let childrenByParent: Map<number, (string | number)[]> | undefined;
153
176
  const tree = trees[zoom];
177
+ if (!tree) continue;
154
178
  if (zoom < maxAvailZoom) {
155
- childrenByParent = rollup<Point<L>, string[], number>(
156
- trees[zoom + 1].points,
179
+ childrenByParent = rollup<Point<L>, (string | number)[], number>(
180
+ trees[prevZoom].points,
157
181
  (points: any[]) =>
158
182
  points.map((p: any) =>
159
183
  p.id ? makeClusterId(p.id) : getLocationId(p.location),
@@ -192,6 +216,7 @@ export function clusterLocations<L>(
192
216
  zoom,
193
217
  nodes,
194
218
  });
219
+ prevZoom = zoom;
195
220
  }
196
221
  return clusterLevels;
197
222
  }
@@ -303,3 +328,33 @@ function getX<L>(p: Point<L>) {
303
328
  function getY<L>(p: Point<L>) {
304
329
  return p.y;
305
330
  }
331
+
332
+ /**
333
+ * Finds groups of locations which share the same positions.
334
+ * They will always be clustered together at any zoom level
335
+ * which can lead to having too many zooms.
336
+ */
337
+ function getMaxNumberOfClusters<L>(
338
+ locations: Iterable<L>,
339
+ locationAccessors: LocationAccessors<L>,
340
+ ) {
341
+ const {getLocationLon, getLocationLat} = locationAccessors;
342
+ const countByLatLon = new Map<string, number>();
343
+ let numLocations = 0;
344
+ for (const loc of locations) {
345
+ const lon = getLocationLon(loc);
346
+ const lat = getLocationLat(loc);
347
+ const key = `${lon},${lat}`;
348
+ const prev = countByLatLon.get(key);
349
+ countByLatLon.set(key, prev ? prev + 1 : 1);
350
+ numLocations++;
351
+ }
352
+
353
+ let numSame = 0;
354
+ for (const [key, count] of countByLatLon) {
355
+ if (count > 1) {
356
+ numSame++;
357
+ }
358
+ }
359
+ return numLocations - numSame;
360
+ }
package/src/colors.ts CHANGED
@@ -333,17 +333,15 @@ const diffColors: DiffColors = {
333
333
  outlineColor: 'rgb(230,233,237)',
334
334
  };
335
335
 
336
- export function getFlowmapColors(
337
- settingsState: SettingsState,
338
- ): Colors | DiffColors {
336
+ export function getFlowmapColors(settings: SettingsState): Colors | DiffColors {
339
337
  return getColors(
340
338
  false, // TODO: diffMode
341
- settingsState.colorScheme,
342
- settingsState.darkMode,
343
- settingsState.fadeEnabled,
344
- settingsState.fadeOpacityEnabled,
345
- settingsState.fadeAmount,
346
- settingsState.animationEnabled,
339
+ settings.colorScheme,
340
+ settings.darkMode,
341
+ settings.fadeEnabled,
342
+ settings.fadeOpacityEnabled,
343
+ settings.fadeAmount,
344
+ settings.animationEnabled,
347
345
  );
348
346
  }
349
347
 
package/src/index.ts CHANGED
@@ -2,9 +2,12 @@ export * from './types';
2
2
  export * from './colors';
3
3
  export * from './FlowmapState';
4
4
  export * from './FlowmapSelectors';
5
+ export * from './selector-functions';
5
6
  export * from './time';
6
7
  export * from './getViewStateForLocations';
7
8
  export * from './provider/FlowmapDataProvider';
9
+ export * from './cluster/cluster';
10
+ export * from './cluster/ClusterIndex';
8
11
  export {default as FlowmapAggregateAccessors} from './FlowmapAggregateAccessors';
9
12
  export type {default as FlowmapDataProvider} from './provider/FlowmapDataProvider';
10
13
  export {default as LocalFlowmapDataProvider} from './provider/LocalFlowmapDataProvider';
@@ -23,11 +23,13 @@ export default interface FlowmapDataProvider<L, F> {
23
23
 
24
24
  getFlowByIndex(index: number): Promise<F | AggregateFlow | undefined>;
25
25
 
26
- getLocationById(id: string): Promise<L | Cluster | undefined>;
26
+ getLocationById(id: string | number): Promise<L | Cluster | undefined>;
27
27
 
28
28
  getLocationByIndex(idx: number): Promise<L | ClusterNode | undefined>;
29
29
 
30
- getTotalsForLocation(id: string): Promise<LocationTotals | undefined>;
30
+ getTotalsForLocation(
31
+ id: string | number,
32
+ ): Promise<LocationTotals | undefined>;
31
33
 
32
34
  // getLocationsInBbox(
33
35
  // bbox: [number, number, number, number],
@@ -36,6 +38,15 @@ export default interface FlowmapDataProvider<L, F> {
36
38
  // getLocationsForSearchBox(): Promise<(FlowLocation | ClusterNode)[] | undefined>;
37
39
 
38
40
  getLayersData(): Promise<LayersData | undefined>;
41
+
42
+ /**
43
+ * This is to give the data provider control over when/how often layersData
44
+ * is updated which leads to the flowmap being redrawn.
45
+ */
46
+ updateLayersData(
47
+ setLayersData: (layersData: LayersData | undefined) => void,
48
+ changeFlags: Record<string, boolean>,
49
+ ): Promise<void>;
39
50
  }
40
51
 
41
52
  export function isFlowmapData<L, F>(
@@ -72,7 +72,7 @@ export default class LocalFlowmapDataProvider<L, F>
72
72
  return this.selectors.getLayersData(this.flowmapState, this.flowmapData);
73
73
  }
74
74
 
75
- async getLocationById(id: string): Promise<L | Cluster | undefined> {
75
+ async getLocationById(id: string | number): Promise<L | Cluster | undefined> {
76
76
  if (!this.flowmapState || !this.flowmapData) {
77
77
  return undefined;
78
78
  }
@@ -93,7 +93,9 @@ export default class LocalFlowmapDataProvider<L, F>
93
93
  return locationsById?.get(id);
94
94
  }
95
95
 
96
- async getTotalsForLocation(id: string): Promise<LocationTotals | undefined> {
96
+ async getTotalsForLocation(
97
+ id: string | number,
98
+ ): Promise<LocationTotals | undefined> {
97
99
  if (!this.flowmapState || !this.flowmapData) {
98
100
  return undefined;
99
101
  }
@@ -120,4 +122,10 @@ export default class LocalFlowmapDataProvider<L, F>
120
122
  opts,
121
123
  );
122
124
  }
125
+
126
+ async updateLayersData(
127
+ setLayersData: (layersData: LayersData | undefined) => void,
128
+ ) {
129
+ setLayersData(await this.getLayersData());
130
+ }
123
131
  }
@@ -0,0 +1,34 @@
1
+ import {WebMercatorViewport} from '@math.gl/web-mercator';
2
+ import {ViewportProps} from './types';
3
+ import {scaleLinear} from 'd3-scale';
4
+
5
+ // TODO: use re-reselect
6
+
7
+ export const getViewportBoundingBox = (
8
+ viewport: ViewportProps,
9
+ maxLocationCircleSize = 0,
10
+ ): [number, number, number, number] => {
11
+ const pad = maxLocationCircleSize;
12
+ const bounds = new WebMercatorViewport({
13
+ ...viewport,
14
+ width: viewport.width + pad * 2,
15
+ height: viewport.height + pad * 2,
16
+ }).getBounds();
17
+ return [bounds[0][0], bounds[0][1], bounds[1][0], bounds[1][1]];
18
+ };
19
+
20
+ export const getFlowThicknessScale = (
21
+ magnitudeExtent: [number, number] | undefined,
22
+ ) => {
23
+ if (!magnitudeExtent) return undefined;
24
+ return scaleLinear()
25
+ .range([0.025, 0.5])
26
+ .domain([
27
+ 0,
28
+ // should support diff mode too
29
+ Math.max.apply(
30
+ null,
31
+ magnitudeExtent.map((x: number | undefined) => Math.abs(x || 0)),
32
+ ),
33
+ ]);
34
+ };
package/src/types.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type FlowmapData<L, F> = {
2
2
  locations: Iterable<L> | undefined;
3
3
  flows: Iterable<F> | undefined;
4
+ clusterLevels?: ClusterLevels;
4
5
  };
5
6
 
6
7
  export interface ViewState {
@@ -16,19 +17,19 @@ export type FlowAccessor<F, T> = (flow: F) => T; // objectInfo?: AccessorObjectI
16
17
  export type LocationAccessor<L, T> = (location: L) => T;
17
18
 
18
19
  export interface FlowAccessors<F> {
19
- getFlowOriginId: FlowAccessor<F, string>;
20
- getFlowDestId: FlowAccessor<F, string>;
20
+ getFlowOriginId: FlowAccessor<F, string | number>;
21
+ getFlowDestId: FlowAccessor<F, string | number>;
21
22
  getFlowMagnitude: FlowAccessor<F, number>;
22
23
  getFlowTime?: FlowAccessor<F, Date>; // TODO: use number instead of Date
23
24
  // getFlowColor?: FlowAccessor<string | undefined>;
24
25
  }
25
26
 
26
27
  export interface LocationAccessors<L> {
27
- getLocationId: LocationAccessor<L, string>;
28
- getLocationName?: LocationAccessor<L, string>;
28
+ getLocationId: LocationAccessor<L, string | number>;
29
+ getLocationName?: LocationAccessor<L, string | number>;
29
30
  getLocationLat: LocationAccessor<L, number>;
30
31
  getLocationLon: LocationAccessor<L, number>;
31
- getLocationClusterName?: (locationIds: string[]) => string;
32
+ getLocationClusterName?: (locationIds: (string | number)[]) => string;
32
33
  // getLocationTotalIn?: LocationAccessor<number>;
33
34
  // getLocationTotalOut?: LocationAccessor<number>;
34
35
  // getLocationTotalInternal?: LocationAccessor<number>;
@@ -59,9 +60,9 @@ export interface ViewportProps {
59
60
  height: number;
60
61
  latitude: number;
61
62
  longitude: number;
62
- zoom: number;
63
- bearing: number;
64
- pitch: number;
63
+ zoom?: number;
64
+ bearing?: number;
65
+ pitch?: number;
65
66
  altitude?: number;
66
67
  maxZoom?: number;
67
68
  minZoom?: number;
@@ -74,7 +75,7 @@ export interface ViewportProps {
74
75
  }
75
76
 
76
77
  export interface ClusterNode {
77
- id: string;
78
+ id: string | number;
78
79
  zoom: number;
79
80
  lat: number;
80
81
  lon: number;
@@ -104,8 +105,8 @@ export function isLocationClusterNode<L>(l: L | ClusterNode): l is ClusterNode {
104
105
  }
105
106
 
106
107
  export interface AggregateFlow {
107
- origin: string;
108
- dest: string;
108
+ origin: string | number;
109
+ dest: string | number;
109
110
  count: number;
110
111
  aggregate: true;
111
112
  }