@carto/api-client 0.5.7 → 0.5.8-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/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",
11
+ "version": "0.5.8-alpha-others-orderby.1",
12
12
  "license": "MIT",
13
13
  "publishConfig": {
14
14
  "access": "public"
@@ -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
- /** @privateRemarks Source: @carto/react-core */
6
- export type GroupByFeature = {
7
- name: string;
8
- value: number;
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
- }): GroupByFeature | null {
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 Array.from(groups).map(([name, value]) => ({
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
- import type {GroupByFeature} from './groupBy.js';
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()),
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Name of the category that represents the "Others" category.
3
+ *
4
+ * See `WidgetSource.getCategories` for more information.
5
+ */
6
+ export const OTHERS_CATEGORY_NAME = '_carto_others';
@@ -5,3 +5,4 @@ export * from './widget-remote-source.js';
5
5
  export * from './widget-table-source.js';
6
6
  export * from './widget-tileset-source.js';
7
7
  export * from './types.js';
8
+ export * from './constants.js';
@@ -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 = {name: string; value: number}[];
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 {column, operation, operationColumn, operationExp} = params;
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
- type CategoriesModelResponse = {rows: {name: string; value: number}[]};
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
- }).then((res: CategoriesModelResponse) => normalizeObjectKeys(res.rows));
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(
@@ -40,6 +40,7 @@ import {booleanEqual} from '@turf/boolean-equal';
40
40
  import type {WidgetTilesetSourceProps} from './widget-tileset-source.js';
41
41
  import {getApplicableFilters} from '../filters.js';
42
42
  import {AggregationTypes} from '../constants.js';
43
+ import {OTHERS_CATEGORY_NAME} from './constants.js';
43
44
 
44
45
  // TODO(cleanup): Parameter defaults in source functions and widget API calls are
45
46
  // currently duplicated and possibly inconsistent. Consider consolidating and
@@ -189,6 +190,9 @@ export class WidgetTilesetSourceImpl extends WidgetSource<WidgetTilesetSourcePro
189
190
  filters,
190
191
  filterOwner,
191
192
  spatialFilter,
193
+ othersThreshold,
194
+ orderBy = 'frequency_desc',
195
+ rawResult,
192
196
  }: CategoryRequestOptions): Promise<CategoryResponse> {
193
197
  const filteredFeatures = this._getFilteredFeatures(
194
198
  spatialFilter,
@@ -202,15 +206,28 @@ export class WidgetTilesetSourceImpl extends WidgetSource<WidgetTilesetSourcePro
202
206
 
203
207
  assertColumn(this._features, column, operationColumn as string);
204
208
 
205
- const groups = groupValuesByColumn({
209
+ const result = groupValuesByColumn({
206
210
  data: filteredFeatures,
207
211
  valuesColumns: normalizeColumns(operationColumn || column),
208
212
  joinOperation,
209
213
  keysColumn: column,
210
214
  operation,
215
+ othersThreshold,
216
+ orderBy,
211
217
  });
212
218
 
213
- return groups || [];
219
+ if (rawResult) {
220
+ return result as unknown as CategoryResponse;
221
+ }
222
+
223
+ if (!othersThreshold) {
224
+ return result?.rows || [];
225
+ }
226
+
227
+ return [
228
+ ...(result?.rows || []),
229
+ {name: OTHERS_CATEGORY_NAME, value: result?.metadata?.others as number},
230
+ ];
214
231
  }
215
232
 
216
233
  override async getScatter({