• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

keplergl / kepler.gl / 19768106976

28 Nov 2025 03:32PM UTC coverage: 61.675% (-0.09%) from 61.76%
19768106976

push

github

web-flow
chore: patch release 3.2.3 (#3250)

* draft

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

* patch

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

* fix eslint during release

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

---------

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

6352 of 12229 branches covered (51.94%)

Branch coverage included in aggregate %.

13043 of 19218 relevant lines covered (67.87%)

81.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/deckgl-arrow-layers/src/utils/utils.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
// deck.gl-community
5
// SPDX-License-Identifier: MIT
6
// Copyright (c) vis.gl contributors
7

8
import {assert} from '@deck.gl/core/typed';
9
import * as arrow from 'apache-arrow';
10
import * as ga from '@geoarrow/geoarrow-js';
11
import {AccessorContext, AccessorFunction, _InternalAccessorContext} from '../types';
12
import {isArrowFixedSizeList, isArrowStruct, isArrowVector} from '@kepler.gl/utils';
13

14
export type TypedArray =
15
  | Uint8Array
16
  | Uint8ClampedArray
17
  | Uint16Array
18
  | Uint32Array
19
  | Int8Array
20
  | Int16Array
21
  | Int32Array
22
  | Float32Array
23
  | Float64Array;
24

25
export function findGeometryColumnIndex(
26
  schema: arrow.Schema,
27
  extensionName: string,
28
  geometryColumnName?: string | null
29
): number | null {
30
  const index = schema.fields.findIndex(
×
31
    field =>
32
      field.name === geometryColumnName ||
×
33
      field.metadata.get('ARROW:extension:name') === extensionName
34
  );
35
  return index !== -1 ? index : null;
×
36
}
37

38
/**
39
 * Returns `true` if the input is a reference to a column in the table
40
 */
41
export function isColumnReference(input: any): input is string {
42
  return typeof input === 'string';
×
43
}
44

45
function isDataInterleavedCoords(
46
  data: arrow.Data
47
): data is arrow.Data<arrow.FixedSizeList<arrow.Float64>> {
48
  // TODO: also check 2 or 3d? Float64?
49
  return isArrowFixedSizeList(data.type);
×
50
}
51

52
function isDataSeparatedCoords(
53
  data: arrow.Data
54
): data is arrow.Data<arrow.Struct<{x: arrow.Float64; y: arrow.Float64}>> {
55
  // TODO: also check child names? Float64?
56
  return isArrowStruct(data.type);
×
57
}
58

59
/**
60
 * Convert geoarrow Struct coordinates to FixedSizeList coords
61
 *
62
 * The GeoArrow spec allows for either separated or interleaved coords, but at
63
 * this time deck.gl only supports interleaved.
64
 */
65
// TODO: this hasn't been tested yet
66
export function convertStructToFixedSizeList(
67
  coords:
68
    | arrow.Data<arrow.FixedSizeList<arrow.Float64>>
69
    | arrow.Data<arrow.Struct<{x: arrow.Float64; y: arrow.Float64}>>
70
): arrow.Data<arrow.FixedSizeList<arrow.Float64>> {
71
  if (isDataInterleavedCoords(coords)) {
×
72
    return coords;
×
73
  } else if (isDataSeparatedCoords(coords)) {
×
74
    // TODO: support 3d
75
    const interleavedCoords = new Float64Array(coords.length * 2);
×
76
    const [xChild, yChild] = coords.children;
×
77
    for (let i = 0; i < coords.length; i++) {
×
78
      interleavedCoords[i * 2] = xChild.values[i];
×
79
      interleavedCoords[i * 2 + 1] = yChild.values[i];
×
80
    }
81

82
    const childDataType = new arrow.Float64();
×
83
    const dataType = new arrow.FixedSizeList(2, new arrow.Field('coords', childDataType));
×
84

85
    const interleavedCoordsData = arrow.makeData({
×
86
      type: childDataType,
87
      length: interleavedCoords.length
88
    });
89

90
    const data = arrow.makeData({
×
91
      type: dataType,
92
      length: coords.length,
93
      nullCount: coords.nullCount,
94
      nullBitmap: coords.nullBitmap,
95
      child: interleavedCoordsData
96
    });
97
    return data;
×
98
  }
99

100
  assert(false);
×
101
}
102

103
type AssignAccessorProps = {
104
  /** The object on which to assign the resolved accesor */
105
  props: Record<string, any>;
106
  /** The name of the prop to set */
107
  propName: string;
108
  /** The user-supplied input to the layer. Must either be a scalar value or a reference to a column in the table. */
109
  propInput: any;
110
  /** Numeric index in the table */
111
  chunkIdx: number;
112
  /** a map from the geometry index to the coord offsets for that geometry. */
113
  geomCoordOffsets?: Int32Array | null;
114
  /** Absolute offset of the batch in the table/vector. Added to the sampling index. */
115
  batchOffset?: number;
116
};
117

118
/**
119
 * A wrapper around a user-provided accessor function
120
 *
121
 * For layers like Scatterplot, Path, and Polygon, we automatically handle
122
 * "exploding" the table when multi-geometry input are provided. This means that
123
 * the upstream `index` value passed to the user will be the correct row index
124
 * _only_ for non-exploded data.
125
 *
126
 * With this function, we simplify the user usage by automatically converting
127
 * back from "exploded" index back to the original row index.
128
 */
129
function wrapAccessorFunction<In, Out>(
130
  objectInfo: _InternalAccessorContext<In>,
131
  userAccessorFunction: AccessorFunction<In, Out>,
132
  batchOffset: number
133
): Out {
134
  const {index, data} = objectInfo;
×
135
  let newIndex = index + batchOffset;
×
136
  if (data.invertedGeomOffsets !== undefined) {
×
137
    newIndex = data.invertedGeomOffsets[index];
×
138
  }
139
  const newObjectData = {
×
140
    data: data.data,
141
    length: data.length,
142
    attributes: data.attributes
143
  };
144
  const newObjectInfo = {
×
145
    index: newIndex,
146
    data: newObjectData,
147
    target: objectInfo.target
148
  };
149
  return userAccessorFunction(newObjectInfo);
×
150
}
151

152
/**
153
 * Resolve accessor and assign to props object
154
 *
155
 * This is useful as a helper function because a scalar prop is set at the top
156
 * level while a vectorized prop is set inside data.attributes
157
 *
158
 */
159
export function assignAccessor(args: AssignAccessorProps) {
160
  const {props, propName, propInput, chunkIdx, geomCoordOffsets, batchOffset = 0} = args;
×
161

162
  if (propInput === undefined) {
×
163
    return;
×
164
  }
165

166
  if (isArrowVector(propInput)) {
×
167
    const columnData = propInput.data[chunkIdx];
×
168

169
    if (arrow.DataType.isFixedSizeList(columnData)) {
×
170
      assert(columnData.children.length === 1);
×
171
      let values = columnData.children[0].values;
×
172

173
      if (geomCoordOffsets) {
×
174
        values = expandArrayToCoords(values, columnData.type.listSize, geomCoordOffsets);
×
175
      }
176

177
      props.data.attributes[propName] = {
×
178
        value: values,
179
        size: columnData.type.listSize,
180
        // Set to `true` to signify that colors are already 0-255, and deck/luma
181
        // does not need to rescale
182
        // https://github.com/visgl/deck.gl/blob/401d624c0529faaa62125714c376b3ba3b8f379f/docs/api-reference/core/attribute-manager.md?plain=1#L66
183
        normalized: true
184
      };
185
    } else if (arrow.DataType.isFloat(columnData)) {
×
186
      let values = columnData.values;
×
187

188
      if (geomCoordOffsets) {
×
189
        values = expandArrayToCoords(values, 1, geomCoordOffsets);
×
190
      }
191

192
      props.data.attributes[propName] = {
×
193
        value: values,
194
        size: 1
195
      };
196
    }
197
  } else if (typeof propInput === 'function') {
×
198
    props[propName] = <In>(object: any, objectInfo: AccessorContext<In>) => {
×
199
      // Special case that doesn't have the same parameters
200
      if (propName === 'getPolygonOffset') {
×
201
        return propInput(object, objectInfo);
×
202
      }
203

204
      return wrapAccessorFunction(objectInfo, propInput, batchOffset);
×
205
    };
206
  } else {
207
    props[propName] = propInput;
×
208
  }
209
}
210

211
/**
212
 * Expand an array from "one element per geometry" to "one element per coordinate"
213
 *
214
 * @param input: the input array to expand
215
 * @param size : the number of nested elements in the input array per geometry. So for example, for RGB data this would be 3, for RGBA this would be 4. For radius, this would be 1.
216
 * @param geomOffsets : an offsets array mapping from the geometry to the coordinate indexes. So in the case of a LineStringArray, this is retrieved directly from the GeoArrow storage. In the case of a PolygonArray, this comes from the resolved indexes that need to be given to the SolidPolygonLayer anyways.
217
 * @param numPositions : end position in geomOffsets, as geomOffsets can potentially contain preallocated zeroes in the end of the buffer.
218
 *
219
 * @return  {TypedArray} values expanded to be per-coordinate
220
 */
221
export function expandArrayToCoords<T extends TypedArray>(
222
  input: T,
223
  size: number,
224
  geomOffsets: Int32Array,
225
  numPositions?: number
226
): T {
227
  const lastIndex = numPositions || geomOffsets.length - 1;
×
228
  const numCoords = geomOffsets[lastIndex];
×
229
  // @ts-expect-error
230
  const outputArray: T = new input.constructor(numCoords * size);
×
231

232
  // geomIdx is an index into the geomOffsets array
233
  // geomIdx is also the geometry/table index
234
  for (let geomIdx = 0; geomIdx < lastIndex; geomIdx++) {
×
235
    // geomOffsets maps from the geometry index to the coord index
236
    // So here we get the range of coords that this geometry covers
237
    const lastCoordIdx = geomOffsets[geomIdx];
×
238
    const nextCoordIdx = geomOffsets[geomIdx + 1];
×
239

240
    // Iterate over this range of coord indices
241
    for (let coordIdx = lastCoordIdx; coordIdx < nextCoordIdx; coordIdx++) {
×
242
      // Iterate over size
243
      for (let i = 0; i < size; i++) {
×
244
        // Copy from the geometry index in `input` to the coord index in
245
        // `output`
246
        outputArray[coordIdx * size + i] = input[geomIdx * size + i];
×
247
      }
248
    }
249
  }
250

251
  return outputArray;
×
252
}
253

254
/**
255
 * Get a geometry vector with the specified extension type name from the table.
256
 */
257
export function getGeometryVector(
258
  table: arrow.Table,
259
  geoarrowTypeName: string
260
): arrow.Vector | null {
261
  const geometryColumnIdx = findGeometryColumnIndex(table.schema, geoarrowTypeName);
×
262

263
  if (geometryColumnIdx === null) {
×
264
    return null;
×
265
    // throw new Error(`No column found with extension type ${geoarrowTypeName}`);
266
  }
267

268
  return table.getChildAt(geometryColumnIdx);
×
269
}
270

271
export function getListNestingLevels(data: arrow.Data): number {
272
  let nestingLevels = 0;
×
273
  if (arrow.DataType.isList(data.type)) {
×
274
    nestingLevels += 1;
×
275
    data = data.children[0];
×
276
  }
277
  return nestingLevels;
×
278
}
279

280
export function getMultiLineStringResolvedOffsets(data: ga.data.MultiLineStringData): Int32Array {
281
  const geomOffsets = data.valueOffsets;
×
282
  const lineStringData = ga.child.getMultiLineStringChild(data);
×
283
  const ringOffsets = lineStringData.valueOffsets;
×
284

285
  const resolvedRingOffsets = new Int32Array(geomOffsets.length);
×
286
  for (let i = 0; i < resolvedRingOffsets.length; ++i) {
×
287
    // Perform the lookup into the ringIndices array using the geomOffsets
288
    // array
289
    resolvedRingOffsets[i] = ringOffsets[geomOffsets[i]];
×
290
  }
291

292
  return resolvedRingOffsets;
×
293
}
294

295
export function getPolygonResolvedOffsets(data: ga.data.PolygonData): Int32Array {
296
  const geomOffsets = data.valueOffsets;
×
297
  const ringData = ga.child.getPolygonChild(data);
×
298
  const ringOffsets = ringData.valueOffsets;
×
299

300
  const resolvedRingOffsets = new Int32Array(geomOffsets.length);
×
301
  for (let i = 0; i < resolvedRingOffsets.length; ++i) {
×
302
    // Perform the lookup into the ringIndices array using the geomOffsets
303
    // array
304
    resolvedRingOffsets[i] = ringOffsets[geomOffsets[i]];
×
305
  }
306

307
  return resolvedRingOffsets;
×
308
}
309

310
export function getMultiPolygonResolvedOffsets(data: ga.data.MultiPolygonData): Int32Array {
311
  const polygonData = ga.child.getMultiPolygonChild(data);
×
312
  const ringData = ga.child.getPolygonChild(polygonData);
×
313

314
  const geomOffsets = data.valueOffsets;
×
315
  const polygonOffsets = polygonData.valueOffsets;
×
316
  const ringOffsets = ringData.valueOffsets;
×
317

318
  const resolvedRingOffsets = new Int32Array(geomOffsets.length);
×
319
  for (let i = 0; i < resolvedRingOffsets.length; ++i) {
×
320
    resolvedRingOffsets[i] = ringOffsets[polygonOffsets[geomOffsets[i]]];
×
321
  }
322

323
  return resolvedRingOffsets;
×
324
}
325

326
/**
327
 * Invert offsets so that lookup can go in the opposite direction
328
 */
329
export function invertOffsets(offsets: Int32Array): Uint8Array | Uint16Array | Uint32Array {
330
  const largestOffset = offsets[offsets.length - 1];
×
331

332
  const arrayConstructor =
333
    offsets.length < Math.pow(2, 8)
×
334
      ? Uint8Array
335
      : offsets.length < Math.pow(2, 16)
×
336
      ? Uint16Array
337
      : Uint32Array;
338

339
  const invertedOffsets = new arrayConstructor(largestOffset);
×
340
  for (let arrayIdx = 0; arrayIdx < offsets.length - 1; arrayIdx++) {
×
341
    const thisOffset = offsets[arrayIdx];
×
342
    const nextOffset = offsets[arrayIdx + 1];
×
343
    for (let offset = thisOffset; offset < nextOffset; offset++) {
×
344
      invertedOffsets[offset] = arrayIdx;
×
345
    }
346
  }
347

348
  return invertedOffsets;
×
349
}
350

351
// TODO: better typing
352
export function extractAccessorsFromProps(
353
  props: Record<string, any>,
354
  excludeKeys: string[]
355
): [Record<string, any>, Record<string, any>] {
356
  const accessors: Record<string, any> = {};
×
357
  const otherProps: Record<string, any> = {};
×
358
  for (const [key, value] of Object.entries(props)) {
×
359
    if (excludeKeys.includes(key)) {
×
360
      continue;
×
361
    }
362

363
    if (key.startsWith('get')) {
×
364
      accessors[key] = value;
×
365
    } else {
366
      otherProps[key] = value;
×
367
    }
368
  }
369

370
  return [accessors, otherProps];
×
371
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc