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

keplergl / kepler.gl / 25351425939

05 May 2026 12:36AM UTC coverage: 58.829% (+0.03%) from 58.796%
25351425939

Pull #3414

github

web-flow
Merge 9769e89aa into 9c7af408f
Pull Request #3414: feat(processors): auto-detect delimiter for CSV/TSV/DSV files

6977 of 14243 branches covered (48.99%)

Branch coverage included in aggregate %.

17 of 17 new or added lines in 2 files covered. (100.0%)

51 existing lines in 1 file now uncovered.

14338 of 21989 relevant lines covered (65.21%)

79.6 hits per line

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

51.42
/src/processors/src/data-processor.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import * as arrow from 'apache-arrow';
5
import {csvParseRows, tsvParseRows, dsvFormat} from 'd3-dsv';
6
import {DATA_TYPES as AnalyzerDATA_TYPES} from 'type-analyzer';
7
import normalize from '@mapbox/geojson-normalize';
8
import {parseSync} from '@loaders.gl/core';
9
import {ArrowTable} from '@loaders.gl/schema';
10
import {WKBLoader} from '@loaders.gl/wkt';
11

12
import {
13
  ALL_FIELD_TYPES,
14
  DATASET_FORMATS,
15
  GEOARROW_EXTENSIONS,
16
  GEOARROW_METADATA_KEY,
17
  GUIDES_FILE_FORMAT_DOC
18
} from '@kepler.gl/constants';
19
import {ProcessorResult, Field} from '@kepler.gl/types';
20
import {
21
  arrowDataTypeToAnalyzerDataType,
22
  arrowDataTypeToFieldType,
23
  hasOwnProperty,
24
  isPlainObject
25
} from '@kepler.gl/utils';
26
import {
27
  analyzerTypeToFieldType,
28
  getSampleForTypeAnalyze,
29
  getSampleForTypeAnalyzeArrow,
30
  getFieldsFromData,
31
  h3IsValid,
32
  notNullorUndefined,
33
  toArray
34
} from '@kepler.gl/common-utils';
35
import {KeplerGlSchema, ParsedDataset, SavedMap, LoadedMap} from '@kepler.gl/schemas';
36
import {Feature} from '@deck.gl-community/editable-layers';
37

38
// if any of these value occurs in csv, parse it to null;
39
// const CSV_NULLS = ['', 'null', 'NULL', 'Null', 'NaN', '/N'];
40
// matches empty string
41
export const CSV_NULLS = /^(null|NULL|Null|NaN|\/N||)$/;
13✔
42

43
const SUPPORTED_DELIMITERS = [',', '\t', ';', '|'] as const;
13✔
44

45
/**
46
 * Detect the delimiter used in a DSV string by checking the first line.
47
 * Returns the delimiter that produces the most columns (minimum 2).
48
 * Falls back to comma if no delimiter produces multiple columns.
49
 */
50
export function detectDelimiter(rawData: string): string {
51
  const newlineIdx = rawData.indexOf('\n');
71✔
52
  const firstLine = newlineIdx === -1 ? rawData : rawData.slice(0, newlineIdx);
71✔
53
  if (!firstLine) return ',';
71✔
54

55
  let bestDelimiter = ',';
69✔
56
  let bestCount = 1;
69✔
57

58
  for (const delimiter of SUPPORTED_DELIMITERS) {
69✔
59
    const parseRows =
60
      delimiter === ','
276✔
61
        ? csvParseRows
62
        : delimiter === '\t'
207✔
63
          ? tsvParseRows
64
          : dsvFormat(delimiter).parseRows;
65
    const parsed = parseRows(firstLine);
276✔
66
    const count = parsed[0]?.length || 0;
276!
67
    if (count > bestCount) {
276✔
68
      bestCount = count;
72✔
69
      bestDelimiter = delimiter;
72✔
70
    }
71
  }
72

73
  return bestDelimiter;
69✔
74
}
75

