@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.
@@ -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
- getAnimate: Selector<L, F, boolean> = (
146
+ getFlowLinesRenderingMode: Selector<L, F, FlowLinesRenderingMode> = (
145
147
  state: FlowmapState,
146
148
  props: FlowmapData<L, F>,
147
- ) => state.settings.animationEnabled;
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.getAnimate,
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
- animationEnabled,
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
- animationEnabled,
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
- state.settings.animationEnabled,
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
- animationEnabled: boolean,
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
- false,
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 = animationEnabled
1282
- ? Float32Array.from(
1283
- (function* () {
1284
- for (const f of flows) {
1285
- // @ts-ignore
1286
- yield new alea(`${getFlowOriginId(f)}-${getFlowDestId(f)}`)();
1287
- }
1288
- })(),
1289
- )
1290
- : undefined;
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
+ }
@@ -4,7 +4,11 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import {LocationFilterMode, ViewportProps} from './types';
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
- animationEnabled: boolean;
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.animationEnabled,
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