@carto/api-client 0.5.27 → 0.5.29

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.27",
11
+ "version": "0.5.29",
12
12
  "license": "MIT",
13
13
  "publishConfig": {
14
14
  "access": "public"
@@ -302,6 +302,28 @@ export async function fetchMap({
302
302
  }
303
303
  });
304
304
 
305
+ // Flag datasets that need feature bounding boxes for label positioning.
306
+ // Some data waraehouse (notably when using MVT) only support clipped
307
+ // geometry. The server-provided bbox enables stable label placement.
308
+ const layers = map.keplerMapConfig.config.visState.layers;
309
+ const datasetsWithLabels = new Set<string>();
310
+ for (const layer of layers) {
311
+ const hasTextLabel = layer.config?.textLabel?.some(
312
+ (t: any) => t.field?.name
313
+ );
314
+ if (hasTextLabel) {
315
+ datasetsWithLabels.add(layer.config.dataId);
316
+ }
317
+ }
318
+ map.datasets.forEach((dataset: any) => {
319
+ if (
320
+ datasetsWithLabels.has(dataset.id) &&
321
+ (dataset.type === 'table' || dataset.type === 'query')
322
+ ) {
323
+ dataset.featureBbox = true;
324
+ }
325
+ });
326
+
305
327
  const [basemap] = await Promise.all([
306
328
  fetchBasemapProps({config: map.keplerMapConfig.config, errorContext}),
307
329
 
@@ -514,10 +514,11 @@ export function getMaxMarkerSize(
514
514
  visConfig: VisConfig,
515
515
  visualChannels: VisualChannels
516
516
  ): number {
517
- const {radiusRange, radius} = visConfig;
517
+ const {radiusRange, radius, sizeMaxPixels} = visConfig;
518
518
  const {radiusField, sizeField} = visualChannels;
519
519
  const field = radiusField || sizeField;
520
- return Math.ceil(radiusRange && field ? radiusRange[1] : radius);
520
+ const baseSize = radiusRange && field ? radiusRange[1] : radius;
521
+ return Math.ceil(sizeMaxPixels ?? baseSize);
521
522
  }
522
523
 
523
524
  type Accessor = number | ((d: any, i: any) => number);
@@ -128,6 +128,7 @@ export function getLayerDescriptor({
128
128
  ...createInteractionProps(interactionConfig),
129
129
  ...styleProps,
130
130
  ...channelProps,
131
+ ...createZoomScaleProps(config, visualChannels),
131
132
  ...createParametersProp(layerBlending, styleProps.parameters || {}), // Must come after style
132
133
  ...createLoadOptions(data.accessToken),
133
134
  },
@@ -208,6 +209,38 @@ function createInteractionProps(interactionConfig: any) {
208
209
  };
209
210
  }
210
211
 
212
+ function createZoomScaleProps(
213
+ config: MapLayerConfig,
214
+ visualChannels: VisualChannels
215
+ ): Record<string, any> {
216
+ const {visConfig} = config;
217
+ if (
218
+ !visConfig.radiusScaleWithZoom ||
219
+ visualChannels.radiusField ||
220
+ visualChannels.sizeField
221
+ ) {
222
+ return {};
223
+ }
224
+ // When `radiusScaleWithZoom` is enabled, render the point in `common`
225
+ // coordinate space so it scales proportionally with zoom.
226
+ const scale = Math.pow(2, -(visConfig.radiusReferenceZoom as number));
227
+ const result: Record<string, any> = {
228
+ pointRadiusUnits: 'common',
229
+ pointRadiusScale: scale,
230
+ iconSizeUnits: 'common',
231
+ iconSizeScale: scale,
232
+ };
233
+ if (visConfig.sizeMinPixels !== undefined) {
234
+ result.pointRadiusMinPixels = visConfig.sizeMinPixels;
235
+ result.iconSizeMinPixels = visConfig.sizeMinPixels;
236
+ }
237
+ if (visConfig.sizeMaxPixels !== undefined) {
238
+ result.pointRadiusMaxPixels = visConfig.sizeMaxPixels;
239
+ result.iconSizeMaxPixels = visConfig.sizeMaxPixels;
240
+ }
241
+ return result;
242
+ }
243
+
211
244
  function mapProps(source: any, target: any, mapping: any) {
212
245
  for (const sourceKey in mapping) {
213
246
  const sourceValue = source[sourceKey];
@@ -667,7 +700,24 @@ function createChannelProps(
667
700
  const getSecondaryText =
668
701
  secondaryField && getTextAccessor(secondaryField, data);
669
702
 
670
- result.pointType = `${result.pointType}+text`;
703
+ // For line/polygon tileset layers, deck.gl's VectorTileLayer can synthesize
704
+ // point labels at line midpoints / polygon centroids via `autoLabels`. The
705
+ // optional `uniqueIdProperty` dedupes features that span multiple tiles so
706
+ // each feature gets one label instead of one-per-tile.
707
+ const geometry = data.tilestats?.layers?.[0]?.geometry;
708
+ const isLineOrPolygon =
709
+ geometry === 'Polygon' ||
710
+ geometry === 'MultiPolygon' ||
711
+ geometry === 'Line' ||
712
+ geometry === 'LineString' ||
713
+ geometry === 'MultiLineString';
714
+ if (isLineOrPolygon && (layerType === 'tileset' || layerType === 'mvt')) {
715
+ const uniqueIdProperty = visConfig.textLabelUniqueIdField;
716
+ result.autoLabels = uniqueIdProperty ? {uniqueIdProperty} : true;
717
+ result.pointType = 'text';
718
+ } else {
719
+ result.pointType = `${result.pointType}+text`;
720
+ }
671
721
  result.textCharacterSet = 'auto';
672
722
  result.textFontFamily = 'Inter, sans';
673
723
  result.textFontSettings = {sdf: true};
@@ -106,11 +106,13 @@ export function configureSource({
106
106
  tileResolution,
107
107
  ...(queryParameters && {queryParameters}),
108
108
  } as QuerySourceOptions;
109
+ const {featureBbox} = dataset;
109
110
  const vectorOptions = {
110
111
  spatialDataColumn,
111
112
  ...(columns && {columns}),
112
113
  ...(filters && {filters}),
113
114
  ...(aggregationExp && {aggregationExp}),
115
+ ...(featureBbox && {featureBbox}),
114
116
  } as VectorTableSourceOptions;
115
117
 
116
118
  if (type === 'raster') {
@@ -80,6 +80,11 @@ export type VisConfig = {
80
80
  radiusAggregationExp?: string;
81
81
  radiusAggregationDomain?: [number, number];
82
82
 
83
+ sizeMinPixels?: number;
84
+ sizeMaxPixels?: number;
85
+ radiusScaleWithZoom?: boolean;
86
+ radiusReferenceZoom?: number;
87
+
83
88
  sizeAggregation?: string;
84
89
  sizeAggregationExp?: string;
85
90
  sizeAggregationDomain?: [number, number];
@@ -108,6 +113,7 @@ export type VisConfig = {
108
113
  colorBands?: RasterLayerConfigColorBand[];
109
114
 
110
115
  uniqueValuesColorRange?: ColorRange;
116
+ textLabelUniqueIdField?: string | null;
111
117
  };
112
118
 
113
119
  export type TextLabel = {
@@ -259,6 +265,7 @@ export type Dataset = {
259
265
  name?: string | null;
260
266
  spatialIndex?: string | null;
261
267
  exportToBucketAvailable?: boolean;
268
+ featureBbox?: boolean;
262
269
  };
263
270
 
264
271
  export type AttributeType = 'String' | 'Number' | 'Timestamp' | 'Boolean';
@@ -23,7 +23,15 @@ import type {
23
23
  export type VectorQuerySourceOptions = SourceOptions &
24
24
  QuerySourceOptions &
25
25
  FilterOptions &
26
- ColumnsOption;
26
+ ColumnsOption & {
27
+ /**
28
+ * If `true`, the server includes a `_carto_bbox` property on each polygon
29
+ * feature, containing the bounding box of the full (unclipped) geometry as
30
+ * a `"west,south,east,north"` string in WGS84. Used by clients to compute
31
+ * stable label positions for polygons that span multiple tiles.
32
+ */
33
+ featureBbox?: boolean;
34
+ };
27
35
 
28
36
  type UrlParameters = {
29
37
  columns?: string;
@@ -34,6 +42,7 @@ type UrlParameters = {
34
42
  q: string;
35
43
  queryParameters?: Record<string, unknown> | unknown[];
36
44
  aggregationExp?: string;
45
+ featureBbox?: boolean;
37
46
  };
38
47
 
39
48
  export type VectorQuerySourceResponse = TilejsonResult &
@@ -50,6 +59,7 @@ export const vectorQuerySource = async function (
50
59
  tileResolution = DEFAULT_TILE_RESOLUTION,
51
60
  queryParameters,
52
61
  aggregationExp,
62
+ featureBbox,
53
63
  } = options;
54
64
 
55
65
  const spatialDataType = 'geo';
@@ -73,6 +83,9 @@ export const vectorQuerySource = async function (
73
83
  if (aggregationExp) {
74
84
  urlParameters.aggregationExp = aggregationExp;
75
85
  }
86
+ if (featureBbox) {
87
+ urlParameters.featureBbox = true;
88
+ }
76
89
 
77
90
  return baseSource<UrlParameters>('query', options, urlParameters).then(
78
91
  (result) => ({
@@ -23,7 +23,15 @@ import type {
23
23
  export type VectorTableSourceOptions = SourceOptions &
24
24
  TableSourceOptions &
25
25
  FilterOptions &
26
- ColumnsOption;
26
+ ColumnsOption & {
27
+ /**
28
+ * If `true`, the server includes a `_carto_bbox` property on each polygon
29
+ * feature, containing the bounding box of the full (unclipped) geometry as
30
+ * a `"west,south,east,north"` string in WGS84. Used by clients to compute
31
+ * stable label positions for polygons that span multiple tiles.
32
+ */
33
+ featureBbox?: boolean;
34
+ };
27
35
 
28
36
  type UrlParameters = {
29
37
  columns?: string;
@@ -33,6 +41,7 @@ type UrlParameters = {
33
41
  tileResolution?: string;
34
42
  name: string;
35
43
  aggregationExp?: string;
44
+ featureBbox?: boolean;
36
45
  };
37
46
 
38
47
  export type VectorTableSourceResponse = TilejsonResult &
@@ -48,6 +57,7 @@ export const vectorTableSource = async function (
48
57
  tableName,
49
58
  tileResolution = DEFAULT_TILE_RESOLUTION,
50
59
  aggregationExp,
60
+ featureBbox,
51
61
  } = options;
52
62
 
53
63
  const spatialDataType = 'geo';
@@ -68,6 +78,9 @@ export const vectorTableSource = async function (
68
78
  if (aggregationExp) {
69
79
  urlParameters.aggregationExp = aggregationExp;
70
80
  }
81
+ if (featureBbox) {
82
+ urlParameters.featureBbox = true;
83
+ }
71
84
 
72
85
  return baseSource<UrlParameters>('table', options, urlParameters).then(
73
86
  (result) => ({