76
function tryParseJsonString(str) {
77
  try {
31✔
78
    return JSON.parse(str);
31✔
79
  } catch (e) {
80
    return null;
×
81
  }
82
}
83

84
export const PARSE_FIELD_VALUE_FROM_STRING = {
13✔
85
  [ALL_FIELD_TYPES.boolean]: {
86
    valid: (d: unknown): boolean => typeof d === 'boolean',
31✔
87
    parse: (d: unknown): boolean => {
88
      const s = String(d).toLowerCase();
387✔
89
      return s === 'true' || s === 'yes' || s === '1';
387✔
90
    }
91
  },
92
  [ALL_FIELD_TYPES.integer]: {
93
    // @ts-ignore
94
    valid: (d: unknown): boolean => parseInt(d, 10) === d,
140✔
95
    // @ts-ignore
96
    parse: (d: unknown): number => parseInt(d, 10)
540✔
97
  },
98
  [ALL_FIELD_TYPES.timestamp]: {
99
    valid: (d: unknown, field: Field): boolean =>
100
      ['x', 'X'].includes(field.format) ? typeof d === 'number' : typeof d === 'string',
106✔
101
    parse: (d: any, field: Field) => (['x', 'X'].includes(field.format) ? Number(d) : d)
386!
102
  },
103
  [ALL_FIELD_TYPES.real]: {
104
    // @ts-ignore
105
    valid: (d: unknown): boolean => parseFloat(d) === d,
130✔
106
    // Note this will result in NaN for some string
107
    parse: parseFloat
108
  },
109
  [ALL_FIELD_TYPES.object]: {
110
    valid: isPlainObject,
111
    parse: tryParseJsonString
112
  },
113

114
  [ALL_FIELD_TYPES.array]: {
115
    valid: Array.isArray,
116
    parse: tryParseJsonString
117
  },
118

119
  [ALL_FIELD_TYPES.h3]: {
120
    valid: d => h3IsValid(d),
15✔
121
    parse: d => d
×
122
  }
123
};
124

125
/**
126
 * Process csv data, output a data object with `{fields: [], rows: []}`.
127
 * The data object can be wrapped in a `dataset` and pass to [`addDataToMap`](../actions/actions.md#adddatatomap)
128
 * @param rawData raw csv string
129
 * @returns data object `{fields: [], rows: []}` can be passed to addDataToMaps
130
 * @public
131
 * @example
132
 * import {processCsvData} from '@kepler.gl/processors';
133
 *
134
 * const testData = `gps_data.utc_timestamp,gps_data.lat,gps_data.lng,gps_data.types,epoch,has_result,id,time,begintrip_ts_utc,begintrip_ts_local,date
135
 * 2016-09-17 00:09:55,29.9900937,31.2590542,driver_analytics,1472688000000,False,1,2016-09-23T00:00:00.000Z,2016-10-01 09:41:39+00:00,2016-10-01 09:41:39+00:00,2016-09-23
136
 * 2016-09-17 00:10:56,29.9927699,31.2461142,driver_analytics,1472688000000,False,2,2016-09-23T00:00:00.000Z,2016-10-01 09:46:37+00:00,2016-10-01 16:46:37+00:00,2016-09-23
137
 * 2016-09-17 00:11:56,29.9907261,31.2312742,driver_analytics,1472688000000,False,3,2016-09-23T00:00:00.000Z,,,2016-09-23
138
 * 2016-09-17 00:12:58,29.9870074,31.2175827,driver_analytics,1472688000000,False,4,2016-09-23T00:00:00.000Z,,,2016-09-23`
139
 *
140
 * const dataset = {
141
 *  info: {id: 'test_data', label: 'My Csv'},
142
 *  data: processCsvData(testData)
143
 * };
144
 *
145
 * dispatch(addDataToMap({
146
 *  datasets: [dataset],
147
 *  options: {centerMap: true, readOnly: true}
148
 * }));
149
 */
150
export function processCsvData(rawData: unknown[][] | string, header?: string[]): ProcessorResult {
151
  let rows: unknown[][] | undefined;
152
  let headerRow: string[] | undefined;
153

154
  if (typeof rawData === 'string') {
89✔
155
    const delimiter = detectDelimiter(rawData);
53✔
156
    const parseRows =
157
      delimiter === ','
53✔
158
        ? csvParseRows
159
        : delimiter === '\t'
12✔
160
          ? tsvParseRows
161
          : dsvFormat(delimiter).parseRows;
162
    const parsedRows: string[][] = parseRows(rawData);
53✔
163

164
    if (!Array.isArray(parsedRows) || parsedRows.length < 2) {
53✔
165
      // looks like an empty file, throw error to be catch
166
      throw new Error('process Csv Data Failed: CSV is empty');
1✔
167
    }
168
    headerRow = parsedRows[0];
52✔
169
    rows = parsedRows.slice(1);
52✔
170
  } else if (Array.isArray(rawData) && rawData.length) {
36!
171
    rows = rawData;
36✔
172
    headerRow = header;
36✔
173

174
    if (!Array.isArray(headerRow)) {
36!
175
      // if data is passed in as array of rows and missing header
176
      // assume first row is header
177
      // @ts-ignore
UNCOV
178
      headerRow = rawData[0];
×
UNCOV
179
      rows = rawData.slice(1);
×
180
    }
181
  }
182

183
  if (!rows || !headerRow) {
88!
UNCOV
184
    throw new Error('invalid input passed to processCsvData');
×
185
  }
186

187
  // here we assume the csv file that people uploaded will have first row
188
  // as name of the column
189

190
  cleanUpFalsyCsvValue(rows);
88✔
191
  // No need to run type detection on every data point
192
  // here we get a list of none null values to run analyze on
193
  const sample = getSampleForTypeAnalyze({fields: headerRow, rows});
88✔
194
  const fields = getFieldsFromData(sample, headerRow);
88✔
195
  const parsedRows = parseRowsByFields(rows, fields);
88✔
196

197
  return {fields, rows: parsedRows};
88✔
198
}
199

200
/**
201
 * Parse rows of csv by analyzed field types. So that `'1'` -> `1`, `'True'` -> `true`
202
 * @param rows
203
 * @param fields
204
 */
205
export function parseRowsByFields(rows: any[][], fields: Field[]) {
206
  // Edit rows in place
207
  const geojsonFieldIdx = fields.findIndex(f => f.name === '_geojson');
504✔
208
  fields.forEach(parseCsvRowsByFieldType.bind(null, rows, geojsonFieldIdx));
88✔
209

210
  return rows;
88✔
211
}
212

213
/**
214
 * Convert falsy value in csv including `'', 'null', 'NULL', 'Null', 'NaN'` to `null`,
215
 * so that type-analyzer won't detect it as string
216
 *
217
 * @param rows
218
 */
219
function cleanUpFalsyCsvValue(rows: unknown[][]): void {
220
  const re = new RegExp(CSV_NULLS, 'g');
124✔
221
  for (let i = 0; i < rows.length; i++) {
124✔
222
    for (let j = 0; j < rows[i].length; j++) {
1,086✔
223
      // analyzer will set any fields to 'string' if there are empty values
224
      // which will be parsed as '' by d3.csv
225
      // here we parse empty data as null
226
      // TODO: create warning when deltect `CSV_NULLS` in the data
227
      if (typeof rows[i][j] === 'string' && (rows[i][j] as string).match(re)) {
8,858✔
228
        rows[i][j] = null;
959✔
229
      }
230
    }
231
  }
232
}
233

234
/**
235
 * Process uploaded csv file to parse value by field type
236
 *
237
 * @param rows
238
 * @param geoFieldIdx field index
239
 * @param field
240
 * @param i
241
 */
