• 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

89.19
/src/processors/src/file-handler.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import {parseInBatches} from '@loaders.gl/core';
5
import {JSONLoader, _JSONPath} from '@loaders.gl/json';
6
import {CSVLoader} from '@loaders.gl/csv';
7
import {GeoArrowLoader} from '@loaders.gl/arrow';
8
import {ParquetWasmLoader} from '@loaders.gl/parquet';
9
import {Loader} from '@loaders.gl/loader-utils';
10
import {
11
  isPlainObject,
12
  generateHashIdFromString,
13
  getApplicationConfig,
14
  getError,
15
  isArrowTable
16
} from '@kepler.gl/utils';
17
import {generateHashId} from '@kepler.gl/common-utils';
18
import {DATASET_FORMATS} from '@kepler.gl/constants';
19
import {AddDataToMapPayload, Feature, LoadedMap, ProcessorResult} from '@kepler.gl/types';
20
import {KeplerTable} from '@kepler.gl/table';
21
import {FeatureCollection} from '@turf/helpers';
22

23
import {
24
  processArrowBatches,
25
  processGeojson,
26
  processKeplerglJSON,
27
  processRowObject
28
} from './data-processor';
29

30
import {FileCacheItem, ValidKeplerGlMap} from './types';
31

32
const BATCH_TYPE = {
13✔
33
  METADATA: 'metadata',
34
  PARTIAL_RESULT: 'partial-result',
35
  FINAL_RESULT: 'final-result'
36
};
37

38
const CSV_LOADER_OPTIONS = {
13✔
39
  shape: 'object-row-table',
40
  dynamicTyping: false // not working for now
41
};
42

43
const ARROW_LOADER_OPTIONS = {
13✔
44
  shape: 'arrow-table',
45
  batchDebounceMs: 10 // time to delay between batches, for incremental loading
46
};
47

48
const PARQUET_LOADER_OPTIONS = {
13✔
49
  shape: 'arrow-table'
50
};
51

52
const JSON_LOADER_OPTIONS = {
13✔
53
  shape: 'object-row-table',
54
  // instruct loaders.gl on what json paths to stream
55
  jsonpaths: [
56
    '$', // JSON Row array
57
    '$.features', // GeoJSON
58
    '$.datasets' // KeplerGL JSON
59
  ]
60
};
61

62
export type ProcessFileDataContent = {
63
  data: unknown;
64
  fileName: string;
65
  length?: number;
66
  progress?: {rowCount?: number; rowCountInBatch?: number; percent?: number};
67
  /**  metadata e.g. for arrow data, metadata could be the schema.fields */
68
  metadata?: Map<string, string>;
69
};
70

71
export {isArrowTable};
72

73
/**
74
 * check if data is an ArrowData object, which is an array of RecordBatch
75
 * @param data - object to check
76
 * @returns {boolean} - true if data is an ArrowData object type guarded
77
 */
78
export function isArrowData(data: any): boolean {
79
  return Array.isArray(data) && Boolean(data.length && data[0].data && data[0].schema);
5!
80
}
81

82
export function isGeoJson(json: unknown): json is Feature | FeatureCollection {
83
  // json can be feature collection
84
  // or single feature
85
  return isPlainObject(json) && (isFeature(json) || isFeatureCollection(json));
2✔
86
}
87

88
export function isFeature(json: unknown): json is Feature {
89
  return isPlainObject(json) && json.type === 'Feature' && Boolean(json.geometry);
2✔
90
}
91

92
export function isFeatureCollection(json: unknown): json is FeatureCollection {
93
  return isPlainObject(json) && json.type === 'FeatureCollection' && Boolean(json.features);
1✔
94
}
95

96
export function isRowObject(json: any): boolean {
97
  return Array.isArray(json) && isPlainObject(json[0]);
4✔
98
}
99

100
export function isKeplerGlMap(json: unknown): json is ValidKeplerGlMap {
101
  return Boolean(
8✔
102
    isPlainObject(json) &&
22✔
103
      json.datasets &&
104
      json.config &&
105
      json.info &&
106
      isPlainObject(json.info) &&
107
      json.info.app === 'kepler.gl'
108
  );
109
}
110

111
export async function* makeProgressIterator(
112
  asyncIterator: AsyncIterable<any>,
113
  info: {size: number}
114
): AsyncGenerator {
115
  let rowCount = 0;
6✔
116

117
  for await (const batch of asyncIterator) {
6✔
118
    // the length could be stored in `batch.length` for arrow batch
119
    const rowCountInBatch = (batch.data && (batch.data.length || batch.length)) || 0;
18✔
120
    rowCount += rowCountInBatch;
18✔
121
    const percent = Number.isFinite(batch.bytesUsed) ? batch.bytesUsed / info.size : null;
18✔
122

123
    // Update progress object
124
    const progress = {
18✔
125
      rowCount,
126
      rowCountInBatch,
127
      ...(Number.isFinite(percent) ? {percent} : {})
18✔
128
    };
129

130
    yield {...batch, progress};
18✔
131
  }
132
}
133

134
// eslint-disable-next-line complexity
135
export async function* readBatch(
136
  asyncIterator: AsyncIterable<any>,
137
  fileName: string
138
): AsyncGenerator {
139
  let result = null;
5✔
140
  const batches = <any>[];
5✔
141
  for await (const batch of asyncIterator) {
5✔
142
    // Last batch will have this special type and will provide all the root
143
    // properties of the parsed document.
144
    // Only json parse will have `FINAL_RESULT`
145
    if (batch.batchType === BATCH_TYPE.FINAL_RESULT) {
16✔
146
      if (batch.container) {
4!
147
        result = {...batch.container};
4✔
148
      }
149
      // Set the streamed data correctly is Batch json path is set
150
      // and the path streamed is not the top level object (jsonpath = '$')
151
      if (batch.jsonpath && batch.jsonpath.length > 1) {
4✔
152
        const streamingPath = new _JSONPath(batch.jsonpath);
2✔
153
        streamingPath.setFieldAtPath(result, batches);
2✔
154
      } else if (batch.jsonpath && batch.jsonpath.length === 1) {
2✔
155
        // The streamed object is a ROW JSON-batch (jsonpath = '$')
156
        // row objects
157
        result = batches;
1✔
158
      }
159
    } else {
160
      const batchData = isArrowTable(batch.data) ? batch.data.batches : batch.data;
12!
161
      for (let i = 0; i < batchData?.length; i++) {
12✔
162
        batches.push(batchData[i]);
48✔
163
      }
164
    }
165

166
    yield {
16✔
167
      ...batch,
168
      ...(batch.schema ? {headers: Object.keys(batch.schema)} : {}),
16✔
169
      fileName,
170
      // if dataset is CSV, data is set to the raw batches
171
      data: result ? result : batches
16✔
172
    };
173
  }
174
}
175

176
export async function readFileInBatches({
177
  file,
178
  loaders = [],
5✔
179
  loadOptions = {}
5✔
180
}: {
181
  file: File;
182
  fileCache: FileCacheItem[];
183
  loaders: Loader[];
184
  loadOptions: any;
185
}): Promise<AsyncGenerator> {
186
  loaders = [JSONLoader, CSVLoader, GeoArrowLoader, ParquetWasmLoader, ...loaders];
5✔
187
  loadOptions = {
5✔
188
    csv: CSV_LOADER_OPTIONS,
189
    arrow: ARROW_LOADER_OPTIONS,
190
    json: JSON_LOADER_OPTIONS,
191
    parquet: PARQUET_LOADER_OPTIONS,
192
    metadata: true,
193
    ...loadOptions
194
  };
195

196
  const batchIterator = await parseInBatches(file, loaders, loadOptions);
5✔
197
  const progressIterator = makeProgressIterator(batchIterator, {size: file.size});
5✔
198

199
  return readBatch(progressIterator, file.name);
5✔
200
}
201

