@flowmap.gl/data 8.0.3 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FlowmapAggregateAccessors.d.ts.map +1 -1
- package/dist/FlowmapAggregateAccessors.js +1 -1
- package/dist/FlowmapSelectors.d.ts +237 -33
- package/dist/FlowmapSelectors.d.ts.map +1 -1
- package/dist/FlowmapSelectors.js +136 -35
- package/dist/FlowmapState.d.ts +3 -3
- package/dist/FlowmapState.d.ts.map +1 -1
- package/dist/FlowmapState.js +1 -1
- package/dist/cluster/ClusterIndex.js +1 -1
- package/dist/cluster/cluster.js +9 -9
- package/dist/colors.d.ts.map +1 -1
- package/dist/colors.js +5 -2
- package/dist/getViewStateForLocations.js +1 -1
- package/dist/provider/LocalFlowmapDataProvider.js +1 -1
- package/dist/selector-functions.d.ts +1 -0
- package/dist/selector-functions.d.ts.map +1 -1
- package/dist/selector-functions.js +8 -1
- package/dist/time.js +1 -1
- package/dist/types.d.ts +5 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/util.d.ts +2 -4
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +3 -3
- package/package.json +22 -22
- package/src/FlowmapSelectors.ts +225 -67
- package/src/FlowmapState.ts +7 -3
- package/src/colors.ts +8 -1
- package/src/selector-functions.ts +8 -0
- package/src/types.ts +8 -3
- package/src/util.ts +2 -2
- package/tsconfig.json +2 -1
package/src/FlowmapSelectors.ts
CHANGED
|
@@ -7,12 +7,7 @@
|
|
|
7
7
|
import {ascending, descending, extent, min, rollup} from 'd3-array';
|
|
8
8
|
import {ScaleLinear, scaleSqrt} from 'd3-scale';
|
|
9
9
|
import KDBush from 'kdbush';
|
|
10
|
-
import {
|
|
11
|
-
ParametricSelector,
|
|
12
|
-
createSelector,
|
|
13
|
-
createSelectorCreator,
|
|
14
|
-
defaultMemoize,
|
|
15
|
-
} from 'reselect';
|
|
10
|
+
import {createSelector, createSelectorCreator, lruMemoize} from 'reselect';
|
|
16
11
|
import {alea} from 'seedrandom';
|
|
17
12
|
import FlowmapAggregateAccessors from './FlowmapAggregateAccessors';
|
|
18
13
|
import {FlowmapState} from './FlowmapState';
|
|
@@ -53,22 +48,23 @@ import {
|
|
|
53
48
|
FlowAccessors,
|
|
54
49
|
FlowCirclesLayerAttributes,
|
|
55
50
|
FlowLinesLayerAttributes,
|
|
51
|
+
FlowLinesRenderingMode,
|
|
56
52
|
FlowmapData,
|
|
57
53
|
FlowmapDataAccessors,
|
|
58
54
|
LayersData,
|
|
59
55
|
LocationFilterMode,
|
|
60
56
|
LocationTotals,
|
|
57
|
+
ViewportProps,
|
|
61
58
|
isLocationClusterNode,
|
|
62
59
|
} from './types';
|
|
63
60
|
|
|
64
61
|
const MAX_CLUSTER_ZOOM_LEVEL = 20;
|
|
65
62
|
type KDBushTree = any;
|
|
66
63
|
|
|
67
|
-
export type Selector<L, F, T> =
|
|
68
|
-
FlowmapState,
|
|
69
|
-
FlowmapData<L, F>,
|
|
70
|
-
|
|
71
|
-
>;
|
|
64
|
+
export type Selector<L, F, T> = (
|
|
65
|
+
state: FlowmapState,
|
|
66
|
+
props: FlowmapData<L, F>,
|
|
67
|
+
) => T;
|
|
72
68
|
|
|
73
69
|
export default class FlowmapSelectors<
|
|
74
70
|
L extends Record<string, any>,
|
|
@@ -147,10 +143,15 @@ export default class FlowmapSelectors<
|
|
|
147
143
|
props: FlowmapData<L, F>,
|
|
148
144
|
) => state.settings.fadeAmount;
|
|
149
145
|
|
|
150
|
-
|
|
146
|
+
getFlowLinesRenderingMode: Selector<L, F, FlowLinesRenderingMode> = (
|
|
151
147
|
state: FlowmapState,
|
|
152
148
|
props: FlowmapData<L, F>,
|
|
153
|
-
) => state.settings.
|
|
149
|
+
) => state.settings.flowLinesRenderingMode;
|
|
150
|
+
|
|
151
|
+
getAnimate: Selector<L, F, boolean> = createSelector(
|
|
152
|
+
this.getFlowLinesRenderingMode,
|
|
153
|
+
(flowLinesRenderingMode) => flowLinesRenderingMode === 'animated-straight',
|
|
154
|
+
);
|
|
154
155
|
|
|
155
156
|
getInvalidLocationIds: Selector<L, F, (string | number)[] | undefined> =
|
|
156
157
|
createSelector(this.getLocationsFromProps, (locations) => {
|
|
@@ -246,7 +247,7 @@ export default class FlowmapSelectors<
|
|
|
246
247
|
(flows, timeExtent) => {
|
|
247
248
|
if (!flows || !timeExtent) return undefined;
|
|
248
249
|
|
|
249
|
-
const minOrder = min(flows, (d) => {
|
|
250
|
+
const minOrder = min(flows, (d: F) => {
|
|
250
251
|
const t = this.accessors.getFlowTime(d);
|
|
251
252
|
return t ? getTimeGranularityForDate(t).order : null;
|
|
252
253
|
});
|
|
@@ -286,7 +287,7 @@ export default class FlowmapSelectors<
|
|
|
286
287
|
) {
|
|
287
288
|
return flows;
|
|
288
289
|
}
|
|
289
|
-
return flows.filter((flow) => {
|
|
290
|
+
return flows.filter((flow: F) => {
|
|
290
291
|
const time = this.accessors.getFlowTime(flow);
|
|
291
292
|
return time && timeRange[0] <= time && time < timeRange[1];
|
|
292
293
|
});
|
|
@@ -403,7 +404,7 @@ export default class FlowmapSelectors<
|
|
|
403
404
|
}
|
|
404
405
|
|
|
405
406
|
return clusterIndex.availableZoomLevels.filter(
|
|
406
|
-
(level) => minZoom <= level && level <= maxZoom,
|
|
407
|
+
(level: number) => minZoom <= level && level <= maxZoom,
|
|
407
408
|
);
|
|
408
409
|
},
|
|
409
410
|
);
|
|
@@ -627,7 +628,7 @@ export default class FlowmapSelectors<
|
|
|
627
628
|
? getTimeGranularityByKey(timeGranularityKey)
|
|
628
629
|
: undefined;
|
|
629
630
|
if (!flows || !timeGranularity || !timeExtent) return undefined;
|
|
630
|
-
const byTime = flows.reduce((m, flow) => {
|
|
631
|
+
const byTime = flows.reduce((m: Map<number, number>, flow: F) => {
|
|
631
632
|
if (
|
|
632
633
|
this.isFlowInSelection(
|
|
633
634
|
flow,
|
|
@@ -646,10 +647,12 @@ export default class FlowmapSelectors<
|
|
|
646
647
|
return m;
|
|
647
648
|
}, new Map<number, number>());
|
|
648
649
|
|
|
649
|
-
return Array.from(byTime.entries()).map(
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
650
|
+
return Array.from(byTime.entries()).map(
|
|
651
|
+
([millis, count]: [number, number]) => ({
|
|
652
|
+
time: new Date(millis),
|
|
653
|
+
count,
|
|
654
|
+
}),
|
|
655
|
+
);
|
|
653
656
|
},
|
|
654
657
|
);
|
|
655
658
|
|
|
@@ -766,21 +769,21 @@ export default class FlowmapSelectors<
|
|
|
766
769
|
);
|
|
767
770
|
|
|
768
771
|
getLocationIdsInViewport: Selector<L, F, Set<string | number> | undefined> =
|
|
769
|
-
createSelectorCreator(
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
772
|
+
createSelectorCreator({
|
|
773
|
+
memoize: lruMemoize,
|
|
774
|
+
memoizeOptions: {
|
|
775
|
+
equalityCheck: (
|
|
776
|
+
s1: Set<string> | undefined,
|
|
777
|
+
s2: Set<string> | undefined,
|
|
778
|
+
) => {
|
|
779
|
+
if (s1 === s2) return true;
|
|
780
|
+
if (s1 == null || s2 == null) return false;
|
|
781
|
+
if (s1.size !== s2.size) return false;
|
|
782
|
+
for (const item of s1) if (!s2.has(item)) return false;
|
|
783
|
+
return true;
|
|
784
|
+
},
|
|
782
785
|
},
|
|
783
|
-
)(
|
|
786
|
+
})(
|
|
784
787
|
this._getLocationIdsInViewport,
|
|
785
788
|
(locationIds: Set<string> | undefined) => {
|
|
786
789
|
if (!locationIds) return undefined;
|
|
@@ -793,7 +796,7 @@ export default class FlowmapSelectors<
|
|
|
793
796
|
(flows) => {
|
|
794
797
|
if (!flows) return undefined;
|
|
795
798
|
return flows.reduce(
|
|
796
|
-
(m, flow) => m + this.accessors.getFlowMagnitude(flow),
|
|
799
|
+
(m: number, flow: F) => m + this.accessors.getFlowMagnitude(flow),
|
|
797
800
|
0,
|
|
798
801
|
);
|
|
799
802
|
},
|
|
@@ -805,7 +808,7 @@ export default class FlowmapSelectors<
|
|
|
805
808
|
this.getLocationFilterMode,
|
|
806
809
|
(flows, selectedLocationSet, locationFilterMode) => {
|
|
807
810
|
if (!flows) return undefined;
|
|
808
|
-
const count = flows.reduce((m, flow) => {
|
|
811
|
+
const count = flows.reduce((m: number, flow: F | AggregateFlow) => {
|
|
809
812
|
if (
|
|
810
813
|
this.isFlowInSelection(flow, selectedLocationSet, locationFilterMode)
|
|
811
814
|
) {
|
|
@@ -1075,11 +1078,14 @@ export default class FlowmapSelectors<
|
|
|
1075
1078
|
getLocationsForFlowmapLayerById: Selector<
|
|
1076
1079
|
L,
|
|
1077
1080
|
F,
|
|
1078
|
-
Map<string, L | ClusterNode> | undefined
|
|
1081
|
+
Map<string | number, L | ClusterNode> | undefined
|
|
1079
1082
|
> = createSelector(this.getLocationsForFlowmapLayer, (locations) => {
|
|
1080
1083
|
if (!locations) return undefined;
|
|
1081
1084
|
return locations.reduce(
|
|
1082
|
-
(m, d) => (
|
|
1085
|
+
(m: Map<string | number, L | ClusterNode>, d: L | ClusterNode) => (
|
|
1086
|
+
m.set(this.accessors.getLocationId(d), d),
|
|
1087
|
+
m
|
|
1088
|
+
),
|
|
1083
1089
|
new Map(),
|
|
1084
1090
|
);
|
|
1085
1091
|
});
|
|
@@ -1102,7 +1108,8 @@ export default class FlowmapSelectors<
|
|
|
1102
1108
|
this.getInCircleSizeGetter,
|
|
1103
1109
|
this.getOutCircleSizeGetter,
|
|
1104
1110
|
this.getFlowThicknessScale,
|
|
1105
|
-
this.
|
|
1111
|
+
this.getViewport,
|
|
1112
|
+
this.getFlowLinesRenderingMode,
|
|
1106
1113
|
this.getLocationLabelsEnabled,
|
|
1107
1114
|
(
|
|
1108
1115
|
locations,
|
|
@@ -1113,7 +1120,8 @@ export default class FlowmapSelectors<
|
|
|
1113
1120
|
getInCircleSize,
|
|
1114
1121
|
getOutCircleSize,
|
|
1115
1122
|
flowThicknessScale,
|
|
1116
|
-
|
|
1123
|
+
viewport,
|
|
1124
|
+
flowLinesRenderingMode,
|
|
1117
1125
|
locationLabelsEnabled,
|
|
1118
1126
|
) => {
|
|
1119
1127
|
return this._prepareLayersData(
|
|
@@ -1125,7 +1133,8 @@ export default class FlowmapSelectors<
|
|
|
1125
1133
|
getInCircleSize,
|
|
1126
1134
|
getOutCircleSize,
|
|
1127
1135
|
flowThicknessScale,
|
|
1128
|
-
|
|
1136
|
+
viewport,
|
|
1137
|
+
flowLinesRenderingMode,
|
|
1129
1138
|
locationLabelsEnabled,
|
|
1130
1139
|
);
|
|
1131
1140
|
},
|
|
@@ -1134,13 +1143,16 @@ export default class FlowmapSelectors<
|
|
|
1134
1143
|
prepareLayersData(state: FlowmapState, props: FlowmapData<L, F>): LayersData {
|
|
1135
1144
|
const locations = this.getLocationsForFlowmapLayer(state, props) || [];
|
|
1136
1145
|
const flows = this.getFlowsForFlowmapLayer(state, props) || [];
|
|
1137
|
-
const flowmapColors =
|
|
1146
|
+
const flowmapColors = (
|
|
1147
|
+
this.getFlowmapColorsRGBA as Selector<L, F, DiffColorsRGBA | ColorsRGBA>
|
|
1148
|
+
)(state, props);
|
|
1138
1149
|
const locationsById = this.getLocationsForFlowmapLayerById(state, props);
|
|
1139
1150
|
const locationIdsInViewport = this.getLocationIdsInViewport(state, props);
|
|
1140
1151
|
const getInCircleSize = this.getInCircleSizeGetter(state, props);
|
|
1141
1152
|
const getOutCircleSize = this.getOutCircleSizeGetter(state, props);
|
|
1142
1153
|
const flowThicknessScale = this.getFlowThicknessScale(state, props);
|
|
1143
1154
|
const locationLabelsEnabled = this.getLocationLabelsEnabled(state, props);
|
|
1155
|
+
const viewport = this.getViewport(state, props);
|
|
1144
1156
|
return this._prepareLayersData(
|
|
1145
1157
|
locations,
|
|
1146
1158
|
flows,
|
|
@@ -1150,7 +1162,8 @@ export default class FlowmapSelectors<
|
|
|
1150
1162
|
getInCircleSize,
|
|
1151
1163
|
getOutCircleSize,
|
|
1152
1164
|
flowThicknessScale,
|
|
1153
|
-
|
|
1165
|
+
viewport,
|
|
1166
|
+
state.settings.flowLinesRenderingMode,
|
|
1154
1167
|
locationLabelsEnabled,
|
|
1155
1168
|
);
|
|
1156
1169
|
}
|
|
@@ -1164,7 +1177,8 @@ export default class FlowmapSelectors<
|
|
|
1164
1177
|
getInCircleSize: (locationId: string | number) => number,
|
|
1165
1178
|
getOutCircleSize: (locationId: string | number) => number,
|
|
1166
1179
|
flowThicknessScale: ScaleLinear<number, number, never> | undefined,
|
|
1167
|
-
|
|
1180
|
+
viewport: ViewportProps,
|
|
1181
|
+
flowLinesRenderingMode: FlowLinesRenderingMode,
|
|
1168
1182
|
locationLabelsEnabled: boolean,
|
|
1169
1183
|
): LayersData {
|
|
1170
1184
|
if (!locations) locations = [];
|
|
@@ -1186,15 +1200,16 @@ export default class FlowmapSelectors<
|
|
|
1186
1200
|
const flowColorScale = getFlowColorScale(
|
|
1187
1201
|
flowmapColors,
|
|
1188
1202
|
flowMagnitudeExtent,
|
|
1189
|
-
|
|
1203
|
+
flowLinesRenderingMode === 'animated-straight',
|
|
1190
1204
|
);
|
|
1191
1205
|
|
|
1192
1206
|
// Using a generator here helps to avoid creating intermediary arrays
|
|
1193
|
-
const circlePositions =
|
|
1207
|
+
const circlePositions = Float64Array.from(
|
|
1194
1208
|
(function* () {
|
|
1195
1209
|
for (const location of locations) {
|
|
1196
1210
|
yield getLocationLon(location);
|
|
1197
1211
|
yield getLocationLat(location);
|
|
1212
|
+
yield 0;
|
|
1198
1213
|
}
|
|
1199
1214
|
})(),
|
|
1200
1215
|
);
|
|
@@ -1229,21 +1244,23 @@ export default class FlowmapSelectors<
|
|
|
1229
1244
|
})(),
|
|
1230
1245
|
);
|
|
1231
1246
|
|
|
1232
|
-
const sourcePositions =
|
|
1247
|
+
const sourcePositions = Float64Array.from(
|
|
1233
1248
|
(function* () {
|
|
1234
1249
|
for (const flow of flows) {
|
|
1235
1250
|
const loc = locationsById?.get(getFlowOriginId(flow));
|
|
1236
1251
|
yield loc ? getLocationLon(loc) : 0;
|
|
1237
1252
|
yield loc ? getLocationLat(loc) : 0;
|
|
1253
|
+
yield 0;
|
|
1238
1254
|
}
|
|
1239
1255
|
})(),
|
|
1240
1256
|
);
|
|
1241
|
-
const targetPositions =
|
|
1257
|
+
const targetPositions = Float64Array.from(
|
|
1242
1258
|
(function* () {
|
|
1243
1259
|
for (const flow of flows) {
|
|
1244
1260
|
const loc = locationsById?.get(getFlowDestId(flow));
|
|
1245
1261
|
yield loc ? getLocationLon(loc) : 0;
|
|
1246
1262
|
yield loc ? getLocationLat(loc) : 0;
|
|
1263
|
+
yield 0;
|
|
1247
1264
|
}
|
|
1248
1265
|
})(),
|
|
1249
1266
|
);
|
|
@@ -1274,22 +1291,36 @@ export default class FlowmapSelectors<
|
|
|
1274
1291
|
})(),
|
|
1275
1292
|
);
|
|
1276
1293
|
|
|
1277
|
-
const staggeringValues =
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1294
|
+
const staggeringValues =
|
|
1295
|
+
flowLinesRenderingMode === 'animated-straight'
|
|
1296
|
+
? Float32Array.from(
|
|
1297
|
+
(function* () {
|
|
1298
|
+
for (const f of flows) {
|
|
1299
|
+
// @ts-ignore
|
|
1300
|
+
yield new alea(`${getFlowOriginId(f)}-${getFlowDestId(f)}`)();
|
|
1301
|
+
}
|
|
1302
|
+
})(),
|
|
1303
|
+
)
|
|
1304
|
+
: undefined;
|
|
1305
|
+
|
|
1306
|
+
const curveOffsets =
|
|
1307
|
+
flowLinesRenderingMode === 'curved'
|
|
1308
|
+
? calculateCurveOffsets(
|
|
1309
|
+
flows,
|
|
1310
|
+
viewport,
|
|
1311
|
+
locationsById,
|
|
1312
|
+
getFlowOriginId,
|
|
1313
|
+
getFlowDestId,
|
|
1314
|
+
getLocationLon,
|
|
1315
|
+
getLocationLat,
|
|
1316
|
+
)
|
|
1317
|
+
: undefined;
|
|
1287
1318
|
|
|
1288
1319
|
return {
|
|
1289
1320
|
circleAttributes: {
|
|
1290
1321
|
length: locations.length,
|
|
1291
1322
|
attributes: {
|
|
1292
|
-
getPosition: {value: circlePositions, size:
|
|
1323
|
+
getPosition: {value: circlePositions, size: 3},
|
|
1293
1324
|
getColor: {value: circleColors, size: 4},
|
|
1294
1325
|
getInRadius: {value: inCircleRadii, size: 1},
|
|
1295
1326
|
getOutRadius: {value: outCircleRadii, size: 1},
|
|
@@ -1298,14 +1329,17 @@ export default class FlowmapSelectors<
|
|
|
1298
1329
|
lineAttributes: {
|
|
1299
1330
|
length: flows.length,
|
|
1300
1331
|
attributes: {
|
|
1301
|
-
getSourcePosition: {value: sourcePositions, size:
|
|
1302
|
-
getTargetPosition: {value: targetPositions, size:
|
|
1332
|
+
getSourcePosition: {value: sourcePositions, size: 3},
|
|
1333
|
+
getTargetPosition: {value: targetPositions, size: 3},
|
|
1303
1334
|
getThickness: {value: thicknesses, size: 1},
|
|
1304
1335
|
getColor: {value: flowLineColors, size: 4},
|
|
1305
1336
|
getEndpointOffsets: {value: endpointOffsets, size: 2},
|
|
1306
1337
|
...(staggeringValues
|
|
1307
1338
|
? {getStaggering: {value: staggeringValues, size: 1}}
|
|
1308
1339
|
: {}),
|
|
1340
|
+
...(curveOffsets
|
|
1341
|
+
? {getCurveOffset: {value: curveOffsets, size: 1}}
|
|
1342
|
+
: {}),
|
|
1309
1343
|
},
|
|
1310
1344
|
},
|
|
1311
1345
|
...(locationLabelsEnabled
|
|
@@ -1488,7 +1522,8 @@ export function getLocationCoordsByIndex(
|
|
|
1488
1522
|
index: number,
|
|
1489
1523
|
): [number, number] {
|
|
1490
1524
|
const {getPosition} = circleAttributes.attributes;
|
|
1491
|
-
|
|
1525
|
+
const offset = index * getPosition.size;
|
|
1526
|
+
return [getPosition.value[offset], getPosition.value[offset + 1]];
|
|
1492
1527
|
}
|
|
1493
1528
|
|
|
1494
1529
|
export function getFlowLineAttributesByIndex(
|
|
@@ -1497,6 +1532,7 @@ export function getFlowLineAttributesByIndex(
|
|
|
1497
1532
|
): FlowLinesLayerAttributes {
|
|
1498
1533
|
const {
|
|
1499
1534
|
getColor,
|
|
1535
|
+
getCurveOffset,
|
|
1500
1536
|
getEndpointOffsets,
|
|
1501
1537
|
getSourcePosition,
|
|
1502
1538
|
getTargetPosition,
|
|
@@ -1515,12 +1551,18 @@ export function getFlowLineAttributesByIndex(
|
|
|
1515
1551
|
size: 2,
|
|
1516
1552
|
},
|
|
1517
1553
|
getSourcePosition: {
|
|
1518
|
-
value: getSourcePosition.value.subarray(
|
|
1519
|
-
|
|
1554
|
+
value: getSourcePosition.value.subarray(
|
|
1555
|
+
index * getSourcePosition.size,
|
|
1556
|
+
(index + 1) * getSourcePosition.size,
|
|
1557
|
+
),
|
|
1558
|
+
size: getSourcePosition.size,
|
|
1520
1559
|
},
|
|
1521
1560
|
getTargetPosition: {
|
|
1522
|
-
value: getTargetPosition.value.subarray(
|
|
1523
|
-
|
|
1561
|
+
value: getTargetPosition.value.subarray(
|
|
1562
|
+
index * getTargetPosition.size,
|
|
1563
|
+
(index + 1) * getTargetPosition.size,
|
|
1564
|
+
),
|
|
1565
|
+
size: getTargetPosition.size,
|
|
1524
1566
|
},
|
|
1525
1567
|
getThickness: {
|
|
1526
1568
|
value: getThickness.value.subarray(index, index + 1),
|
|
@@ -1534,6 +1576,122 @@ export function getFlowLineAttributesByIndex(
|
|
|
1534
1576
|
},
|
|
1535
1577
|
}
|
|
1536
1578
|
: undefined),
|
|
1579
|
+
...(getCurveOffset
|
|
1580
|
+
? {
|
|
1581
|
+
getCurveOffset: {
|
|
1582
|
+
value: getCurveOffset.value.subarray(index, index + 1),
|
|
1583
|
+
size: 1,
|
|
1584
|
+
},
|
|
1585
|
+
}
|
|
1586
|
+
: undefined),
|
|
1537
1587
|
},
|
|
1538
1588
|
};
|
|
1539
1589
|
}
|
|
1590
|
+
|
|
1591
|
+
type FlowLineScreenGeometry = {
|
|
1592
|
+
index: number;
|
|
1593
|
+
originId: string | number;
|
|
1594
|
+
destId: string | number;
|
|
1595
|
+
sx: number;
|
|
1596
|
+
sy: number;
|
|
1597
|
+
tx: number;
|
|
1598
|
+
ty: number;
|
|
1599
|
+
chordLengthPx: number;
|
|
1600
|
+
};
|
|
1601
|
+
|
|
1602
|
+
function calculateCurveOffsets<L, F>(
|
|
1603
|
+
flows: (F | AggregateFlow)[],
|
|
1604
|
+
viewport: ViewportProps,
|
|
1605
|
+
locationsById: Map<string | number, L | ClusterNode> | undefined,
|
|
1606
|
+
getFlowOriginId: (flow: F | AggregateFlow) => string | number,
|
|
1607
|
+
getFlowDestId: (flow: F | AggregateFlow) => string | number,
|
|
1608
|
+
getLocationLon: (location: L | ClusterNode) => number,
|
|
1609
|
+
getLocationLat: (location: L | ClusterNode) => number,
|
|
1610
|
+
): Float32Array {
|
|
1611
|
+
const curveOffsets = new Float32Array(flows.length);
|
|
1612
|
+
const corridorBuckets = new Map<string, FlowLineScreenGeometry[]>();
|
|
1613
|
+
const worldScale = 512 * Math.pow(2, viewport.zoom ?? 0);
|
|
1614
|
+
|
|
1615
|
+
flows.forEach((flow, index) => {
|
|
1616
|
+
const originId = getFlowOriginId(flow);
|
|
1617
|
+
const destId = getFlowDestId(flow);
|
|
1618
|
+
const origin = locationsById?.get(originId);
|
|
1619
|
+
const dest = locationsById?.get(destId);
|
|
1620
|
+
if (!origin || !dest) {
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
const sourceLon = getLocationLon(origin);
|
|
1625
|
+
const sourceLat = getLocationLat(origin);
|
|
1626
|
+
const targetLon = getLocationLon(dest);
|
|
1627
|
+
const targetLat = getLocationLat(dest);
|
|
1628
|
+
const sx = lngX(sourceLon) * worldScale;
|
|
1629
|
+
const sy = latY(sourceLat) * worldScale;
|
|
1630
|
+
const tx = lngX(targetLon) * worldScale;
|
|
1631
|
+
const ty = latY(targetLat) * worldScale;
|
|
1632
|
+
|
|
1633
|
+
let corridorSourceX = sx;
|
|
1634
|
+
let corridorSourceY = sy;
|
|
1635
|
+
let corridorTargetX = tx;
|
|
1636
|
+
let corridorTargetY = ty;
|
|
1637
|
+
if (
|
|
1638
|
+
corridorSourceX > corridorTargetX ||
|
|
1639
|
+
(corridorSourceX === corridorTargetX && corridorSourceY > corridorTargetY)
|
|
1640
|
+
) {
|
|
1641
|
+
[corridorSourceX, corridorTargetX] = [corridorTargetX, corridorSourceX];
|
|
1642
|
+
[corridorSourceY, corridorTargetY] = [corridorTargetY, corridorSourceY];
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
const dx = corridorTargetX - corridorSourceX;
|
|
1646
|
+
const dy = corridorTargetY - corridorSourceY;
|
|
1647
|
+
const chordLengthPx = Math.hypot(dx, dy);
|
|
1648
|
+
if (!isFinite(chordLengthPx) || chordLengthPx < 1) {
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const angle = ((Math.atan2(dy, dx) % Math.PI) + Math.PI) % Math.PI;
|
|
1653
|
+
const signedDistance =
|
|
1654
|
+
(corridorSourceX * corridorTargetY - corridorSourceY * corridorTargetX) /
|
|
1655
|
+
chordLengthPx;
|
|
1656
|
+
const key = [
|
|
1657
|
+
Math.round(angle / ((6 * Math.PI) / 180)),
|
|
1658
|
+
Math.round(signedDistance / 18),
|
|
1659
|
+
Math.round(chordLengthPx / 24),
|
|
1660
|
+
].join(':');
|
|
1661
|
+
|
|
1662
|
+
const bucket = corridorBuckets.get(key) ?? [];
|
|
1663
|
+
bucket.push({index, originId, destId, sx, sy, tx, ty, chordLengthPx});
|
|
1664
|
+
corridorBuckets.set(key, bucket);
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
corridorBuckets.forEach((bucket) => {
|
|
1668
|
+
bucket
|
|
1669
|
+
.sort((a, b) => {
|
|
1670
|
+
const originCompare = compareIds(a.originId, b.originId);
|
|
1671
|
+
if (originCompare !== 0) return originCompare;
|
|
1672
|
+
const destCompare = compareIds(a.destId, b.destId);
|
|
1673
|
+
if (destCompare !== 0) return destCompare;
|
|
1674
|
+
return a.index - b.index;
|
|
1675
|
+
})
|
|
1676
|
+
.forEach((entry, bucketIndex) => {
|
|
1677
|
+
const maxOffsetPx = Math.min(72, entry.chordLengthPx * 0.35);
|
|
1678
|
+
curveOffsets[entry.index] = Math.min(
|
|
1679
|
+
maxOffsetPx,
|
|
1680
|
+
(bucketIndex + 1) * 18,
|
|
1681
|
+
);
|
|
1682
|
+
});
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
return curveOffsets;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function compareIds(a: string | number, b: string | number): number {
|
|
1689
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
1690
|
+
return a - b;
|
|
1691
|
+
}
|
|
1692
|
+
const aString = String(a);
|
|
1693
|
+
const bString = String(b);
|
|
1694
|
+
if (aString < bString) return -1;
|
|
1695
|
+
if (aString > bString) return 1;
|
|
1696
|
+
return 0;
|
|
1697
|
+
}
|
package/src/FlowmapState.ts
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
FlowLinesRenderingMode,
|
|
9
|
+
LocationFilterMode,
|
|
10
|
+
ViewportProps,
|
|
11
|
+
} from './types';
|
|
8
12
|
|
|
9
13
|
export type FlowEndpointsInViewportMode = 'any' | 'both';
|
|
10
14
|
|
|
@@ -15,7 +19,7 @@ export interface FilterState {
|
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
export interface SettingsState {
|
|
18
|
-
|
|
22
|
+
flowLinesRenderingMode: FlowLinesRenderingMode;
|
|
19
23
|
fadeEnabled: boolean;
|
|
20
24
|
fadeOpacityEnabled: boolean;
|
|
21
25
|
locationsEnabled: boolean;
|
|
@@ -28,7 +32,7 @@ export interface SettingsState {
|
|
|
28
32
|
darkMode: boolean;
|
|
29
33
|
fadeAmount: number;
|
|
30
34
|
colorScheme: string | string[] | undefined;
|
|
31
|
-
highlightColor: string;
|
|
35
|
+
highlightColor: string | number[];
|
|
32
36
|
maxTopFlowsDisplayNum: number;
|
|
33
37
|
flowEndpointsInViewportMode: FlowEndpointsInViewportMode;
|
|
34
38
|
}
|
package/src/colors.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {scalePow, scaleSequential, scaleSequentialPow} from 'd3-scale';
|
|
|
35
35
|
import {interpolateBasis, interpolateRgbBasis} from 'd3-interpolate';
|
|
36
36
|
import {color as d3color, hcl, rgb as colorRgb} from 'd3-color';
|
|
37
37
|
import {SettingsState} from './FlowmapState';
|
|
38
|
+
import {FlowLinesRenderingMode} from './types';
|
|
38
39
|
|
|
39
40
|
const DEFAULT_OUTLINE_COLOR = '#fff';
|
|
40
41
|
const DEFAULT_DIMMED_OPACITY = 0.4;
|
|
@@ -347,10 +348,16 @@ export function getFlowmapColors(settings: SettingsState): Colors | DiffColors {
|
|
|
347
348
|
settings.fadeEnabled,
|
|
348
349
|
settings.fadeOpacityEnabled,
|
|
349
350
|
settings.fadeAmount,
|
|
350
|
-
settings.
|
|
351
|
+
isAnimatedFlowLinesMode(settings.flowLinesRenderingMode),
|
|
351
352
|
);
|
|
352
353
|
}
|
|
353
354
|
|
|
355
|
+
function isAnimatedFlowLinesMode(
|
|
356
|
+
flowLinesRenderingMode: FlowLinesRenderingMode,
|
|
357
|
+
): boolean {
|
|
358
|
+
return flowLinesRenderingMode === 'animated-straight';
|
|
359
|
+
}
|
|
360
|
+
|
|
354
361
|
export function getColors(
|
|
355
362
|
diffMode: boolean,
|
|
356
363
|
colorScheme: string | string[] | undefined,
|
|
@@ -30,6 +30,14 @@ export const getViewportBoundingBox = (
|
|
|
30
30
|
return [bounds[0][0], bounds[0][1], bounds[1][0], bounds[1][1]];
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
export const makeViewportProjector = (viewport: ViewportProps) => {
|
|
34
|
+
const mercatorViewport = new WebMercatorViewport(viewport);
|
|
35
|
+
return (coords: [number, number]): [number, number] => {
|
|
36
|
+
const [x, y] = mercatorViewport.project(coords);
|
|
37
|
+
return [x, y];
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
33
41
|
export const getFlowThicknessScale = (
|
|
34
42
|
magnitudeExtent: [number, number] | undefined,
|
|
35
43
|
) => {
|
package/src/types.ts
CHANGED
|
@@ -21,6 +21,10 @@ export interface ViewState {
|
|
|
21
21
|
|
|
22
22
|
export type FlowAccessor<F, T> = (flow: F) => T; // objectInfo?: AccessorObjectInfo,
|
|
23
23
|
export type LocationAccessor<L, T> = (location: L) => T;
|
|
24
|
+
export type FlowLinesRenderingMode =
|
|
25
|
+
| 'straight'
|
|
26
|
+
| 'animated-straight'
|
|
27
|
+
| 'curved';
|
|
24
28
|
|
|
25
29
|
export interface FlowAccessors<F> {
|
|
26
30
|
getFlowOriginId: FlowAccessor<F, string | number>;
|
|
@@ -144,7 +148,7 @@ export enum LocationFilterMode {
|
|
|
144
148
|
export interface FlowCirclesLayerAttributes {
|
|
145
149
|
length: number;
|
|
146
150
|
attributes: {
|
|
147
|
-
getPosition: LayersDataAttrValues<
|
|
151
|
+
getPosition: LayersDataAttrValues<Float64Array>;
|
|
148
152
|
getColor: LayersDataAttrValues<Uint8Array>;
|
|
149
153
|
getInRadius: LayersDataAttrValues<Float32Array>;
|
|
150
154
|
getOutRadius: LayersDataAttrValues<Float32Array>;
|
|
@@ -154,12 +158,13 @@ export interface FlowCirclesLayerAttributes {
|
|
|
154
158
|
export interface FlowLinesLayerAttributes {
|
|
155
159
|
length: number;
|
|
156
160
|
attributes: {
|
|
157
|
-
getSourcePosition: LayersDataAttrValues<
|
|
158
|
-
getTargetPosition: LayersDataAttrValues<
|
|
161
|
+
getSourcePosition: LayersDataAttrValues<Float64Array>;
|
|
162
|
+
getTargetPosition: LayersDataAttrValues<Float64Array>;
|
|
159
163
|
getThickness: LayersDataAttrValues<Float32Array>;
|
|
160
164
|
getColor: LayersDataAttrValues<Uint8Array>;
|
|
161
165
|
getEndpointOffsets: LayersDataAttrValues<Float32Array>;
|
|
162
166
|
getStaggering?: LayersDataAttrValues<Float32Array>;
|
|
167
|
+
getCurveOffset?: LayersDataAttrValues<Float32Array>;
|
|
163
168
|
};
|
|
164
169
|
}
|
|
165
170
|
|
package/src/util.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {createSelectorCreator,
|
|
7
|
+
import {createSelectorCreator, lruMemoize} from 'reselect';
|
|
8
8
|
|
|
9
|
-
export const createDebugSelector = createSelectorCreator(
|
|
9
|
+
export const createDebugSelector = createSelectorCreator(lruMemoize, {
|
|
10
10
|
equalityCheck: (previousVal: any, currentVal: any) => {
|
|
11
11
|
const rv = currentVal === previousVal;
|
|
12
12
|
if (!rv) {
|