242
export function parseCsvRowsByFieldType(
243
  rows: unknown[][],
244
  geoFieldIdx: number,
245
  field: Field,
246
  i: number
247
): void {
248
  const parser = PARSE_FIELD_VALUE_FROM_STRING[field.type];
628✔
249
  if (parser) {
628✔
250
    // check first not null value of it's already parsed
251
    const first = rows.find(r => notNullorUndefined(r[i]));
475✔
252
    if (!first || parser.valid(first[i], field)) {
451✔
253
      return;
215✔
254
    }
255
    rows.forEach(row => {
236✔
256
      // parse string value based on field type
257
      if (row[i] !== null) {
3,113✔
258
        row[i] = parser.parse(row[i], field);
2,792✔
259
        if (
2,792✔
260
          geoFieldIdx > -1 &&
2,810✔
261
          isPlainObject(row[geoFieldIdx]) &&
262
          // @ts-ignore
263
          hasOwnProperty(row[geoFieldIdx], 'properties')
264
        ) {
265
          // @ts-ignore
266
          row[geoFieldIdx].properties[field.name] = row[i];
9✔
267
        }
268
      }
269
    });
270
  }
271
}
272

273
/* eslint-enable complexity */
274

275
/**
276
 * Process data where each row is an object, output can be passed to [`addDataToMap`](../actions/actions.md#adddatatomap)
277
 * NOTE: This function may mutate input.
278
 * @param rawData an array of row object, each object should have the same number of keys
279
 * @returns dataset containing `fields` and `rows`
280
 * @public
281
 * @example
282
 * import {addDataToMap} from '@kepler.gl/actions';
283
 * import {processRowObject} from '@kepler.gl/processors';
284
 *
285
 * const data = [
286
 *  {lat: 31.27, lng: 127.56, value: 3},
287
 *  {lat: 31.22, lng: 126.26, value: 1}
288
 * ];
289
 *
290
 * dispatch(addDataToMap({
291
 *  datasets: {
292
 *    info: {label: 'My Data', id: 'my_data'},
293
 *    data: processRowObject(data)
294
 *  }
295
 * }));
296
 */
297
export function processRowObject(rawData: unknown[]): ProcessorResult {
298
  if (!Array.isArray(rawData)) {
37✔
299
    return null;
1✔
300
  } else if (!rawData.length) {
36!
301
    // data is empty
UNCOV
302
    return {
×
303
      fields: [],
304
      rows: []
305
    };
306
  }
307

308
  const firstRow = rawData[0] as Record<string, unknown>;
36✔
309
  const keys = Object.keys(firstRow); // [lat, lng, value]
36✔
310
  const rows = rawData.map(d => keys.map(key => (d as Record<string, unknown>)[key])); // [[31.27, 127.56, 3]]
1,551✔
311

312
  // row object can still contain values like `Null` or `N/A`
313
  cleanUpFalsyCsvValue(rows);
36✔
314

315
  return processCsvData(rows, keys);
36✔
316
}
317

318
/**
319
 * Process GeoJSON [`FeatureCollection`](http://wiki.geojson.org/GeoJSON_draft_version_6#FeatureCollection),
320
 * output a data object with `{fields: [], rows: []}`.
321
 * The data object can be wrapped in a `dataset` and passed to [`addDataToMap`](../actions/actions.md#adddatatomap)
322
 * NOTE: This function may mutate input.
323
 *
324
 * @param rawData raw geojson feature collection
325
 * @returns dataset containing `fields` and `rows`
326
 * @public
327
 * @example
328
 * import {addDataToMap} from '@kepler.gl/actions';
329
 * import {processGeojson} from '@kepler.gl/processors';
330
 *
331
 * const geojson = {
332
 *         "type" : "FeatureCollection",
333
 *         "features" : [{
334
 *                 "type" : "Feature",
335
 *                 "properties" : {
336
 *                         "capacity" : "10",
337
 *                         "type" : "U-Rack"
338
 *                 },
339
 *                 "geometry" : {
340
 *                         "type" : "Point",
341
 *                         "coordinates" : [ -71.073283, 42.417500 ]
342
 *                 }
343
 *         }]
344
 * };
345
 *
346
 * dispatch(addDataToMap({
347
 *  datasets: {
348
 *    info: {
349
 *      label: 'Sample Taxi Trips in New York City',
350
 *      id: 'test_trip_data'
351
 *    },
352
 *    data: processGeojson(geojson)
353
 *  }
354
 * }));
355
 */