202
export async function processFileData({
203
  content,
204
  fileCache
205
}: {
206
  content: ProcessFileDataContent;
207
  fileCache: FileCacheItem[];
208
}): Promise<FileCacheItem[]> {
209
  const {fileName, data} = content;
5✔
210
  let format: string | undefined;
211
  let processor: ((data: any) => ProcessorResult | LoadedMap | null) | undefined;
212
  // generate unique id with length of 4 using fileName string
213
  const id = generateHashIdFromString(fileName);
5✔
214
  // decide on which table class to use based on application config
215
  const table = getApplicationConfig().table ?? KeplerTable;
5✔
216

217
  if (typeof table.getFileProcessor === 'function') {
5!
218
    // use custom processors from table class
219
    const processorResult = table.getFileProcessor(data);
×
220
    format = processorResult.format;
×
221
    processor = processorResult.processor;
×
222
  } else {
223
    // use default processors
224
    if (isArrowData(data)) {
5!
225
      format = DATASET_FORMATS.arrow;
×
226
      processor = processArrowBatches;
×
227
    } else if (isKeplerGlMap(data)) {
5✔
228
      format = DATASET_FORMATS.keplergl;
1✔
229
      processor = processKeplerglJSON;
1✔
230
    } else if (isRowObject(data)) {
4✔
231
      // csv file goes here
232
      format = DATASET_FORMATS.row;
2✔
233
      processor = processRowObject;
2✔
234
    } else if (isGeoJson(data)) {
2!
235
      format = DATASET_FORMATS.geojson;
2✔
236
      processor = processGeojson;
2✔
237
    }
238
  }
239
  if (format && processor) {
5!
240
    // eslint-disable-next-line no-useless-catch
241
    let result;
242
    try {
5✔
243
      result = await processor(data);
5✔
244
    } catch (error) {
245
      throw new Error(`Can not process uploaded file, ${getError(error as Error)}`);
×
246
    }
247

248
    return [
5✔
249
      ...fileCache,
250
      {
251
        data: result,
252
        info: {
253
          id,
254
          label: content.fileName,
255
          format
256
        }
257
      }
258
    ];
259
  } else {
260
    throw new Error('Can not process uploaded file, unknown file format');
×
261
  }
262
}
263

264
export function filesToDataPayload(fileCache: FileCacheItem[]): AddDataToMapPayload[] {
265
  // seperate out files which could be a single datasets. or a keplergl map json
266
  const collection = fileCache.reduce<{
1✔
267
    datasets: FileCacheItem[];
268
    keplerMaps: AddDataToMapPayload[];
269
  }>(
270
    (accu, file) => {
271
      const {data, info} = file;
2✔
272
      if (info?.format === DATASET_FORMATS.keplergl) {
2✔
273
        // if file contains a single kepler map dataset & config
274
        accu.keplerMaps.push({
1✔
275
          ...data,
276
          options: {
277
            centerMap: !(data.config && data.config.mapState)
2✔
278
          }
279
        });
280
      } else if (DATASET_FORMATS[info?.format]) {
1!
281
        // if file contains only data
282
        const newDataset = {
1✔
283
          data,
284
          info: {
285
            id: info?.id || generateHashId(4),
2✔
286
            ...(info || {})
1!
287
          }
288
        };
289
        accu.datasets.push(newDataset);
1✔
290
      }
291
      return accu;
2✔
292
    },
293
    {datasets: [], keplerMaps: []}
294
  );
295

296
  // add kepler map first with config
297
  // add datasets later in one add data call
298
  return collection.keplerMaps.concat({datasets: collection.datasets});
1✔
299
}
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

© 2025 Coveralls, Inc