@flowmap.gl/data 9.0.0 → 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/FlowmapSelectors.d.ts +4 -3
- package/dist/FlowmapSelectors.d.ts.map +1 -1
- package/dist/FlowmapSelectors.js +102 -9
- package/dist/FlowmapState.d.ts +2 -2
- package/dist/FlowmapState.d.ts.map +1 -1
- package/dist/FlowmapState.js +1 -1
- package/dist/colors.d.ts.map +1 -1
- package/dist/colors.js +5 -2
- 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/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/package.json +2 -2
- package/src/FlowmapSelectors.ts +165 -18
- package/src/FlowmapState.ts +6 -2
- package/src/colors.ts +8 -1
- package/src/selector-functions.ts +8 -0
- package/src/types.ts +5 -0
package/src/FlowmapSelectors.ts
CHANGED
|
@@ -48,11 +48,13 @@ import {
|
|
|
48
48
|
FlowAccessors,
|
|
49
49
|
FlowCirclesLayerAttributes,
|
|
50
50
|
FlowLinesLayerAttributes,
|
|
51
|
+
FlowLinesRenderingMode,
|
|
51
52
|
FlowmapData,
|
|
52
53
|
FlowmapDataAccessors,
|
|
53
54
|
LayersData,
|
|
54
55
|
LocationFilterMode,
|
|
55
56
|
LocationTotals,
|
|
57
|
+
ViewportProps,
|
|
56
58
|
isLocationClusterNode,
|
|
57
59
|
} from './types';
|
|
58
60
|
|
|
@@ -141,10 +143,15 @@ export default class FlowmapSelectors<
|
|
|
141
143
|
props: FlowmapData<L, F>,
|
|
142
144
|
) => state.settings.fadeAmount;
|
|
143
145
|
|
|
144
|
-
|
|
146
|
+
getFlowLinesRenderingMode: Selector<L, F, FlowLinesRenderingMode> = (
|
|
145
147
|
state: FlowmapState,
|
|
146
148
|
props: FlowmapData<L, F>,
|
|
147
|
-
) => state.settings.
|
|
149
|
+
) => state.settings.flowLinesRenderingMode;
|
|
150
|
+
|
|
151
|
+
getAnimate: Selector<L, F, boolean> = createSelector(
|
|
152
|
+
this.getFlowLinesRenderingMode,
|
|
153
|
+
(flowLinesRenderingMode) => flowLinesRenderingMode === 'animated-straight',
|
|
154
|
+
);
|
|
148
155
|
|
|
149
156
|
getInvalidLocationIds: Selector<L, F, (string | number)[] | undefined> =
|
|
150
157
|
createSelector(this.getLocationsFromProps, (locations) => {
|
|
@@ -1101,7 +1108,8 @@ export default class FlowmapSelectors<
|
|
|
1101
1108
|
this.getInCircleSizeGetter,
|
|
1102
1109
|
this.getOutCircleSizeGetter,
|
|
1103
1110
|
this.getFlowThicknessScale,
|
|
1104
|
-
this.
|
|
1111
|
+
this.getViewport,
|
|
1112
|
+
this.getFlowLinesRenderingMode,
|
|
1105
1113
|
this.getLocationLabelsEnabled,
|
|
1106
1114
|
(
|
|
1107
1115
|
locations,
|
|
@@ -1112,7 +1120,8 @@ export default class FlowmapSelectors<
|
|
|
1112
1120
|
getInCircleSize,
|
|
1113
1121
|
getOutCircleSize,
|
|
1114
1122
|
flowThicknessScale,
|
|
1115
|
-
|
|
1123
|
+
viewport,
|
|
1124
|
+
flowLinesRenderingMode,
|
|
1116
1125
|
locationLabelsEnabled,
|
|
1117
1126
|
) => {
|
|
1118
1127
|
return this._prepareLayersData(
|
|
@@ -1124,7 +1133,8 @@ export default class FlowmapSelectors<
|
|
|
1124
1133
|
getInCircleSize,
|
|
1125
1134
|
getOutCircleSize,
|
|
1126
1135
|
flowThicknessScale,
|
|
1127
|
-
|
|
1136
|
+
viewport,
|
|
1137
|
+
flowLinesRenderingMode,
|
|
1128
1138
|
locationLabelsEnabled,
|
|
1129
1139
|
);
|
|
1130
1140
|
},
|
|
@@ -1142,6 +1152,7 @@ export default class FlowmapSelectors<
|
|
|
1142
1152
|
const getOutCircleSize = this.getOutCircleSizeGetter(state, props);
|
|
1143
1153
|
const flowThicknessScale = this.getFlowThicknessScale(state, props);
|
|
1144
1154
|
const locationLabelsEnabled = this.getLocationLabelsEnabled(state, props);
|
|
1155
|
+
const viewport = this.getViewport(state, props);
|
|
1145
1156
|
return this._prepareLayersData(
|
|
1146
1157
|
locations,
|
|
1147
1158
|
flows,
|
|
@@ -1151,7 +1162,8 @@ export default class FlowmapSelectors<
|
|
|
1151
1162
|
getInCircleSize,
|
|
1152
1163
|
getOutCircleSize,
|
|
1153
1164
|
flowThicknessScale,
|
|
1154
|
-
|
|
1165
|
+
viewport,
|
|
1166
|
+
state.settings.flowLinesRenderingMode,
|
|
1155
1167
|
locationLabelsEnabled,
|
|
1156
1168
|
);
|
|
1157
1169
|
}
|
|
@@ -1165,7 +1177,8 @@ export default class FlowmapSelectors<
|
|
|
1165
1177
|
getInCircleSize: (locationId: string | number) => number,
|
|
1166
1178
|
getOutCircleSize: (locationId: string | number) => number,
|
|
1167
1179
|
flowThicknessScale: ScaleLinear<number, number, never> | undefined,
|
|
1168
|
-
|
|
1180
|
+
viewport: ViewportProps,
|
|
1181
|
+
flowLinesRenderingMode: FlowLinesRenderingMode,
|
|
1169
1182
|
locationLabelsEnabled: boolean,
|
|
1170
1183
|
): LayersData {
|
|
1171
1184
|
if (!locations) locations = [];
|
|
@@ -1187,7 +1200,7 @@ export default class FlowmapSelectors<
|
|
|
1187
1200
|
const flowColorScale = getFlowColorScale(
|
|
1188
1201
|
flowmapColors,
|
|
1189
1202
|
flowMagnitudeExtent,
|
|
1190
|
-
|
|
1203
|
+
flowLinesRenderingMode === 'animated-straight',
|
|
1191
1204
|
);
|
|
1192
1205
|
|
|
1193
1206
|
// Using a generator here helps to avoid creating intermediary arrays
|
|
@@ -1278,16 +1291,30 @@ export default class FlowmapSelectors<
|
|
|
1278
1291
|
})(),
|
|
1279
1292
|
);
|
|
1280
1293
|
|
|
1281
|
-
const staggeringValues =
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
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;
|
|
1291
1318
|
|
|
1292
1319
|
return {
|
|
1293
1320
|
circleAttributes: {
|
|
@@ -1310,6 +1337,9 @@ export default class FlowmapSelectors<
|
|
|
1310
1337
|
...(staggeringValues
|
|
1311
1338
|
? {getStaggering: {value: staggeringValues, size: 1}}
|
|
1312
1339
|
: {}),
|
|
1340
|
+
...(curveOffsets
|
|
1341
|
+
? {getCurveOffset: {value: curveOffsets, size: 1}}
|
|
1342
|
+
: {}),
|
|
1313
1343
|
},
|
|
1314
1344
|
},
|
|
1315
1345
|
...(locationLabelsEnabled
|
|
@@ -1502,6 +1532,7 @@ export function getFlowLineAttributesByIndex(
|
|
|
1502
1532
|
): FlowLinesLayerAttributes {
|
|
1503
1533
|
const {
|
|
1504
1534
|
getColor,
|
|
1535
|
+
getCurveOffset,
|
|
1505
1536
|
getEndpointOffsets,
|
|
1506
1537
|
getSourcePosition,
|
|
1507
1538
|
getTargetPosition,
|
|
@@ -1545,6 +1576,122 @@ export function getFlowLineAttributesByIndex(
|
|
|
1545
1576
|
},
|
|
1546
1577
|
}
|
|
1547
1578
|
: undefined),
|
|
1579
|
+
...(getCurveOffset
|
|
1580
|
+
? {
|
|
1581
|
+
getCurveOffset: {
|
|
1582
|
+
value: getCurveOffset.value.subarray(index, index + 1),
|
|
1583
|
+
size: 1,
|
|
1584
|
+
},
|
|
1585
|
+
}
|
|
1586
|
+
: undefined),
|
|
1548
1587
|
},
|
|
1549
1588
|
};
|
|
1550
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;
|
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>;
|
|
@@ -160,6 +164,7 @@ export interface FlowLinesLayerAttributes {
|
|
|
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
|
|