356
export function processGeojson(rawData: unknown): ProcessorResult {
357
  const normalizedGeojson = normalize(rawData);
28✔
358

359
  if (!normalizedGeojson || !Array.isArray(normalizedGeojson.features)) {
28✔
360
    throw new Error(
1✔
361
      `Read File Failed: File is not a valid GeoJSON. Read more about [supported file format](${GUIDES_FILE_FORMAT_DOC})`
362
    );
363
  }
364

365
  // getting all feature fields
366
  const allDataRows: Array<{_geojson: Feature} & keyof Feature> = [];
27✔
367
  for (let i = 0; i < normalizedGeojson.features.length; i++) {
27✔
368
    const f = normalizedGeojson.features[i];
160✔
369
    if (f.geometry) {
160!
370
      allDataRows.push({
160✔
371
        // add feature to _geojson field
372
        _geojson: f,
373
        ...(f.properties || {})
161✔
374
      });
375
    }
376
  }
377
  // get all the field
378
  const fields = allDataRows.reduce<string[]>((accu, curr) => {
27✔
379
    Object.keys(curr).forEach(key => {
160✔
380
      if (!accu.includes(key)) {
807✔
381
        accu.push(key);
148✔
382
      }
383
    });
384
    return accu;
160✔
385
  }, []);
386

387
  // make sure each feature has exact same fields
388
  allDataRows.forEach(d => {
27✔
389
    fields.forEach(f => {
160✔
390
      if (!(f in d)) {
860✔
391
        d[f] = null;
53✔
392
        if (d._geojson.properties) {
53!
393
          d._geojson.properties[f] = null;
53✔
394
        }
395
      }
396
    });
397
  });
398

399
  return processRowObject(allDataRows);
27✔
400
}
401

402
/**
403
 * Process saved kepler.gl json to be pass to [`addDataToMap`](../actions/actions.md#adddatatomap).
404
 * The json object should contain `datasets` and `config`.
405
 * @param rawData
406
 * @param schema
407
 * @returns datasets and config `{datasets: {}, config: {}}`
408
 * @public
409
 * @example
410
 * import {addDataToMap} from '@kepler.gl/actions';
411
 * import {processKeplerglJSON} from '@kepler.gl/processors';
412
 *
413
 * dispatch(addDataToMap(processKeplerglJSON(keplerGlJson)));
414
 */
415
export function processKeplerglJSON(rawData: SavedMap, schema = KeplerGlSchema): LoadedMap | null {
5✔
416
  return rawData ? schema.load(rawData.datasets, rawData.config) : null;
5!
417
}
418

419
/**
420
 * Parse a single or an array of datasets saved using kepler.gl schema
421
 * @param rawData
422
 * @param schema
423
 */
424
export function processKeplerglDataset(
425
  rawData: object | object[],
426
  schema = KeplerGlSchema
×
427
): ParsedDataset | ParsedDataset[] | null {
UNCOV
428
  if (!rawData) {
×
429
    return null;
×
430
  }
431

UNCOV
432
  const results = schema.parseSavedData(toArray(rawData));
×
UNCOV
433
  if (!results) {
×
UNCOV
434
    return null;
×
435
  }
UNCOV
436
  return Array.isArray(rawData) ? results : results[0];
×
437
}
438

