@carto/api-client 0.5.7-alpha-optional-spatial-filter.1 → 0.5.7-alpha-others-orderby.1
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/build/api-client.cjs +436 -406
- package/build/api-client.cjs.map +1 -1
- package/build/api-client.d.cts +45 -17
- package/build/api-client.d.ts +45 -17
- package/build/api-client.js +400 -371
- package/build/api-client.js.map +1 -1
- package/build/worker-compat.js +865 -856
- package/build/worker-compat.js.map +1 -1
- package/build/worker.js +373 -367
- package/build/worker.js.map +1 -1
- package/package.json +1 -1
- package/src/filters/tileFeatures.ts +1 -1
- package/src/filters/tileFeaturesGeometries.ts +55 -28
- package/src/filters/tileFeaturesRaster.ts +21 -10
- package/src/filters/tileFeaturesSpatialIndex.ts +50 -37
- package/src/operations/groupBy.ts +61 -12
- package/src/operations/groupByDate.ts +6 -1
- package/src/widget-sources/constants.ts +6 -0
- package/src/widget-sources/index.ts +1 -0
- package/src/widget-sources/types.ts +25 -1
- package/src/widget-sources/widget-remote-source.ts +28 -5
- package/src/widget-sources/widget-tileset-source-impl.ts +23 -10
- package/src/filters/tileIntersection.ts +0 -155
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"homepage": "https://github.com/CartoDB/carto-api-client#readme",
|
|
9
9
|
"author": "Don McCurdy <donmccurdy@carto.com>",
|
|
10
10
|
"packageManager": "yarn@4.3.1",
|
|
11
|
-
"version": "0.5.7-alpha-
|
|
11
|
+
"version": "0.5.7-alpha-others-orderby.1",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"publishConfig": {
|
|
14
14
|
"access": "public"
|
|
@@ -19,7 +19,7 @@ export type TileFeatures = {
|
|
|
19
19
|
tileFormat: TileFormat;
|
|
20
20
|
spatialDataType: SpatialDataType;
|
|
21
21
|
spatialDataColumn?: string;
|
|
22
|
-
spatialFilter
|
|
22
|
+
spatialFilter: SpatialFilter;
|
|
23
23
|
uniqueIdProperty?: string;
|
|
24
24
|
rasterMetadata?: RasterMetadata;
|
|
25
25
|
storeGeometry?: boolean;
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
import bboxPolygon from '@turf/bbox-polygon';
|
|
1
2
|
import intersects from '@turf/boolean-intersects';
|
|
3
|
+
import booleanWithin from '@turf/boolean-within';
|
|
4
|
+
import intersect from '@turf/intersect';
|
|
5
|
+
import {transformToTileCoords} from '../utils/transformToTileCoords.js';
|
|
2
6
|
import {transformTileCoordsToWGS84} from '../utils/transformTileCoordsToWGS84.js';
|
|
3
7
|
import {TileFormat} from '../constants.js';
|
|
4
8
|
import type {
|
|
5
9
|
BBox,
|
|
10
|
+
Feature,
|
|
6
11
|
Geometry,
|
|
7
12
|
LineString,
|
|
13
|
+
MultiPolygon,
|
|
8
14
|
Point,
|
|
9
15
|
Polygon,
|
|
10
16
|
Position,
|
|
11
17
|
} from 'geojson';
|
|
12
18
|
import type {SpatialFilter, Tile} from '../types.js';
|
|
19
|
+
import {featureCollection} from '@turf/helpers';
|
|
13
20
|
import type {FeatureData} from '../types-internal.js';
|
|
14
21
|
import type {
|
|
15
22
|
BinaryAttribute,
|
|
@@ -20,7 +27,6 @@ import type {
|
|
|
20
27
|
BinaryPolygonFeature,
|
|
21
28
|
TypedArrayConstructor,
|
|
22
29
|
} from '@loaders.gl/schema';
|
|
23
|
-
import {intersectTileGeometry} from './tileIntersection.js';
|
|
24
30
|
|
|
25
31
|
export const FEATURE_GEOM_PROPERTY = '__geomValue';
|
|
26
32
|
|
|
@@ -45,7 +51,7 @@ export function tileFeaturesGeometries({
|
|
|
45
51
|
}: {
|
|
46
52
|
tiles: Tile[];
|
|
47
53
|
tileFormat?: TileFormat;
|
|
48
|
-
spatialFilter
|
|
54
|
+
spatialFilter: SpatialFilter;
|
|
49
55
|
uniqueIdProperty?: string;
|
|
50
56
|
options?: GeometryExtractOptions;
|
|
51
57
|
}): FeatureData[] {
|
|
@@ -58,50 +64,65 @@ export function tileFeaturesGeometries({
|
|
|
58
64
|
continue;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
const
|
|
67
|
+
const bbox = [
|
|
62
68
|
tile.bbox.west,
|
|
63
69
|
tile.bbox.south,
|
|
64
70
|
tile.bbox.east,
|
|
65
71
|
tile.bbox.north,
|
|
66
72
|
] as BBox;
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
const bboxToGeom = bboxPolygon(bbox);
|
|
74
|
+
const tileIsFullyVisible = booleanWithin(bboxToGeom, spatialFilter);
|
|
75
|
+
|
|
76
|
+
// Clip the geometry to intersect with the tile
|
|
77
|
+
const spatialFilterFeature: Feature<Polygon | MultiPolygon> = {
|
|
78
|
+
type: 'Feature',
|
|
79
|
+
geometry: spatialFilter,
|
|
80
|
+
properties: {},
|
|
81
|
+
};
|
|
82
|
+
const clippedGeometryToIntersect = intersect(
|
|
83
|
+
featureCollection([bboxToGeom, spatialFilterFeature])
|
|
72
84
|
);
|
|
73
85
|
|
|
74
|
-
if (
|
|
86
|
+
if (!clippedGeometryToIntersect) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
75
89
|
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
// We assume that MVT tileFormat uses local coordinates so we transform the geometry to intersect to tile coordinates [0..1],
|
|
91
|
+
// while in the case of 'geojson' or binary, the geometries are already in WGS84
|
|
92
|
+
const transformedGeometryToIntersect =
|
|
93
|
+
tileFormat === TileFormat.MVT
|
|
94
|
+
? transformToTileCoords(clippedGeometryToIntersect.geometry, bbox)
|
|
95
|
+
: clippedGeometryToIntersect.geometry;
|
|
78
96
|
|
|
79
97
|
calculateFeatures({
|
|
80
98
|
map,
|
|
81
|
-
|
|
99
|
+
tileIsFullyVisible,
|
|
100
|
+
geometryIntersection: transformedGeometryToIntersect,
|
|
82
101
|
data: tile.data.points!,
|
|
83
102
|
type: 'Point',
|
|
84
|
-
bbox
|
|
103
|
+
bbox,
|
|
85
104
|
tileFormat,
|
|
86
105
|
uniqueIdProperty,
|
|
87
106
|
options,
|
|
88
107
|
});
|
|
89
108
|
calculateFeatures({
|
|
90
109
|
map,
|
|
91
|
-
|
|
110
|
+
tileIsFullyVisible,
|
|
111
|
+
geometryIntersection: transformedGeometryToIntersect,
|
|
92
112
|
data: tile.data.lines!,
|
|
93
113
|
type: 'LineString',
|
|
94
|
-
bbox
|
|
114
|
+
bbox,
|
|
95
115
|
tileFormat,
|
|
96
116
|
uniqueIdProperty,
|
|
97
117
|
options,
|
|
98
118
|
});
|
|
99
119
|
calculateFeatures({
|
|
100
120
|
map,
|
|
101
|
-
|
|
121
|
+
tileIsFullyVisible,
|
|
122
|
+
geometryIntersection: transformedGeometryToIntersect,
|
|
102
123
|
data: tile.data.polygons!,
|
|
103
124
|
type: 'Polygon',
|
|
104
|
-
bbox
|
|
125
|
+
bbox,
|
|
105
126
|
tileFormat,
|
|
106
127
|
uniqueIdProperty,
|
|
107
128
|
options,
|
|
@@ -120,7 +141,7 @@ function processTileFeatureProperties({
|
|
|
120
141
|
tileFormat,
|
|
121
142
|
uniqueIdProperty,
|
|
122
143
|
storeGeometry,
|
|
123
|
-
|
|
144
|
+
geometryIntersection,
|
|
124
145
|
}: {
|
|
125
146
|
map: TileMap;
|
|
126
147
|
data: BinaryFeature;
|
|
@@ -131,7 +152,7 @@ function processTileFeatureProperties({
|
|
|
131
152
|
tileFormat?: TileFormat;
|
|
132
153
|
uniqueIdProperty?: string;
|
|
133
154
|
storeGeometry: boolean;
|
|
134
|
-
|
|
155
|
+
geometryIntersection?: Geometry;
|
|
135
156
|
}) {
|
|
136
157
|
const tileProps = getPropertiesFromTile(data, startIndex);
|
|
137
158
|
const uniquePropertyValue = getUniquePropertyValue(
|
|
@@ -146,7 +167,7 @@ function processTileFeatureProperties({
|
|
|
146
167
|
let geometry: Geometry | null = null;
|
|
147
168
|
|
|
148
169
|
// Only calculate geometry if necessary
|
|
149
|
-
if (storeGeometry ||
|
|
170
|
+
if (storeGeometry || geometryIntersection) {
|
|
150
171
|
const {positions} = data;
|
|
151
172
|
const ringCoordinates = getRingCoordinatesFor(
|
|
152
173
|
startIndex,
|
|
@@ -157,7 +178,11 @@ function processTileFeatureProperties({
|
|
|
157
178
|
}
|
|
158
179
|
|
|
159
180
|
// If intersection is required, check before proceeding
|
|
160
|
-
if (
|
|
181
|
+
if (
|
|
182
|
+
geometry &&
|
|
183
|
+
geometryIntersection &&
|
|
184
|
+
!intersects(geometry, geometryIntersection)
|
|
185
|
+
) {
|
|
161
186
|
return;
|
|
162
187
|
}
|
|
163
188
|
|
|
@@ -176,7 +201,7 @@ function processTileFeatureProperties({
|
|
|
176
201
|
function addIntersectedFeaturesInTile({
|
|
177
202
|
map,
|
|
178
203
|
data,
|
|
179
|
-
|
|
204
|
+
geometryIntersection,
|
|
180
205
|
type,
|
|
181
206
|
bbox,
|
|
182
207
|
tileFormat,
|
|
@@ -185,7 +210,7 @@ function addIntersectedFeaturesInTile({
|
|
|
185
210
|
}: {
|
|
186
211
|
map: TileMap;
|
|
187
212
|
data: BinaryFeature;
|
|
188
|
-
|
|
213
|
+
geometryIntersection: Geometry;
|
|
189
214
|
type: BinaryGeometryType;
|
|
190
215
|
bbox: BBox;
|
|
191
216
|
tileFormat?: TileFormat;
|
|
@@ -208,7 +233,7 @@ function addIntersectedFeaturesInTile({
|
|
|
208
233
|
tileFormat,
|
|
209
234
|
uniqueIdProperty,
|
|
210
235
|
storeGeometry,
|
|
211
|
-
|
|
236
|
+
geometryIntersection,
|
|
212
237
|
});
|
|
213
238
|
}
|
|
214
239
|
}
|
|
@@ -325,7 +350,8 @@ function getRingCoordinatesFor(
|
|
|
325
350
|
|
|
326
351
|
function calculateFeatures({
|
|
327
352
|
map,
|
|
328
|
-
|
|
353
|
+
tileIsFullyVisible,
|
|
354
|
+
geometryIntersection,
|
|
329
355
|
data,
|
|
330
356
|
type,
|
|
331
357
|
bbox,
|
|
@@ -334,7 +360,8 @@ function calculateFeatures({
|
|
|
334
360
|
options,
|
|
335
361
|
}: {
|
|
336
362
|
map: TileMap;
|
|
337
|
-
|
|
363
|
+
tileIsFullyVisible: boolean;
|
|
364
|
+
geometryIntersection: SpatialFilter;
|
|
338
365
|
data: BinaryFeature;
|
|
339
366
|
type: BinaryGeometryType;
|
|
340
367
|
bbox: BBox;
|
|
@@ -346,7 +373,7 @@ function calculateFeatures({
|
|
|
346
373
|
return;
|
|
347
374
|
}
|
|
348
375
|
|
|
349
|
-
if (
|
|
376
|
+
if (tileIsFullyVisible) {
|
|
350
377
|
addAllFeaturesInTile({
|
|
351
378
|
map,
|
|
352
379
|
data,
|
|
@@ -360,7 +387,7 @@ function calculateFeatures({
|
|
|
360
387
|
addIntersectedFeaturesInTile({
|
|
361
388
|
map,
|
|
362
389
|
data,
|
|
363
|
-
|
|
390
|
+
geometryIntersection,
|
|
364
391
|
type,
|
|
365
392
|
bbox,
|
|
366
393
|
tileFormat,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cellToChildren as _cellToChildren,
|
|
3
|
+
cellToBoundary,
|
|
3
4
|
cellToTile,
|
|
5
|
+
geometryToCells,
|
|
4
6
|
getResolution,
|
|
5
7
|
} from 'quadbin';
|
|
6
8
|
import type {RasterTile, SpatialFilter, Tile} from '../types.js';
|
|
@@ -10,11 +12,13 @@ import type {
|
|
|
10
12
|
RasterMetadataBand,
|
|
11
13
|
SpatialDataType,
|
|
12
14
|
} from '../sources/types.js';
|
|
13
|
-
import
|
|
15
|
+
import booleanWithin from '@turf/boolean-within';
|
|
16
|
+
import intersect from '@turf/intersect';
|
|
17
|
+
import {feature, featureCollection} from '@turf/helpers';
|
|
14
18
|
|
|
15
19
|
export type TileFeaturesRasterOptions = {
|
|
16
20
|
tiles: RasterTile[];
|
|
17
|
-
spatialFilter
|
|
21
|
+
spatialFilter: SpatialFilter;
|
|
18
22
|
spatialDataColumn: string;
|
|
19
23
|
spatialDataType: SpatialDataType;
|
|
20
24
|
rasterMetadata: RasterMetadata;
|
|
@@ -48,20 +52,27 @@ export function tileFeaturesRaster({
|
|
|
48
52
|
for (const tile of tiles as Required<RasterTile>[]) {
|
|
49
53
|
const parent = tile.index.q;
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
// If tile is partially overlapping with the spatial filter, compute a quadbin covering for the
|
|
56
|
+
// spatial filter + tile intersection, at cell resolution. If tile is fully inside or outside
|
|
57
|
+
// the spatial filter, computing a covering can be skipped. Avoid computing a quadbin covering
|
|
58
|
+
// for the _entire_ spatial filter, which may contain far too many cells (e.g. at zoom=3 and
|
|
59
|
+
// resolution=14, we have ~18,000,000 cells in the viewport.)
|
|
60
|
+
const tilePolygon = cellToBoundary(parent);
|
|
61
|
+
const tileFilter = intersect(
|
|
62
|
+
featureCollection([feature(tilePolygon), feature(options.spatialFilter)])
|
|
55
63
|
);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
const needsFilter = tileFilter
|
|
65
|
+
? !booleanWithin(tilePolygon, options.spatialFilter)
|
|
66
|
+
: false;
|
|
67
|
+
const tileFilterCells = needsFilter
|
|
68
|
+
? new Set(geometryToCells(tileFilter!.geometry, cellResolution))
|
|
69
|
+
: null;
|
|
59
70
|
const tileSortedCells = cellToChildrenSorted(parent, cellResolution);
|
|
60
71
|
|
|
61
72
|
// For each pixel/cell within the spatial filter, create a FeatureData.
|
|
62
73
|
// Order is row-major, starting from NW and ending at SE.
|
|
63
74
|
for (let i = 0; i < tileSortedCells.length; i++) {
|
|
64
|
-
if (
|
|
75
|
+
if (needsFilter && !tileFilterCells!.has(tileSortedCells[i])) {
|
|
65
76
|
continue;
|
|
66
77
|
}
|
|
67
78
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {SpatialIndex} from '../constants.js';
|
|
2
|
-
import {getResolution as quadbinGetResolution} from 'quadbin';
|
|
2
|
+
import {getResolution as quadbinGetResolution, geometryToCells} from 'quadbin';
|
|
3
|
+
import bboxClip from '@turf/bbox-clip';
|
|
3
4
|
import type {SpatialFilter, SpatialIndexTile} from '../types.js';
|
|
4
|
-
import type {Feature} from 'geojson';
|
|
5
|
-
import {getResolution as h3GetResolution} from 'h3-js';
|
|
5
|
+
import type {BBox, Feature} from 'geojson';
|
|
6
|
+
import {getResolution as h3GetResolution, polygonToCells} from 'h3-js';
|
|
6
7
|
import type {FeatureData} from '../types-internal.js';
|
|
7
8
|
import type {SpatialDataType} from '../sources/types.js';
|
|
8
|
-
import {intersectTileH3, intersectTileQuadbin} from './tileIntersection.js';
|
|
9
9
|
|
|
10
10
|
export type TileFeaturesSpatialIndexOptions = {
|
|
11
11
|
tiles: SpatialIndexTile[];
|
|
12
|
-
spatialFilter
|
|
12
|
+
spatialFilter: SpatialFilter;
|
|
13
13
|
spatialDataColumn: string;
|
|
14
14
|
spatialDataType: SpatialDataType;
|
|
15
15
|
};
|
|
@@ -22,41 +22,30 @@ export function tileFeaturesSpatialIndex({
|
|
|
22
22
|
}: TileFeaturesSpatialIndexOptions): FeatureData[] {
|
|
23
23
|
const map = new Map();
|
|
24
24
|
const spatialIndex = getSpatialIndex(spatialDataType);
|
|
25
|
-
const
|
|
25
|
+
const resolution = getResolution(tiles, spatialIndex);
|
|
26
26
|
const spatialIndexIDName = spatialDataColumn
|
|
27
27
|
? spatialDataColumn
|
|
28
28
|
: spatialIndex;
|
|
29
29
|
|
|
30
|
-
if (!
|
|
30
|
+
if (!resolution) {
|
|
31
31
|
return [];
|
|
32
32
|
}
|
|
33
|
+
const cells = getCellsCoverGeometry(spatialFilter, spatialIndex, resolution);
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Compute H3 intersection globally, Quadbin intersection per-tile. See tileIntersection.ts.
|
|
37
|
-
if (spatialIndex === SpatialIndex.H3) {
|
|
38
|
-
intersection = intersectTileH3(cellResolution as number, spatialFilter);
|
|
35
|
+
if (!cells?.length) {
|
|
36
|
+
return [];
|
|
39
37
|
}
|
|
40
38
|
|
|
39
|
+
// We transform cells to Set to improve the performace
|
|
40
|
+
const cellsSet = new Set<bigint | string>(cells);
|
|
41
|
+
|
|
41
42
|
for (const tile of tiles) {
|
|
42
43
|
if (tile.isVisible === false || !tile.data) {
|
|
43
44
|
continue;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
if (spatialIndex === SpatialIndex.QUADBIN) {
|
|
47
|
-
const parent = getTileIndex(tile, spatialIndex);
|
|
48
|
-
intersection = intersectTileQuadbin(
|
|
49
|
-
parent as bigint,
|
|
50
|
-
cellResolution as bigint,
|
|
51
|
-
spatialFilter
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!intersection) continue;
|
|
56
|
-
|
|
57
47
|
tile.data.forEach((d: Feature) => {
|
|
58
|
-
|
|
59
|
-
if (intersection === true || intersection.has(d.id as bigint | string)) {
|
|
48
|
+
if (cellsSet.has(d.id as bigint | string)) {
|
|
60
49
|
map.set(d.id, {...d.properties, [spatialIndexIDName]: d.id});
|
|
61
50
|
}
|
|
62
51
|
});
|
|
@@ -64,21 +53,10 @@ export function tileFeaturesSpatialIndex({
|
|
|
64
53
|
return Array.from(map.values());
|
|
65
54
|
}
|
|
66
55
|
|
|
67
|
-
function getTileIndex(
|
|
68
|
-
tile: SpatialIndexTile,
|
|
69
|
-
spatialIndex: SpatialIndex
|
|
70
|
-
): bigint | string {
|
|
71
|
-
if (spatialIndex === SpatialIndex.QUADBIN) {
|
|
72
|
-
// @ts-expect-error Missing types for quadbin tile indices.
|
|
73
|
-
return tile.index.q;
|
|
74
|
-
}
|
|
75
|
-
return tile.id;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
56
|
function getResolution(
|
|
79
57
|
tiles: SpatialIndexTile[],
|
|
80
58
|
spatialIndex: SpatialIndex
|
|
81
|
-
):
|
|
59
|
+
): number | undefined {
|
|
82
60
|
const data = tiles.find((tile) => tile.data?.length)?.data;
|
|
83
61
|
|
|
84
62
|
if (!data) {
|
|
@@ -94,6 +72,41 @@ function getResolution(
|
|
|
94
72
|
}
|
|
95
73
|
}
|
|
96
74
|
|
|
75
|
+
const bboxWest: BBox = [-180, -90, 0, 90];
|
|
76
|
+
const bboxEast: BBox = [0, -90, 180, 90];
|
|
77
|
+
|
|
78
|
+
function getCellsCoverGeometry(
|
|
79
|
+
geometry: SpatialFilter,
|
|
80
|
+
spatialIndex: SpatialIndex,
|
|
81
|
+
resolution: number
|
|
82
|
+
) {
|
|
83
|
+
if (spatialIndex === SpatialIndex.QUADBIN) {
|
|
84
|
+
// @ts-expect-error TODO: Probably ought to be stricter about number vs. bigint types in this file.
|
|
85
|
+
return geometryToCells(geometry, resolution);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (spatialIndex === SpatialIndex.H3) {
|
|
89
|
+
// The current H3 polyfill algorithm can't deal with polygon segments of greater than 180 degrees longitude
|
|
90
|
+
// so we clip the geometry to be sure that none of them is greater than 180 degrees
|
|
91
|
+
// https://github.com/uber/h3-js/issues/24#issuecomment-431893796
|
|
92
|
+
return polygonToCells(
|
|
93
|
+
bboxClip(geometry, bboxWest).geometry.coordinates as
|
|
94
|
+
| number[][]
|
|
95
|
+
| number[][][],
|
|
96
|
+
resolution,
|
|
97
|
+
true
|
|
98
|
+
).concat(
|
|
99
|
+
polygonToCells(
|
|
100
|
+
bboxClip(geometry, bboxEast).geometry.coordinates as
|
|
101
|
+
| number[][]
|
|
102
|
+
| number[][][],
|
|
103
|
+
resolution,
|
|
104
|
+
true
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
97
110
|
function getSpatialIndex(spatialDataType: SpatialDataType): SpatialIndex {
|
|
98
111
|
switch (spatialDataType) {
|
|
99
112
|
case 'h3':
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {aggregationFunctions, aggregate} from './aggregation.js';
|
|
2
|
+
import {OTHERS_CATEGORY_NAME} from '../widget-sources/constants.js';
|
|
2
3
|
import type {AggregationType} from '../types.js';
|
|
3
4
|
import type {FeatureData} from '../types-internal.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}[];
|
|
5
|
+
import type {
|
|
6
|
+
CategoryOrderBy,
|
|
7
|
+
CategoryResponseEntry,
|
|
8
|
+
CategoryResponseRaw,
|
|
9
|
+
} from '../widget-sources/types.js';
|
|
10
10
|
|
|
11
11
|
/** @privateRemarks Source: @carto/react-core */
|
|
12
12
|
export function groupValuesByColumn({
|
|
@@ -15,15 +15,19 @@ export function groupValuesByColumn({
|
|
|
15
15
|
joinOperation,
|
|
16
16
|
keysColumn,
|
|
17
17
|
operation,
|
|
18
|
+
othersThreshold,
|
|
19
|
+
orderBy = 'frequency_desc',
|
|
18
20
|
}: {
|
|
19
21
|
data: FeatureData[];
|
|
20
22
|
valuesColumns?: string[];
|
|
21
23
|
joinOperation?: AggregationType;
|
|
22
24
|
keysColumn: string;
|
|
23
25
|
operation: AggregationType;
|
|
24
|
-
|
|
26
|
+
othersThreshold?: number;
|
|
27
|
+
orderBy?: CategoryOrderBy;
|
|
28
|
+
}): CategoryResponseRaw | null {
|
|
25
29
|
if (Array.isArray(data) && data.length === 0) {
|
|
26
|
-
return null;
|
|
30
|
+
return {rows: null};
|
|
27
31
|
}
|
|
28
32
|
const groups = data.reduce((accumulator, item) => {
|
|
29
33
|
const group = item[keysColumn];
|
|
@@ -48,12 +52,57 @@ export function groupValuesByColumn({
|
|
|
48
52
|
const targetOperation =
|
|
49
53
|
aggregationFunctions[operation as Exclude<AggregationType, 'custom'>];
|
|
50
54
|
|
|
51
|
-
if (targetOperation) {
|
|
52
|
-
return
|
|
55
|
+
if (!targetOperation) {
|
|
56
|
+
return {rows: []};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const allCategories = Array.from(groups)
|
|
60
|
+
.map(([name, value]) => ({
|
|
53
61
|
name,
|
|
54
62
|
value: targetOperation(value),
|
|
55
|
-
}))
|
|
63
|
+
}))
|
|
64
|
+
.sort(getSorter(orderBy));
|
|
65
|
+
|
|
66
|
+
if (othersThreshold && allCategories.length > othersThreshold) {
|
|
67
|
+
const otherValue = allCategories
|
|
68
|
+
.slice(othersThreshold)
|
|
69
|
+
.flatMap(({name}) => groups.get(name));
|
|
70
|
+
allCategories.push({
|
|
71
|
+
name: OTHERS_CATEGORY_NAME,
|
|
72
|
+
value: targetOperation(otherValue),
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
rows: allCategories,
|
|
76
|
+
metadata: {
|
|
77
|
+
others: targetOperation(otherValue),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
56
80
|
}
|
|
57
81
|
|
|
58
|
-
return
|
|
82
|
+
return {
|
|
83
|
+
rows: allCategories,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getSorter(
|
|
88
|
+
orderBy: CategoryOrderBy
|
|
89
|
+
): (a: CategoryResponseEntry, b: CategoryResponseEntry) => number {
|
|
90
|
+
switch (orderBy) {
|
|
91
|
+
case 'frequency_asc':
|
|
92
|
+
// 'value ASC, name ASC'
|
|
93
|
+
return (a, b) => a.value - b.value || localeCompare(a.name, b.name);
|
|
94
|
+
case 'frequency_desc':
|
|
95
|
+
// 'value DESC, name ASC'
|
|
96
|
+
return (a, b) => b.value - a.value || localeCompare(a.name, b.name);
|
|
97
|
+
case 'alphabetical_asc':
|
|
98
|
+
// 'name ASC, value DESC'
|
|
99
|
+
return (a, b) => localeCompare(a.name, b.name) || b.value - a.value;
|
|
100
|
+
case 'alphabetical_desc':
|
|
101
|
+
// 'name DESC, value DESC'
|
|
102
|
+
return (a, b) => localeCompare(b.name, a.name) || b.value - a.value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function localeCompare(a?: string | null, b?: string | null) {
|
|
107
|
+
return (a ?? 'null').localeCompare(b ?? 'null');
|
|
59
108
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type {AggregationType, GroupDateType} from '../types.js';
|
|
2
2
|
import {getUTCMonday} from '../utils/dateUtils.js';
|
|
3
3
|
import {aggregate, aggregationFunctions} from './aggregation.js';
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
/** @privateRemarks Source: @carto/react-core */
|
|
6
|
+
export type GroupByFeature = {
|
|
7
|
+
name: string;
|
|
8
|
+
value: number;
|
|
9
|
+
}[];
|
|
5
10
|
|
|
6
11
|
const GROUP_KEY_FN_MAPPING: Record<GroupDateType, (date: Date) => number> = {
|
|
7
12
|
year: (date: Date) => Date.UTC(date.getUTCFullYear()),
|
|
@@ -31,6 +31,12 @@ export interface BaseRequestOptions {
|
|
|
31
31
|
filterOwner?: string;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export type CategoryOrderBy =
|
|
35
|
+
| 'frequency_asc' // sort by aggregate value ascending, then by name ascending
|
|
36
|
+
| 'frequency_desc' // sort by aggregate value descending, then by name ascending
|
|
37
|
+
| 'alphabetical_asc' // sort by category name ascending, then by value descending
|
|
38
|
+
| 'alphabetical_desc'; // sort by category name descending, then by value descending
|
|
39
|
+
|
|
34
40
|
/**
|
|
35
41
|
* Examples:
|
|
36
42
|
* * population by state
|
|
@@ -59,6 +65,15 @@ export interface CategoryRequestOptions extends BaseRequestOptions {
|
|
|
59
65
|
operationColumn?: string;
|
|
60
66
|
/** Local only. */
|
|
61
67
|
joinOperation?: 'count' | 'avg' | 'min' | 'max' | 'sum';
|
|
68
|
+
/** Calculate `_carto_others` category for all categories after first N (N is threshold). */
|
|
69
|
+
othersThreshold?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Order categories by frequency or alphabetically.
|
|
72
|
+
* @default 'frequency_desc'
|
|
73
|
+
*/
|
|
74
|
+
orderBy?: CategoryOrderBy;
|
|
75
|
+
/** Return raw result (CategoryResponseRaw). */
|
|
76
|
+
rawResult?: boolean;
|
|
62
77
|
}
|
|
63
78
|
|
|
64
79
|
/**
|
|
@@ -205,8 +220,17 @@ export type FeaturesResponse = {rows: Record<string, unknown>[]};
|
|
|
205
220
|
/** Response from {@link WidgetRemoteSource#getFormula}. */
|
|
206
221
|
export type FormulaResponse = {value: number | null};
|
|
207
222
|
|
|
223
|
+
/** Entry in the category widget response, see {@link WidgetRemoteSource#getCategories}. */
|
|
224
|
+
export type CategoryResponseEntry = {name: string | null; value: number};
|
|
208
225
|
/** Response from {@link WidgetRemoteSource#getCategories}. */
|
|
209
|
-
export type CategoryResponse =
|
|
226
|
+
export type CategoryResponse = CategoryResponseEntry[];
|
|
227
|
+
|
|
228
|
+
export type CategoryResponseRaw = {
|
|
229
|
+
rows: CategoryResponseEntry[] | null;
|
|
230
|
+
metadata?: {
|
|
231
|
+
others?: number;
|
|
232
|
+
};
|
|
233
|
+
};
|
|
210
234
|
|
|
211
235
|
/** Response from {@link WidgetRemoteSource#getRange}. */
|
|
212
236
|
export type RangeResponse = {min: number; max: number} | null;
|
|
@@ -23,6 +23,7 @@ import {WidgetSource, type WidgetSourceProps} from './widget-source.js';
|
|
|
23
23
|
import type {Filters} from '../types.js';
|
|
24
24
|
import {AggregationTypes, ApiVersion} from '../constants.js';
|
|
25
25
|
import {getApplicableFilters} from '../filters.js';
|
|
26
|
+
import {OTHERS_CATEGORY_NAME} from './constants.js';
|
|
26
27
|
|
|
27
28
|
export type WidgetRemoteSourceProps = WidgetSourceProps;
|
|
28
29
|
|
|
@@ -72,17 +73,23 @@ export abstract class WidgetRemoteSource<
|
|
|
72
73
|
filterOwner,
|
|
73
74
|
spatialFilter,
|
|
74
75
|
spatialFiltersMode,
|
|
76
|
+
rawResult,
|
|
75
77
|
...params
|
|
76
78
|
} = options;
|
|
77
|
-
const {
|
|
79
|
+
const {
|
|
80
|
+
column,
|
|
81
|
+
operation,
|
|
82
|
+
operationColumn,
|
|
83
|
+
operationExp,
|
|
84
|
+
othersThreshold,
|
|
85
|
+
orderBy,
|
|
86
|
+
} = params;
|
|
78
87
|
|
|
79
88
|
if (operation === AggregationTypes.Custom) {
|
|
80
89
|
assert(operationExp, 'operationExp is required for custom operation');
|
|
81
90
|
}
|
|
82
91
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return executeModel({
|
|
92
|
+
const result = await executeModel({
|
|
86
93
|
model: 'category',
|
|
87
94
|
source: {
|
|
88
95
|
...this.getModelSource(filters, filterOwner),
|
|
@@ -94,9 +101,25 @@ export abstract class WidgetRemoteSource<
|
|
|
94
101
|
operation,
|
|
95
102
|
operationExp,
|
|
96
103
|
operationColumn: operationColumn || column,
|
|
104
|
+
othersThreshold,
|
|
105
|
+
orderBy,
|
|
97
106
|
},
|
|
98
107
|
opts: {signal, headers: this.props.headers},
|
|
99
|
-
})
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const normalizedRows = normalizeObjectKeys(result.rows || []);
|
|
111
|
+
if (rawResult) {
|
|
112
|
+
return result as unknown as CategoryResponse;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!othersThreshold) {
|
|
116
|
+
return normalizedRows;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return [
|
|
120
|
+
...normalizedRows,
|
|
121
|
+
{name: OTHERS_CATEGORY_NAME, value: result?.metadata?.others as number},
|
|
122
|
+
];
|
|
100
123
|
}
|
|
101
124
|
|
|
102
125
|
async getFeatures(
|