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

keplergl / kepler.gl / 23965535588

03 Apr 2026 11:07PM UTC coverage: 59.873% (-1.8%) from 61.699%
23965535588

push

github

web-flow
chore: deck.gl 9.2 upgrade & loaders.gl, luma.gl upgrades (#3271)

* chore: upgrade to deckgl 9.2.11

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

* fixes

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

* fix lint follow up

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

* fix blending

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

* fix geojson layer

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

* more fixes for aggregation layers

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

* fix h3 layer

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

* potential fix for issues with geoarrow in point and line layers

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

* fix postprocessing effects

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

* fixes for light and shadow effect

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

* shadow and light effect - restore uniform shadow during the nighttime

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

* don't hide line and arc layers when layer type changed

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

* restore filters for aggregation layers

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

* fix aggregation layers - hightlight outlines

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

* fixes for raster tile layer - raster pmtiles related

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

* fixes for raster tiles shader modules updates

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

* fix lint

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

* more lint

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

* try to fix CI lint

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

* try to fix CI tests

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

* install webgpu

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

* try to fix Ci test env setup

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

* cont fix setup browser env

Signed-o... (continued)

6517 of 12977 branches covered (50.22%)

Branch coverage included in aggregate %.

324 of 1031 new or added lines in 58 files covered. (31.43%)

123 existing lines in 18 files now uncovered.

13313 of 20143 relevant lines covered (66.09%)

78.43 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 * as arrow from 'apache-arrow';
9
import * as ga from '@geoarrow/geoarrow-js';
10
import {AccessorContext, AccessorFunction, _InternalAccessorContext} from '../types';
11
import {isArrowFixedSizeList, isArrowStruct, isArrowVector} from '@kepler.gl/utils';
12

13
export function assert(condition: unknown, message?: string): asserts condition {
NEW
14
  if (!condition) throw new Error(message || 'assert');
×
15
}
16

17
export type TypedArray =
18
  | Uint8Array
19
  | Uint8ClampedArray
20
  | Uint16Array
21
  | Uint32Array
22
  | Int8Array
23
  | Int16Array
24
  | Int32Array
25
  | Float32Array
26
  | Float64Array;
27

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

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

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

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

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

85
    const childDataType = new arrow.Float64();
×
86
    const dataType = new arrow.FixedSizeList(2, new arrow.Field('coords', childDataType));
×
87

88
    const interleavedCoordsData = arrow.makeData({
×
89
      type: childDataType,
90
      length: interleavedCoords.length
91
    });
92

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

103
  assert(false);
×
104
}
105

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

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

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

165
  if (propInput === undefined) {
×
166
    return;
×
167
  }
168

169
  if (isArrowVector(propInput)) {
×
170
    const columnData = propInput.data[chunkIdx];
×
171

172
    if (arrow.DataType.isFixedSizeList(columnData)) {
×
173
      assert(columnData.children.length === 1);
×
174
      let values = columnData.children[0].values;
×
175

176
      if (geomCoordOffsets) {
×
177
        values = expandArrayToCoords(values, columnData.type.listSize, geomCoordOffsets);
×
178
      }
179

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

191
      if (geomCoordOffsets) {
×
192
        values = expandArrayToCoords(values, 1, geomCoordOffsets);
×
193
      }
194

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

207
      return wrapAccessorFunction(objectInfo, propInput, batchOffset);
×
208
    };
209
  } else {
210
    props[propName] = propInput;
×
211
  }
212
}
213

214
/**
215
 * Expand an array from "one element per geometry" to "one element per coordinate"
216
 *
217
 * @param input: the input array to expand
218
 * @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.
219
 * @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.
220
 * @param numPositions : end position in geomOffsets, as geomOffsets can potentially contain preallocated zeroes in the end of the buffer.
221
 *
222
 * @return  {TypedArray} values expanded to be per-coordinate
223
 */
224
export function expandArrayToCoords<T extends TypedArray>(
225
  input: T,
226
  size: number,
227
  geomOffsets: Int32Array,
228
  numPositions?: number
229
): T {
230
  const lastIndex = numPositions || geomOffsets.length - 1;
×
231
  const numCoords = geomOffsets[lastIndex];
×
232
  // @ts-expect-error
233
  const outputArray: T = new input.constructor(numCoords * size);
×
234

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

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

254
  return outputArray;
×
255
}
256

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

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

271
  return table.getChildAt(geometryColumnIdx);
×
272
}
273

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

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

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

295
  return resolvedRingOffsets;
×
296
}
297

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

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

310
  return resolvedRingOffsets;
×
311
}
312

313
export function getMultiPolygonResolvedOffsets(data: ga.data.MultiPolygonData): Int32Array {
314
  const polygonData = ga.child.getMultiPolygonChild(data);
×
315
  const ringData = ga.child.getPolygonChild(polygonData);
×
316

317
  const geomOffsets = data.valueOffsets;
×
318
  const polygonOffsets = polygonData.valueOffsets;
×
319
  const ringOffsets = ringData.valueOffsets;
×
320

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

326
  return resolvedRingOffsets;
×
327
}
328

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

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

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

351
  return invertedOffsets;
×
352
}
353

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

366
    if (key.startsWith('get')) {
×
367
      accessors[key] = value;
×
368
    } else {
369
      otherProps[key] = value;
×
370
    }
371
  }
372

373
  return [accessors, otherProps];
×
374
}
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