439
/**
440
 * Parse arrow table and return a dataset
441
 *
442
 * @param arrowTable ArrowTable to parse, see loaders.gl/schema
443
 * @returns dataset containing `fields` and `rows` or null
444
 */
445
export function processArrowTable(arrowTable: ArrowTable): ProcessorResult | null {
446
  // @ts-ignore - Unknown data type causing build failures
UNCOV
447
  return processArrowBatches(arrowTable.data.batches);
×
448
}
449

450
/**
451
 * Extracts GeoArrow metadata from an Apache Arrow table schema.
452
 * For geoparquet files geoarrow metadata isn't present in fields, so extract extra info from schema.
453
 * @param table The Apache Arrow table to extract metadata from.
454
 * @returns An object mapping column names to their GeoArrow encoding type.
455
 * @throws Logs an error message if parsing of metadata fails.
456
 */
457
export function getGeoArrowMetadataFromSchema(table: arrow.Table): Record<string, string> {
458
  const geoArrowMetadata: Record<string, string> = {};
×
459
  try {
×
460
    const geoString = table.schema.metadata?.get('geo');
×
UNCOV
461
    if (geoString) {
×
UNCOV
462
      const parsedGeoString = JSON.parse(geoString);
×
UNCOV
463
      if (parsedGeoString.columns) {
×
UNCOV
464
        Object.keys(parsedGeoString.columns).forEach(columnName => {
×
UNCOV
465
          const columnData = parsedGeoString.columns[columnName];
×
UNCOV
466
          if (columnData?.encoding === 'WKB') {
×
467
            geoArrowMetadata[columnName] = GEOARROW_EXTENSIONS.WKB;
×
468
          }
469
          // TODO potentially there are other types but no datasets to test
470
        });
471
      }
472
    }
473
  } catch (error) {
UNCOV
474
    console.error('An error during arrow table schema metadata parsing');
×
475
  }
UNCOV
476
  return geoArrowMetadata;
×
477
}
478

479
/**
480
 * Converts an Apache Arrow table schema into an array of Kepler.gl field objects.
481
 * @param table The Apache Arrow table whose schema needs to be converted.
482
 * @param fieldTypeSuggestions Optional mapping of field names to suggested field types.
483
 * @returns An array of field objects suitable for Kepler.gl.
484
 */
