@flowmap.gl/data 8.0.0-alpha.0 → 8.0.0-alpha.4

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.
@@ -19,7 +19,7 @@
19
19
  import {bounds} from '@mapbox/geo-viewport';
20
20
  import {ascending, descending, extent, min} from 'd3-array';
21
21
  import {nest} from 'd3-collection';
22
- import {scaleLinear, scaleSqrt} from 'd3-scale';
22
+ import {ScaleLinear, scaleLinear, scaleSqrt} from 'd3-scale';
23
23
  import KDBush from 'kdbush';
24
24
  import {
25
25
  createSelector,
@@ -36,6 +36,8 @@ import {
36
36
  makeLocationWeightGetter,
37
37
  } from './cluster/ClusterIndex';
38
38
  import getColors, {
39
+ ColorsRGBA,
40
+ DiffColorsRGBA,
39
41
  getColorsRGBA,
40
42
  getDiffColorsRGBA,
41
43
  getFlowColorScale,
@@ -66,7 +68,6 @@ import {
66
68
  LocationFilterMode,
67
69
  LocationTotals,
68
70
  } from './types';
69
- import {flatMap} from './util';
70
71
 
71
72
  const MAX_CLUSTER_ZOOM_LEVEL = 20;
72
73
  const NUMBER_OF_FLOWS_TO_DISPLAY = 5000;
@@ -124,6 +125,11 @@ export default class FlowMapSelectors<L, F> {
124
125
  props: FlowMapData<L, F>,
125
126
  ) => state.settingsState.fadeEnabled;
126
127
 
128
+ getFadeOpacityEnabled: Selector<L, F, boolean> = (
129
+ state: FlowMapState,
130
+ props: FlowMapData<L, F>,
131
+ ) => state.settingsState.fadeOpacityEnabled;
132
+
127
133
  getFadeAmount: Selector<L, F, number> = (
128
134
  state: FlowMapState,
129
135
  props: FlowMapData<L, F>,
@@ -485,6 +491,7 @@ export default class FlowMapSelectors<L, F> {
485
491
  this.getColorSchemeKey,
486
492
  this.getDarkMode,
487
493
  this.getFadeEnabled,
494
+ this.getFadeOpacityEnabled,
488
495
  this.getFadeAmount,
489
496
  this.getAnimate,
490
497
  getColors,
@@ -560,7 +567,7 @@ export default class FlowMapSelectors<L, F> {
560
567
  },
561
568
  );
562
569
 
563
- _getFlowMagnitudeExtent: Selector<L, F, [number, number] | undefined> =
570
+ getFlowMagnitudeExtent: Selector<L, F, [number, number] | undefined> =
564
571
  createSelector(
565
572
  this.getSortedAggregatedFilteredFlows,
566
573
  this.getSelectedLocationsSet,
@@ -775,10 +782,9 @@ export default class FlowMapSelectors<L, F> {
775
782
  );
776
783
 
777
784
  getLocationIdsInViewport: Selector<L, F, Set<string> | undefined> =
778
- // @ts-ignore
779
- createSelectorCreator<Set<string> | undefined>(
780
- // @ts-ignore
785
+ createSelectorCreator(
781
786
  defaultMemoize,
787
+ // @ts-ignore
782
788
  (
783
789
  s1: Set<string> | undefined,
784
790
  s2: Set<string> | undefined,
@@ -899,10 +905,10 @@ export default class FlowMapSelectors<L, F> {
899
905
  },
900
906
  );
901
907
 
902
- getFlowMagnitudeExtent(
908
+ _getFlowMagnitudeExtent = (
903
909
  state: FlowMapState,
904
910
  props: FlowMapData<L, F>,
905
- ): [number, number] | undefined {
911
+ ): [number, number] | undefined => {
906
912
  if (state.settingsState.adaptiveScalesEnabled) {
907
913
  const flows = this.getFlowsForFlowMapLayer(state, props);
908
914
  if (flows) {
@@ -912,9 +918,9 @@ export default class FlowMapSelectors<L, F> {
912
918
  return undefined;
913
919
  }
914
920
  } else {
915
- return this._getFlowMagnitudeExtent(state, props);
921
+ return this.getFlowMagnitudeExtent(state, props);
916
922
  }
917
- }
923
+ };
918
924
 
919
925
  getLocationMaxAbsTotalGetter = createSelector(
920
926
  this.getLocationTotals,
@@ -930,41 +936,46 @@ export default class FlowMapSelectors<L, F> {
930
936
  },
931
937
  );
932
938
 
933
- getFlowThicknessScale = (state: FlowMapState, props: FlowMapData<L, F>) => {
934
- const magnitudeExtent = this.getFlowMagnitudeExtent(state, props);
935
- if (!magnitudeExtent) return undefined;
936
- return scaleLinear()
937
- .range([0.025, 0.5])
938
- .domain([
939
- 0,
940
- // should support diff mode too
941
- Math.max.apply(
942
- null,
943
- magnitudeExtent.map((x: number | undefined) => Math.abs(x || 0)),
944
- ),
945
- ]);
946
- };
947
-
948
- getCircleSizeScale = (state: FlowMapState, props: FlowMapData<L, F>) => {
949
- const maxLocationCircleSize = this.getMaxLocationCircleSize(state, props);
950
- const {locationTotalsEnabled} = state.settingsState;
951
- if (!locationTotalsEnabled) {
952
- return () => maxLocationCircleSize;
953
- }
939
+ getFlowThicknessScale = createSelector(
940
+ this.getFlowMagnitudeExtent,
941
+ (magnitudeExtent) => {
942
+ if (!magnitudeExtent) return undefined;
943
+ return scaleLinear()
944
+ .range([0.025, 0.5])
945
+ .domain([
946
+ 0,
947
+ // should support diff mode too
948
+ Math.max.apply(
949
+ null,
950
+ magnitudeExtent.map((x: number | undefined) => Math.abs(x || 0)),
951
+ ),
952
+ ]);
953
+ },
954
+ );
954
955
 
955
- const locationTotalsExtent = this.getLocationTotalsExtent(state, props);
956
- if (!locationTotalsExtent) return undefined;
957
- return scaleSqrt()
958
- .range([0, maxLocationCircleSize])
959
- .domain([
960
- 0,
961
- // should support diff mode too
962
- Math.max.apply(
963
- null,
964
- locationTotalsExtent.map((x: number | undefined) => Math.abs(x || 0)),
965
- ),
966
- ]);
967
- };
956
+ getCircleSizeScale = createSelector(
957
+ this.getMaxLocationCircleSize,
958
+ this.getLocationTotalsEnabled,
959
+ this.getLocationTotalsExtent,
960
+ (maxLocationCircleSize, locationTotalsEnabled, locationTotalsExtent) => {
961
+ if (!locationTotalsEnabled) {
962
+ return () => maxLocationCircleSize;
963
+ }
964
+ if (!locationTotalsExtent) return undefined;
965
+ return scaleSqrt()
966
+ .range([0, maxLocationCircleSize])
967
+ .domain([
968
+ 0,
969
+ // should support diff mode too
970
+ Math.max.apply(
971
+ null,
972
+ locationTotalsExtent.map((x: number | undefined) =>
973
+ Math.abs(x || 0),
974
+ ),
975
+ ),
976
+ ]);
977
+ },
978
+ );
968
979
 
969
980
  getInCircleSizeGetter = createSelector(
970
981
  this.getCircleSizeScale,
@@ -1063,9 +1074,76 @@ export default class FlowMapSelectors<L, F> {
1063
1074
  );
1064
1075
  });
1065
1076
 
1077
+ getLayersData: Selector<L, F, LayersData> = createSelector(
1078
+ this.getLocationsForFlowMapLayer,
1079
+ this.getFlowsForFlowMapLayer,
1080
+ this.getFlowMapColorsRGBA,
1081
+ this.getLocationsForFlowMapLayerById,
1082
+ this.getLocationIdsInViewport,
1083
+ this.getInCircleSizeGetter,
1084
+ this.getOutCircleSizeGetter,
1085
+ this.getFlowThicknessScale,
1086
+ this.getAnimate,
1087
+ (
1088
+ locations,
1089
+ flows,
1090
+ flowMapColors,
1091
+ locationsById,
1092
+ locationIdsInViewport,
1093
+ getInCircleSize,
1094
+ getOutCircleSize,
1095
+ flowThicknessScale,
1096
+ animationEnabled,
1097
+ ) => {
1098
+ return this._prepareLayersData(
1099
+ locations,
1100
+ flows,
1101
+ flowMapColors,
1102
+ locationsById,
1103
+ locationIdsInViewport,
1104
+ getInCircleSize,
1105
+ getOutCircleSize,
1106
+ flowThicknessScale,
1107
+ animationEnabled,
1108
+ );
1109
+ },
1110
+ );
1111
+
1066
1112
  prepareLayersData(state: FlowMapState, props: FlowMapData<L, F>): LayersData {
1067
1113
  const locations = this.getLocationsForFlowMapLayer(state, props) || [];
1068
1114
  const flows = this.getFlowsForFlowMapLayer(state, props) || [];
1115
+ const flowMapColors = this.getFlowMapColorsRGBA(state, props);
1116
+ const locationsById = this.getLocationsForFlowMapLayerById(state, props);
1117
+ const locationIdsInViewport = this.getLocationIdsInViewport(state, props);
1118
+ const getInCircleSize = this.getInCircleSizeGetter(state, props);
1119
+ const getOutCircleSize = this.getOutCircleSizeGetter(state, props);
1120
+ const flowThicknessScale = this.getFlowThicknessScale(state, props);
1121
+ return this._prepareLayersData(
1122
+ locations,
1123
+ flows,
1124
+ flowMapColors,
1125
+ locationsById,
1126
+ locationIdsInViewport,
1127
+ getInCircleSize,
1128
+ getOutCircleSize,
1129
+ flowThicknessScale,
1130
+ state.settingsState.animationEnabled,
1131
+ );
1132
+ }
1133
+
1134
+ _prepareLayersData(
1135
+ locations: (L | ClusterNode)[] | undefined,
1136
+ flows: (F | AggregateFlow)[] | undefined,
1137
+ flowMapColors: DiffColorsRGBA | ColorsRGBA,
1138
+ locationsById: Map<string, L | ClusterNode> | undefined,
1139
+ locationIdsInViewport: Set<string> | undefined,
1140
+ getInCircleSize: (locationId: string) => number,
1141
+ getOutCircleSize: (locationId: string) => number,
1142
+ flowThicknessScale: ScaleLinear<number, number, never> | undefined,
1143
+ animationEnabled: boolean,
1144
+ ): LayersData {
1145
+ if (!locations) locations = [];
1146
+ if (!flows) flows = [];
1069
1147
  const {
1070
1148
  getFlowOriginId,
1071
1149
  getFlowDestId,
@@ -1074,21 +1152,11 @@ export default class FlowMapSelectors<L, F> {
1074
1152
  getLocationCentroid,
1075
1153
  } = this.accessors;
1076
1154
 
1077
- const flowMapColors = this.getFlowMapColorsRGBA(state, props);
1078
- const {settingsState} = state;
1079
-
1080
- const locationsById = this.getLocationsForFlowMapLayerById(state, props);
1081
1155
  const getCentroid = (id: string) => {
1082
1156
  const loc = locationsById?.get(id);
1083
1157
  return loc ? getLocationCentroid(loc) : [0, 0];
1084
1158
  };
1085
1159
 
1086
- const locationIdsInViewport = this.getLocationIdsInViewport(state, props);
1087
- const getInCircleSize = this.getInCircleSizeGetter(state, props);
1088
- const getOutCircleSize = this.getOutCircleSizeGetter(state, props);
1089
-
1090
- const flowThicknessScale = this.getFlowThicknessScale(state, props);
1091
-
1092
1160
  const flowMagnitudeExtent = extent(flows, (f) => getFlowMagnitude(f)) as [
1093
1161
  number,
1094
1162
  number,
@@ -1099,8 +1167,14 @@ export default class FlowMapSelectors<L, F> {
1099
1167
  false,
1100
1168
  );
1101
1169
 
1102
- const circlePositions = new Float32Array(
1103
- flatMap(locations, getLocationCentroid),
1170
+ // Using yield here helps to avoid creating intermediary arrays
1171
+ const circlePositions = Float32Array.from(
1172
+ (function* () {
1173
+ for (const location of locations) {
1174
+ // yield* effectively does same as flatMap here
1175
+ yield* getLocationCentroid(location);
1176
+ }
1177
+ })(),
1104
1178
  );
1105
1179
 
1106
1180
  // TODO: diff mode
@@ -1108,53 +1182,80 @@ export default class FlowMapSelectors<L, F> {
1108
1182
  ? flowMapColors.positive.locationCircles.inner
1109
1183
  : flowMapColors.locationCircles.inner;
1110
1184
 
1111
- const circleColors = new Uint8Array(flatMap(locations, (d) => circleColor));
1112
- const inCircleRadii = new Float32Array(
1113
- locations.map((loc) => {
1114
- const id = getLocationId(loc);
1115
- return locationIdsInViewport?.has(id) ? getInCircleSize(id) : 1.0;
1116
- }),
1185
+ const circleColors = Uint8Array.from(
1186
+ (function* () {
1187
+ for (const location of locations) {
1188
+ yield* circleColor;
1189
+ }
1190
+ })(),
1191
+ );
1192
+
1193
+ const inCircleRadii = Float32Array.from(
1194
+ (function* () {
1195
+ for (const location of locations) {
1196
+ const id = getLocationId(location);
1197
+ yield locationIdsInViewport?.has(id) ? getInCircleSize(id) : 1.0;
1198
+ }
1199
+ })(),
1117
1200
  );
1118
- const outCircleRadii = new Float32Array(
1119
- locations.map((loc) => {
1120
- const id = getLocationId(loc);
1121
- return locationIdsInViewport?.has(id) ? getOutCircleSize(id) : 1.0;
1122
- }),
1201
+ const outCircleRadii = Float32Array.from(
1202
+ (function* () {
1203
+ for (const location of locations) {
1204
+ const id = getLocationId(location);
1205
+ yield locationIdsInViewport?.has(id) ? getOutCircleSize(id) : 1.0;
1206
+ }
1207
+ })(),
1123
1208
  );
1124
1209
 
1125
- const sourcePositions = new Float32Array(
1126
- flatMap(flows, (d: F | AggregateFlow) => getCentroid(getFlowOriginId(d))),
1210
+ const sourcePositions = Float32Array.from(
1211
+ (function* () {
1212
+ for (const flow of flows) {
1213
+ yield* getCentroid(getFlowOriginId(flow));
1214
+ }
1215
+ })(),
1127
1216
  );
1128
- const targetPositions = new Float32Array(
1129
- flatMap(flows, (d: F | AggregateFlow) => getCentroid(getFlowDestId(d))),
1217
+ const targetPositions = Float32Array.from(
1218
+ (function* () {
1219
+ for (const flow of flows) {
1220
+ yield* getCentroid(getFlowDestId(flow));
1221
+ }
1222
+ })(),
1130
1223
  );
1131
- const thicknesses = new Float32Array(
1132
- flows.map((d: F | AggregateFlow) =>
1133
- flowThicknessScale ? flowThicknessScale(getFlowMagnitude(d)) || 0 : 0,
1134
- ),
1224
+ const thicknesses = Float32Array.from(
1225
+ (function* () {
1226
+ for (const flow of flows) {
1227
+ yield flowThicknessScale
1228
+ ? flowThicknessScale(getFlowMagnitude(flow)) || 0
1229
+ : 0;
1230
+ }
1231
+ })(),
1135
1232
  );
1136
- const endpointOffsets = new Float32Array(
1137
- flatMap(flows, (d: F | AggregateFlow) => {
1138
- const originId = getFlowOriginId(d);
1139
- const destId = getFlowDestId(d);
1140
- return [
1141
- Math.max(getInCircleSize(originId), getOutCircleSize(originId)),
1142
- Math.max(getInCircleSize(destId), getOutCircleSize(destId)),
1143
- ];
1144
- }),
1233
+ const endpointOffsets = Float32Array.from(
1234
+ (function* () {
1235
+ for (const flow of flows) {
1236
+ const originId = getFlowOriginId(flow);
1237
+ const destId = getFlowDestId(flow);
1238
+ yield Math.max(getInCircleSize(originId), getOutCircleSize(originId));
1239
+ yield Math.max(getInCircleSize(destId), getOutCircleSize(destId));
1240
+ }
1241
+ })(),
1145
1242
  );
1146
- const flowLineColors = new Uint8Array(
1147
- flatMap(flows, (f: F | AggregateFlow) =>
1148
- flowColorScale(getFlowMagnitude(f)),
1149
- ),
1243
+ const flowLineColors = Uint8Array.from(
1244
+ (function* () {
1245
+ for (const flow of flows) {
1246
+ yield* flowColorScale(getFlowMagnitude(flow));
1247
+ }
1248
+ })(),
1150
1249
  );
1151
1250
 
1152
- const staggeringValues = settingsState.animationEnabled
1153
- ? new Float32Array(
1154
- flows.map((f: F | AggregateFlow) =>
1155
- // @ts-ignore
1156
- new alea(`${getFlowOriginId(f)}-${getFlowDestId(f)}`)(),
1157
- ),
1251
+ const staggeringValues = animationEnabled
1252
+ ? Float32Array.from(
1253
+ (function* () {
1254
+ for (const flow of flows) {
1255
+ // @ts-ignore
1256
+ yield new alea(`${getFlowOriginId(f)}-${getFlowDestId(f)}`)();
1257
+ }
1258
+ })(),
1158
1259
  )
1159
1260
  : undefined;
1160
1261
 
@@ -9,6 +9,7 @@ export interface FilterState {
9
9
  export interface SettingsState {
10
10
  animationEnabled: boolean;
11
11
  fadeEnabled: boolean;
12
+ fadeOpacityEnabled: boolean;
12
13
  locationTotalsEnabled: boolean;
13
14
  adaptiveScalesEnabled: boolean;
14
15
  clusteringEnabled: boolean;
package/src/colors.ts CHANGED
@@ -26,8 +26,8 @@ import {
26
26
  } from 'd3-scale-chromatic';
27
27
  import {range} from 'd3-array';
28
28
  import {scalePow, scaleSequential, scaleSequentialPow} from 'd3-scale';
29
- import {interpolateRgbBasis} from 'd3-interpolate';
30
- import {color as d3color, hcl} from 'd3-color';
29
+ import {interpolateBasis, interpolateRgbBasis} from 'd3-interpolate';
30
+ import {color as d3color, hcl, rgb as colorRgb} from 'd3-color';
31
31
  import {SettingsState} from './FlowMapState';
32
32
 
33
33
  const DEFAULT_OUTLINE_COLOR = '#fff';
@@ -338,6 +338,7 @@ export function getFlowMapColors(
338
338
  settingsState.colorScheme,
339
339
  settingsState.darkMode,
340
340
  settingsState.fadeEnabled,
341
+ settingsState.fadeOpacityEnabled,
341
342
  settingsState.fadeAmount,
342
343
  settingsState.animationEnabled,
343
344
  );
@@ -348,6 +349,7 @@ export function getColors(
348
349
  schemeKey: string | undefined,
349
350
  darkMode: boolean,
350
351
  fadeEnabled: boolean,
352
+ fadeOpacityEnabled: boolean,
351
353
  fadeAmount: number,
352
354
  animate: boolean,
353
355
  ): Colors | DiffColors {
@@ -390,13 +392,14 @@ export function getColors(
390
392
  scheme = indices.map(
391
393
  (c, i) => {
392
394
  const color = colorScale(i);
393
- const alpha = amount(i);
394
- if (color == null || alpha == null) return '#000';
395
+ const a = amount(i);
396
+ if (color == null || a == null) return '#000';
395
397
  const col = hcl(color);
396
- col.l = darkMode
397
- ? col.l - col.l * alpha
398
- : col.l + (100 - col.l) * alpha;
399
- col.c = col.c - col.c * (alpha / 4);
398
+ col.l = darkMode ? col.l - col.l * a : col.l + (100 - col.l) * a;
399
+ col.c = col.c - col.c * (a / 4);
400
+ if (fadeOpacityEnabled) {
401
+ col.opacity = col.opacity * (1.0 - a);
402
+ }
400
403
  return col.toString();
401
404
  },
402
405
  // interpolateRgbBasis([colorScale(i), darkMode ? '#000' : '#fff'])(amount(i))
@@ -417,15 +420,46 @@ export function getColors(
417
420
  };
418
421
  }
419
422
 
423
+ function interpolateRgbaBasis(colors: string[]) {
424
+ const spline = interpolateBasis;
425
+ const n = colors.length;
426
+ let r: any = new Array(n),
427
+ g: any = new Array(n),
428
+ b: any = new Array(n),
429
+ opacity: any = new Array(n),
430
+ i,
431
+ color: any;
432
+ for (i = 0; i < n; ++i) {
433
+ color = colorRgb(colors[i]);
434
+ r[i] = color.r || 0;
435
+ g[i] = color.g || 0;
436
+ b[i] = color.b || 0;
437
+ opacity[i] = color.opacity || 0;
438
+ }
439
+ r = spline(r);
440
+ g = spline(g);
441
+ b = spline(b);
442
+ opacity = spline(opacity);
443
+ // color.opacity = 1;
444
+ return function (t: number) {
445
+ color.r = r(t);
446
+ color.g = g(t);
447
+ color.b = b(t);
448
+ color.opacity = opacity(t);
449
+ return color + '';
450
+ };
451
+ }
452
+
420
453
  export function createFlowColorScale(
421
454
  domain: [number, number],
422
455
  scheme: string[],
423
456
  animate: boolean | undefined,
424
457
  ): ColorScale {
425
- const scale = scaleSequentialPow(interpolateRgbBasis(scheme))
458
+ const scale = scaleSequentialPow(interpolateRgbaBasis(scheme))
426
459
  // @ts-ignore
427
460
  .exponent(animate ? 1 / 2 : 1 / 3)
428
461
  .domain(domain);
462
+
429
463
  return (value: number) => colorAsRgba(scale(value));
430
464
  }
431
465
 
@@ -66,10 +66,7 @@ export default class LocalFlowMapDataProvider<L, F>
66
66
  if (!this.flowMapState || !this.flowMapData) {
67
67
  return undefined;
68
68
  }
69
- return this.selectors.prepareLayersData(
70
- this.flowMapState,
71
- this.flowMapData,
72
- );
69
+ return this.selectors.getLayersData(this.flowMapState, this.flowMapData);
73
70
  }
74
71
 
75
72
  async getLocationById(id: string): Promise<L | Cluster | undefined> {
package/src/util.ts CHANGED
@@ -1,3 +1,11 @@
1
- export function flatMap<S, T>(xs: S[], f: (item: S) => T | T[]): T[] {
2
- return xs.reduce((acc: T[], x: S) => acc.concat(f(x)), []);
3
- }
1
+ import {createSelectorCreator, defaultMemoize} from 'reselect';
2
+
3
+ export const createDebugSelector = createSelectorCreator(defaultMemoize, {
4
+ equalityCheck: (previousVal: any, currentVal: any) => {
5
+ const rv = currentVal === previousVal;
6
+ if (!rv) {
7
+ console.log('Selector param value changed', currentVal);
8
+ }
9
+ return rv;
10
+ },
11
+ });