@flowmap.gl/data 8.0.0-alpha.2 → 8.0.0-alpha.22
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 +16 -0
- package/dist/FlowmapAggregateAccessors.d.ts.map +1 -0
- package/dist/FlowmapAggregateAccessors.js +48 -0
- package/dist/FlowmapSelectors.d.ts +213 -0
- package/dist/FlowmapSelectors.d.ts.map +1 -0
- package/dist/FlowmapSelectors.js +861 -0
- package/dist/{FlowMapState.d.ts → FlowmapState.d.ts} +12 -8
- package/dist/FlowmapState.d.ts.map +1 -0
- package/dist/FlowmapState.js +2 -0
- package/dist/cluster/ClusterIndex.d.ts +3 -3
- package/dist/cluster/ClusterIndex.d.ts.map +1 -1
- package/dist/cluster/ClusterIndex.js +1 -1
- package/dist/cluster/cluster.d.ts +6 -5
- package/dist/cluster/cluster.d.ts.map +1 -1
- package/dist/cluster/cluster.js +76 -20
- package/dist/colors.d.ts +7 -7
- package/dist/colors.d.ts.map +1 -1
- package/dist/colors.js +55 -20
- package/dist/getViewStateForLocations.d.ts +18 -11
- package/dist/getViewStateForLocations.d.ts.map +1 -1
- package/dist/getViewStateForLocations.js +37 -23
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -6
- package/dist/provider/FlowmapDataProvider.d.ts +21 -0
- package/dist/provider/FlowmapDataProvider.d.ts.map +1 -0
- package/dist/provider/FlowmapDataProvider.js +17 -0
- package/dist/provider/LocalFlowmapDataProvider.d.ts +25 -0
- package/dist/provider/LocalFlowmapDataProvider.d.ts.map +1 -0
- package/dist/provider/LocalFlowmapDataProvider.js +111 -0
- package/dist/selector-functions.d.ts +10 -0
- package/dist/selector-functions.d.ts.map +1 -0
- package/dist/selector-functions.js +56 -0
- package/dist/types.d.ts +20 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -4
- package/dist/util.d.ts +0 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +1 -4
- package/package.json +10 -12
- package/src/FlowmapAggregateAccessors.ts +67 -0
- package/src/{FlowMapSelectors.ts → FlowmapSelectors.ts} +453 -398
- package/src/{FlowMapState.ts → FlowmapState.ts} +11 -7
- package/src/cluster/ClusterIndex.ts +19 -12
- package/src/cluster/cluster.ts +96 -35
- package/src/colors.ts +70 -28
- package/src/getViewStateForLocations.ts +56 -40
- package/src/index.ts +9 -6
- package/src/provider/FlowmapDataProvider.ts +75 -0
- package/src/provider/LocalFlowmapDataProvider.ts +143 -0
- package/src/selector-functions.ts +87 -0
- package/src/types.ts +23 -19
- package/src/util.ts +0 -4
- package/dist/FlowMapAggregateAccessors.d.ts +0 -15
- package/dist/FlowMapAggregateAccessors.d.ts.map +0 -1
- package/dist/FlowMapAggregateAccessors.js +0 -43
- package/dist/FlowMapSelectors.d.ts +0 -182
- package/dist/FlowMapSelectors.d.ts.map +0 -1
- package/dist/FlowMapSelectors.js +0 -834
- package/dist/FlowMapState.d.ts.map +0 -1
- package/dist/FlowMapState.js +0 -2
- package/dist/provider/FlowMapDataProvider.d.ts +0 -16
- package/dist/provider/FlowMapDataProvider.d.ts.map +0 -1
- package/dist/provider/FlowMapDataProvider.js +0 -17
- package/dist/provider/LocalFlowMapDataProvider.d.ts +0 -20
- package/dist/provider/LocalFlowMapDataProvider.d.ts.map +0 -1
- package/dist/provider/LocalFlowMapDataProvider.js +0 -87
- package/src/FlowMapAggregateAccessors.ts +0 -60
- package/src/provider/FlowMapDataProvider.ts +0 -63
- package/src/provider/LocalFlowMapDataProvider.ts +0 -105
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
import {LocationFilterMode, ViewportProps} from './types';
|
|
2
2
|
|
|
3
3
|
export interface FilterState {
|
|
4
|
-
selectedLocations
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
selectedLocations?: string[];
|
|
5
|
+
locationFilterMode?: LocationFilterMode;
|
|
6
|
+
selectedTimeRange?: [Date, Date];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface SettingsState {
|
|
10
10
|
animationEnabled: boolean;
|
|
11
11
|
fadeEnabled: boolean;
|
|
12
|
+
fadeOpacityEnabled: boolean;
|
|
12
13
|
locationTotalsEnabled: boolean;
|
|
14
|
+
locationLabelsEnabled: boolean;
|
|
13
15
|
adaptiveScalesEnabled: boolean;
|
|
14
16
|
clusteringEnabled: boolean;
|
|
15
17
|
clusteringAuto: boolean;
|
|
16
18
|
clusteringLevel?: number;
|
|
17
19
|
darkMode: boolean;
|
|
18
20
|
fadeAmount: number;
|
|
19
|
-
colorScheme: string | undefined;
|
|
21
|
+
colorScheme: string | string[] | undefined;
|
|
22
|
+
highlightColor: string;
|
|
23
|
+
maxTopFlowsDisplayNum: number;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
export interface
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
export interface FlowmapState {
|
|
27
|
+
filter?: FilterState;
|
|
28
|
+
settings: SettingsState;
|
|
25
29
|
viewport: ViewportProps;
|
|
26
30
|
}
|
|
@@ -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: (
|
|
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<
|
|
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) =>
|
|
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),
|
package/src/cluster/cluster.ts
CHANGED
|
@@ -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';
|
|
@@ -69,8 +69,9 @@ interface BasePoint {
|
|
|
69
69
|
parentId: number; // parent cluster id
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
interface LeafPoint extends BasePoint {
|
|
72
|
+
interface LeafPoint<L> extends BasePoint {
|
|
73
73
|
index: number; // index of the source feature in the original input array,
|
|
74
|
+
location: L;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
interface ClusterPoint extends BasePoint {
|
|
@@ -78,14 +79,14 @@ interface ClusterPoint extends BasePoint {
|
|
|
78
79
|
numPoints: number;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
type Point = LeafPoint | ClusterPoint;
|
|
82
|
+
type Point<L> = LeafPoint<L> | ClusterPoint;
|
|
82
83
|
|
|
83
|
-
export function isLeafPoint(p: Point): p is LeafPoint {
|
|
84
|
-
const {index} = p as LeafPoint
|
|
84
|
+
export function isLeafPoint<L>(p: Point<L>): p is LeafPoint<L> {
|
|
85
|
+
const {index} = p as LeafPoint<L>;
|
|
85
86
|
return index != null;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
export function isClusterPoint(p: Point): p is ClusterPoint {
|
|
89
|
+
export function isClusterPoint<L>(p: Point<L>): p is ClusterPoint {
|
|
89
90
|
const {id} = p as ClusterPoint;
|
|
90
91
|
return id != null;
|
|
91
92
|
}
|
|
@@ -93,12 +94,12 @@ export function isClusterPoint(p: Point): p is ClusterPoint {
|
|
|
93
94
|
type ZoomLevelKDBush = any;
|
|
94
95
|
|
|
95
96
|
export function clusterLocations<L>(
|
|
96
|
-
locations: L
|
|
97
|
+
locations: Iterable<L>,
|
|
97
98
|
locationAccessors: LocationAccessors<L>,
|
|
98
99
|
getLocationWeight: LocationWeightGetter,
|
|
99
100
|
options?: Partial<Options>,
|
|
100
101
|
): ClusterLevel[] {
|
|
101
|
-
const {
|
|
102
|
+
const {getLocationLon, getLocationLat, getLocationId} = locationAccessors;
|
|
102
103
|
const opts = {
|
|
103
104
|
...defaultOptions,
|
|
104
105
|
...options,
|
|
@@ -108,50 +109,78 @@ export function clusterLocations<L>(
|
|
|
108
109
|
const trees = new Array<ZoomLevelKDBush>(maxZoom + 1);
|
|
109
110
|
|
|
110
111
|
// generate a cluster object for each point and index input points into a KD-tree
|
|
111
|
-
let clusters = new Array<Point
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
let clusters = new Array<Point<L>>();
|
|
113
|
+
let locationsCount = 0;
|
|
114
|
+
for (const location of locations) {
|
|
115
|
+
const x = getLocationLon(location);
|
|
116
|
+
const y = getLocationLat(location);
|
|
114
117
|
clusters.push({
|
|
115
118
|
x: lngX(x), // projected point coordinates
|
|
116
119
|
y: latY(y),
|
|
117
|
-
weight: getLocationWeight(getLocationId(
|
|
120
|
+
weight: getLocationWeight(getLocationId(location)),
|
|
118
121
|
zoom: Infinity, // the last zoom the point was processed at
|
|
119
|
-
index:
|
|
122
|
+
index: locationsCount, // index of the source feature in the original input array,
|
|
120
123
|
parentId: -1, // parent cluster id
|
|
124
|
+
location,
|
|
121
125
|
});
|
|
126
|
+
locationsCount++;
|
|
122
127
|
}
|
|
123
|
-
trees[maxZoom + 1] = new KDBush(clusters, getX, getY, nodeSize, Float32Array);
|
|
124
128
|
|
|
125
129
|
// cluster points on max zoom, then cluster the results on previous zoom, etc.;
|
|
126
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
|
+
|
|
127
134
|
for (let z = maxZoom; z >= minZoom; z--) {
|
|
128
135
|
// create a new set of clusters for the zoom and index them with a KD-tree
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|
|
131
149
|
}
|
|
132
150
|
|
|
133
151
|
if (trees.length === 0) {
|
|
134
152
|
return [];
|
|
135
153
|
}
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
);
|
|
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
|
+
}
|
|
140
167
|
const minAvailZoom = Math.min(
|
|
141
168
|
maxAvailZoom,
|
|
142
|
-
numbersOfClusters.lastIndexOf(
|
|
169
|
+
numbersOfClusters.lastIndexOf(minClusters),
|
|
143
170
|
);
|
|
144
171
|
|
|
145
172
|
const clusterLevels = new Array<ClusterLevel>();
|
|
146
|
-
|
|
147
|
-
|
|
173
|
+
prevZoom = NaN;
|
|
174
|
+
for (let zoom = maxAvailZoom; zoom >= minAvailZoom; zoom--) {
|
|
175
|
+
let childrenByParent: Map<number, (string | number)[]> | undefined;
|
|
148
176
|
const tree = trees[zoom];
|
|
177
|
+
if (!tree) continue;
|
|
149
178
|
if (zoom < maxAvailZoom) {
|
|
150
|
-
childrenByParent = rollup<Point
|
|
151
|
-
trees[
|
|
179
|
+
childrenByParent = rollup<Point<L>, (string | number)[], number>(
|
|
180
|
+
trees[prevZoom].points,
|
|
152
181
|
(points: any[]) =>
|
|
153
182
|
points.map((p: any) =>
|
|
154
|
-
p.id ? makeClusterId(p.id) : getLocationId(
|
|
183
|
+
p.id ? makeClusterId(p.id) : getLocationId(p.location),
|
|
155
184
|
),
|
|
156
185
|
(point: any) => point.parentId,
|
|
157
186
|
);
|
|
@@ -159,13 +188,13 @@ export function clusterLocations<L>(
|
|
|
159
188
|
|
|
160
189
|
const nodes: ClusterNode[] = [];
|
|
161
190
|
for (const point of tree.points) {
|
|
162
|
-
const {x, y, numPoints} = point;
|
|
191
|
+
const {x, y, numPoints, location} = point;
|
|
163
192
|
if (isLeafPoint(point)) {
|
|
164
|
-
const location = locations[point.index];
|
|
165
193
|
nodes.push({
|
|
166
194
|
id: getLocationId(location),
|
|
167
195
|
zoom,
|
|
168
|
-
|
|
196
|
+
lat: getLocationLat(location),
|
|
197
|
+
lon: getLocationLon(location),
|
|
169
198
|
});
|
|
170
199
|
} else if (isClusterPoint(point)) {
|
|
171
200
|
const {id} = point;
|
|
@@ -177,7 +206,8 @@ export function clusterLocations<L>(
|
|
|
177
206
|
id: makeClusterId(id),
|
|
178
207
|
name: makeClusterName(id, numPoints),
|
|
179
208
|
zoom,
|
|
180
|
-
|
|
209
|
+
lat: yLat(y),
|
|
210
|
+
lon: xLng(x),
|
|
181
211
|
children,
|
|
182
212
|
} as Cluster);
|
|
183
213
|
}
|
|
@@ -186,6 +216,7 @@ export function clusterLocations<L>(
|
|
|
186
216
|
zoom,
|
|
187
217
|
nodes,
|
|
188
218
|
});
|
|
219
|
+
prevZoom = zoom;
|
|
189
220
|
}
|
|
190
221
|
return clusterLevels;
|
|
191
222
|
}
|
|
@@ -208,13 +239,13 @@ function createCluster(
|
|
|
208
239
|
};
|
|
209
240
|
}
|
|
210
241
|
|
|
211
|
-
function cluster(
|
|
212
|
-
points: Point[],
|
|
242
|
+
function cluster<L>(
|
|
243
|
+
points: Point<L>[],
|
|
213
244
|
zoom: number,
|
|
214
245
|
tree: ZoomLevelKDBush,
|
|
215
246
|
options: Options,
|
|
216
247
|
) {
|
|
217
|
-
const clusters: Point[] = [];
|
|
248
|
+
const clusters: Point<L>[] = [];
|
|
218
249
|
const {radius, extent} = options;
|
|
219
250
|
const r = radius / (extent * Math.pow(2, zoom));
|
|
220
251
|
|
|
@@ -290,10 +321,40 @@ function latY(lat: number) {
|
|
|
290
321
|
return y < 0 ? 0 : y > 1 ? 1 : y;
|
|
291
322
|
}
|
|
292
323
|
|
|
293
|
-
function getX(p: Point) {
|
|
324
|
+
function getX<L>(p: Point<L>) {
|
|
294
325
|
return p.x;
|
|
295
326
|
}
|
|
296
327
|
|
|
297
|
-
function getY(p: Point) {
|
|
328
|
+
function getY<L>(p: Point<L>) {
|
|
298
329
|
return p.y;
|
|
299
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
|
@@ -26,9 +26,9 @@ 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';
|
|
31
|
-
import {SettingsState} from './
|
|
29
|
+
import {interpolateBasis, interpolateRgbBasis} from 'd3-interpolate';
|
|
30
|
+
import {color as d3color, hcl, rgb as colorRgb} from 'd3-color';
|
|
31
|
+
import {SettingsState} from './FlowmapState';
|
|
32
32
|
|
|
33
33
|
const DEFAULT_OUTLINE_COLOR = '#fff';
|
|
34
34
|
const DEFAULT_DIMMED_OPACITY = 0.4;
|
|
@@ -58,7 +58,10 @@ export function opacifyHex(hexCode: string, opacity: number): string {
|
|
|
58
58
|
return `rgba(${col.r}, ${col.g}, ${col.b}, ${opacity})`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export function colorAsRgba(color: string): RGBA {
|
|
61
|
+
export function colorAsRgba(color: string | number[]): RGBA {
|
|
62
|
+
if (Array.isArray(color)) {
|
|
63
|
+
return color as RGBA;
|
|
64
|
+
}
|
|
62
65
|
const col = d3color(color);
|
|
63
66
|
if (!col) {
|
|
64
67
|
console.warn('Invalid color: ', color);
|
|
@@ -100,7 +103,7 @@ const getColorSteps = (interpolate: (x: number) => string) =>
|
|
|
100
103
|
.reverse();
|
|
101
104
|
|
|
102
105
|
const FLOW_MIN_COLOR = 'rgba(240,240,240,0.5)';
|
|
103
|
-
export const
|
|
106
|
+
export const GRAYISH = [FLOW_MIN_COLOR, ColorScheme.primary];
|
|
104
107
|
const schemeBluYl = [
|
|
105
108
|
'#f7feae',
|
|
106
109
|
'#b7e6a5',
|
|
@@ -134,7 +137,6 @@ export const schemeTeal = [
|
|
|
134
137
|
export const DEFAULT_COLOR_SCHEME = schemeTeal;
|
|
135
138
|
export const COLOR_SCHEMES: {[key: string]: string[]} = {
|
|
136
139
|
Blues: asScheme(schemeBlues),
|
|
137
|
-
BluesPale: BLUES_PALE,
|
|
138
140
|
BluGrn: [
|
|
139
141
|
'#c4e6c3',
|
|
140
142
|
'#96d2a4',
|
|
@@ -186,6 +188,7 @@ export const COLOR_SCHEMES: {[key: string]: string[]} = {
|
|
|
186
188
|
],
|
|
187
189
|
Emrld: schemeEmrld,
|
|
188
190
|
GnBu: asScheme(schemeGnBu),
|
|
191
|
+
Grayish: GRAYISH,
|
|
189
192
|
Greens: asScheme(schemeGreens),
|
|
190
193
|
Greys: asScheme(schemeGreys),
|
|
191
194
|
Inferno: getColorSteps(interpolateInferno),
|
|
@@ -330,24 +333,24 @@ const diffColors: DiffColors = {
|
|
|
330
333
|
outlineColor: 'rgb(230,233,237)',
|
|
331
334
|
};
|
|
332
335
|
|
|
333
|
-
export function
|
|
334
|
-
settingsState: SettingsState,
|
|
335
|
-
): Colors | DiffColors {
|
|
336
|
+
export function getFlowmapColors(settings: SettingsState): Colors | DiffColors {
|
|
336
337
|
return getColors(
|
|
337
338
|
false, // TODO: diffMode
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
339
|
+
settings.colorScheme,
|
|
340
|
+
settings.darkMode,
|
|
341
|
+
settings.fadeEnabled,
|
|
342
|
+
settings.fadeOpacityEnabled,
|
|
343
|
+
settings.fadeAmount,
|
|
344
|
+
settings.animationEnabled,
|
|
343
345
|
);
|
|
344
346
|
}
|
|
345
347
|
|
|
346
348
|
export function getColors(
|
|
347
349
|
diffMode: boolean,
|
|
348
|
-
|
|
350
|
+
colorScheme: string | string[] | undefined,
|
|
349
351
|
darkMode: boolean,
|
|
350
352
|
fadeEnabled: boolean,
|
|
353
|
+
fadeOpacityEnabled: boolean,
|
|
351
354
|
fadeAmount: number,
|
|
352
355
|
animate: boolean,
|
|
353
356
|
): Colors | DiffColors {
|
|
@@ -355,11 +358,18 @@ export function getColors(
|
|
|
355
358
|
return diffColors;
|
|
356
359
|
}
|
|
357
360
|
|
|
358
|
-
let scheme
|
|
361
|
+
let scheme;
|
|
359
362
|
|
|
360
|
-
if (
|
|
361
|
-
scheme =
|
|
363
|
+
if (Array.isArray(colorScheme)) {
|
|
364
|
+
scheme = colorScheme;
|
|
365
|
+
} else {
|
|
366
|
+
scheme =
|
|
367
|
+
(colorScheme && COLOR_SCHEMES[colorScheme]) || DEFAULT_COLOR_SCHEME;
|
|
368
|
+
if (darkMode) {
|
|
369
|
+
scheme = scheme.slice().reverse();
|
|
370
|
+
}
|
|
362
371
|
}
|
|
372
|
+
|
|
363
373
|
// if (animate)
|
|
364
374
|
// if (fadeAmount > 0)
|
|
365
375
|
{
|
|
@@ -390,13 +400,14 @@ export function getColors(
|
|
|
390
400
|
scheme = indices.map(
|
|
391
401
|
(c, i) => {
|
|
392
402
|
const color = colorScale(i);
|
|
393
|
-
const
|
|
394
|
-
if (color == null ||
|
|
403
|
+
const a = amount(i);
|
|
404
|
+
if (color == null || a == null) return '#000';
|
|
395
405
|
const col = hcl(color);
|
|
396
|
-
col.l = darkMode
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
406
|
+
col.l = darkMode ? col.l - col.l * a : col.l + (100 - col.l) * a;
|
|
407
|
+
col.c = col.c - col.c * (a / 4);
|
|
408
|
+
if (fadeOpacityEnabled) {
|
|
409
|
+
col.opacity = col.opacity * (1.0 - a);
|
|
410
|
+
}
|
|
400
411
|
return col.toString();
|
|
401
412
|
},
|
|
402
413
|
// interpolateRgbBasis([colorScale(i), darkMode ? '#000' : '#fff'])(amount(i))
|
|
@@ -417,15 +428,46 @@ export function getColors(
|
|
|
417
428
|
};
|
|
418
429
|
}
|
|
419
430
|
|
|
431
|
+
function interpolateRgbaBasis(colors: string[]) {
|
|
432
|
+
const spline = interpolateBasis;
|
|
433
|
+
const n = colors.length;
|
|
434
|
+
let r: any = new Array(n),
|
|
435
|
+
g: any = new Array(n),
|
|
436
|
+
b: any = new Array(n),
|
|
437
|
+
opacity: any = new Array(n),
|
|
438
|
+
i,
|
|
439
|
+
color: any;
|
|
440
|
+
for (i = 0; i < n; ++i) {
|
|
441
|
+
color = colorRgb(colors[i]);
|
|
442
|
+
r[i] = color.r || 0;
|
|
443
|
+
g[i] = color.g || 0;
|
|
444
|
+
b[i] = color.b || 0;
|
|
445
|
+
opacity[i] = color.opacity || 0;
|
|
446
|
+
}
|
|
447
|
+
r = spline(r);
|
|
448
|
+
g = spline(g);
|
|
449
|
+
b = spline(b);
|
|
450
|
+
opacity = spline(opacity);
|
|
451
|
+
// color.opacity = 1;
|
|
452
|
+
return function (t: number) {
|
|
453
|
+
color.r = r(t);
|
|
454
|
+
color.g = g(t);
|
|
455
|
+
color.b = b(t);
|
|
456
|
+
color.opacity = opacity(t);
|
|
457
|
+
return color + '';
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
420
461
|
export function createFlowColorScale(
|
|
421
462
|
domain: [number, number],
|
|
422
463
|
scheme: string[],
|
|
423
464
|
animate: boolean | undefined,
|
|
424
465
|
): ColorScale {
|
|
425
|
-
const scale = scaleSequentialPow(
|
|
466
|
+
const scale = scaleSequentialPow(interpolateRgbaBasis(scheme))
|
|
426
467
|
// @ts-ignore
|
|
427
468
|
.exponent(animate ? 1 / 2 : 1 / 3)
|
|
428
469
|
.domain(domain);
|
|
470
|
+
|
|
429
471
|
return (value: number) => colorAsRgba(scale(value));
|
|
430
472
|
}
|
|
431
473
|
|
|
@@ -516,7 +558,7 @@ export interface LocationCircleColors {
|
|
|
516
558
|
incoming?: string;
|
|
517
559
|
highlighted?: string;
|
|
518
560
|
empty?: string;
|
|
519
|
-
|
|
561
|
+
outlineEmptyMix?: number;
|
|
520
562
|
}
|
|
521
563
|
|
|
522
564
|
export interface LocationAreaColors {
|
|
@@ -563,7 +605,7 @@ export interface LocationCircleColorsRGBA {
|
|
|
563
605
|
incoming: RGBA;
|
|
564
606
|
highlighted: RGBA;
|
|
565
607
|
empty: RGBA;
|
|
566
|
-
|
|
608
|
+
outlineEmptyMix: number;
|
|
567
609
|
}
|
|
568
610
|
|
|
569
611
|
export interface LocationAreaColorsRGBA {
|
|
@@ -648,7 +690,7 @@ function getFlowAndCircleColors(
|
|
|
648
690
|
flowColorHighlighted,
|
|
649
691
|
),
|
|
650
692
|
empty: emptyColor,
|
|
651
|
-
|
|
693
|
+
outlineEmptyMix: inputColors?.locationCircles?.outlineEmptyMix ?? 0.4,
|
|
652
694
|
},
|
|
653
695
|
};
|
|
654
696
|
}
|