485
export function arrowSchemaToFields(
486
  table: arrow.Table,
487
  fieldTypeSuggestions: Record<string, string> = {}
×
488
): Field[] {
489
  const headerRow = table.schema.fields.map(f => f.name);
×
490
  const sample = getSampleForTypeAnalyzeArrow(table, headerRow);
×
UNCOV
491
  const keplerFields = getFieldsFromData(sample, headerRow);
×
492
  const geoArrowMetadata = getGeoArrowMetadataFromSchema(table);
×
493

UNCOV
494
  return table.schema.fields.map((field: arrow.Field, fieldIndex: number) => {
×
UNCOV
495
    let type = arrowDataTypeToFieldType(field.type);
×
496
    let analyzerType = arrowDataTypeToAnalyzerDataType(field.type);
×
497
    let format = '';
×
498

499
    const fieldTypeSuggestion = fieldTypeSuggestions[field.name];
×
UNCOV
500
    const keplerField = keplerFields[fieldIndex];
×
501

502
    // geometry fields produced by DuckDB's st_asgeojson()
503
    if (fieldTypeSuggestion === 'JSON') {
×
504
      type = ALL_FIELD_TYPES.geojson;
×
505
      analyzerType = AnalyzerDATA_TYPES.GEOMETRY_FROM_STRING;
×
506
    } else if (
×
507
      fieldTypeSuggestion === 'GEOMETRY' ||
×
508
      field.metadata.get(GEOARROW_METADATA_KEY)?.startsWith('geoarrow')
509
    ) {
UNCOV
510
      type = ALL_FIELD_TYPES.geoarrow;
×
511
      analyzerType = AnalyzerDATA_TYPES.GEOMETRY;
×
512
    } else if (geoArrowMetadata[field.name]) {
×
513
      type = ALL_FIELD_TYPES.geoarrow;
×
514
      analyzerType = AnalyzerDATA_TYPES.GEOMETRY;
×
515
      field.metadata?.set(GEOARROW_METADATA_KEY, geoArrowMetadata[field.name]);
×
516
    } else if (fieldTypeSuggestion === 'BLOB') {
×
517
      // When arrow wkb column saved to DuckDB as BLOB without any metadata, then queried back
518
      try {
×
UNCOV
519
        const data = table.getChildAt(fieldIndex)?.get(0);
×
UNCOV
520
        if (data) {
×
UNCOV
521
          const binaryGeo = parseSync(data, WKBLoader);
×
UNCOV
522
          if (binaryGeo) {
×
UNCOV
523
            type = ALL_FIELD_TYPES.geoarrow;
×
524
            analyzerType = AnalyzerDATA_TYPES.GEOMETRY;
×
UNCOV
525
            field.metadata?.set(GEOARROW_METADATA_KEY, GEOARROW_EXTENSIONS.WKB);
×
526
          }
527
        }
528
      } catch (error) {
529
        // ignore, not WKB
530
      }
531
    } else if (
×
532
      fieldTypeSuggestion === 'VARCHAR' &&
×
533
      (keplerField.analyzerType === AnalyzerDATA_TYPES.GEOMETRY ||
534
        keplerField.analyzerType === AnalyzerDATA_TYPES.GEOMETRY_FROM_STRING)
535
    ) {
536
      // When wkb/wkt was saved as varchar in DuckDB
UNCOV
537
      type = keplerField.type;
×
UNCOV
538
      analyzerType = keplerField.analyzerType;
×
UNCOV
539
      format = keplerField.format;
×
540
    } else if (fieldTypeSuggestion === 'VARCHAR' && keplerField.type === ALL_FIELD_TYPES.h3) {
×
541
      // when kepler detected h3 column using getFieldsFromData(), set type to h3 and analyzerType to H3
542
      type = ALL_FIELD_TYPES.h3;
×
543
      analyzerType = keplerField.analyzerType;
×
544
    } else {
545
      // TODO should we use Kepler getFieldsFromData instead
546
      // of arrowDataTypeToFieldType for all fields?
547
      if (keplerField.type === ALL_FIELD_TYPES.timestamp) {
×
UNCOV
548
        type = keplerField.type;
×
UNCOV
549
        analyzerType = keplerField.analyzerType;
×
UNCOV
550
        format = keplerField.format;
×
551
      }
552
    }
553

UNCOV
554
    return {
×
555
      ...field,
556
      name: field.name,
557
      id: field.name,
558
      displayName: field.name,
559
      format: format,
560
      fieldIdx: fieldIndex,
561
      type,
562
      analyzerType,
UNCOV
563
      valueAccessor: (dc: any) => d => {
×
UNCOV
564
        return dc.valueAt(d.index, fieldIndex);
×
565
      },
566
      metadata: field.metadata
567
    };
568
  });
569
}
570

571
const CAST_BIGINTS = false;
13✔
572

573
/**
574
 * Cast 64-bit integer Arrow columns (Int64, Uint64) to Float64 to avoid BigInt values
575
 * that are incompatible with d3 scales, sorting, and other numeric operations.
576
 * Mirrors the DuckDB approach of casting BIGINT/UBIGINT to DOUBLE.
577
 */
578
function castBigIntColumnsToFloat64(arrowTable: arrow.Table): arrow.Table {
579
  if (!CAST_BIGINTS) {
×
580
    return arrowTable;
×
581
  }
582

583
  const needsCast = arrowTable.schema.fields.some(
×
584
    f => arrow.DataType.isInt(f.type) && f.type.bitWidth === 64
×
585
  );
586
  if (!needsCast) {
×
587
    return arrowTable;
×
588
  }
589

590
  const newColumns: Record<string, arrow.Vector> = {};
×
591
  for (let i = 0; i < arrowTable.numCols; i++) {
×
UNCOV
592
    const field = arrowTable.schema.fields[i];
×
593
    const col = arrowTable.getChildAt(i)!;
×
UNCOV
594
    if (arrow.DataType.isInt(field.type) && field.type.bitWidth === 64) {
×
595
      const float64Array = new Float64Array(col.length);
×
UNCOV
596
      for (let j = 0; j < col.length; j++) {
×
UNCOV
597
        const val = col.get(j);
×
598
        float64Array[j] = val === null ? NaN : Number(val);
×
599
      }
UNCOV
600
      newColumns[field.name] = arrow.makeVector(float64Array);
×
601
    } else {
UNCOV
602
      newColumns[field.name] = col;
×
603
    }
604
  }
UNCOV
605
  return new arrow.Table(newColumns);
×
606
}
607

608
/**
609
 * Parse arrow batches returned from parseInBatches()
610
 *
611
 * @param arrowTable the arrow table to parse
612
 * @returns dataset containing `fields` and `rows` or null
613
 */
614
export function processArrowBatches(arrowBatches: arrow.RecordBatch[]): ProcessorResult | null {
UNCOV
615
  if (arrowBatches.length === 0) {
×
UNCOV
616
    return null;
×
617
  }
UNCOV
618
  const arrowTable = castBigIntColumnsToFloat64(new arrow.Table(arrowBatches));
×
UNCOV
619
  const fields = arrowSchemaToFields(arrowTable);
×
620

UNCOV
621
  const cols = [...Array(arrowTable.numCols).keys()].map(i => arrowTable.getChildAt(i));
×
622

623
  // return empty rows and use raw arrow table to construct column-wise data container
UNCOV
624
  return {
×
625
    fields,
626
    rows: [],
627
    cols,
628
    metadata: arrowTable.schema.metadata,
629
    // Save original arrow schema, for better ingestion into DuckDB.
630
    // TODO consider returning arrowTable in cols, not an array of Vectors from arrowTable.
631
    arrowSchema: arrowTable.schema
632
  };
633
}
634

635
export const DATASET_HANDLERS = {
13✔
636
  [DATASET_FORMATS.row]: processRowObject,
637
  [DATASET_FORMATS.geojson]: processGeojson,
638
  [DATASET_FORMATS.csv]: processCsvData,
639
  [DATASET_FORMATS.arrow]: processArrowTable,
640
  [DATASET_FORMATS.keplergl]: processKeplerglDataset
641
};
642

643
export const Processors: {
644
  processGeojson: typeof processGeojson;
645
  processCsvData: typeof processCsvData;
646
  processArrowTable: typeof processArrowTable;
647
  processArrowBatches: typeof processArrowBatches;
648
  processRowObject: typeof processRowObject;
649
  processKeplerglJSON: typeof processKeplerglJSON;
650
  processKeplerglDataset: typeof processKeplerglDataset;
651
  analyzerTypeToFieldType: typeof analyzerTypeToFieldType;
652
  getFieldsFromData: typeof getFieldsFromData;
653
  parseCsvRowsByFieldType: typeof parseCsvRowsByFieldType;
654
} = {
13✔
655
  processGeojson,
656
  processCsvData,
657
  processArrowTable,
658
  processArrowBatches,
659
  processRowObject,
660
  processKeplerglJSON,
661
  processKeplerglDataset,
662
  analyzerTypeToFieldType,
663
  getFieldsFromData,
664
  parseCsvRowsByFieldType
665
};
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