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

keplergl / kepler.gl / 13243677469

10 Feb 2025 02:54PM UTC coverage: 66.495% (-0.02%) from 66.518%
13243677469

push

github

web-flow
[fix] export geoarrow to CSV as geojson (#2988)

- [x] geoarrow.polygon save / load as stringified geojson
- [x] geoarrow.linestring save / load as stringified geojson
- [x] geoarrow.point save / load as stringified geojson
- [x] hex wkb string save / load as stringified geojson

---------

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>
Co-authored-by: Ilya Boyandin <ilya@boyandin.me>

6005 of 10538 branches covered (56.98%)

Branch coverage included in aggregate %.

4 of 13 new or added lines in 2 files covered. (30.77%)

12341 of 17052 relevant lines covered (72.37%)

89.25 hits per line

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

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

4
import * as arrow from 'apache-arrow';
5
import assert from 'assert';
6
import {format as d3Format} from 'd3-format';
7
import moment from 'moment-timezone';
8

9
import {parseGeometryFromArrow} from '@loaders.gl/arrow';
10

11
import {
12
  ALL_FIELD_TYPES,
13
  TOOLTIP_FORMATS,
14
  TOOLTIP_FORMAT_TYPES,
15
  TOOLTIP_KEY,
16
  TooltipFormat
17
} from '@kepler.gl/constants';
18
import {notNullorUndefined} from '@kepler.gl/common-utils';
19
import {Field, Millisecond} from '@kepler.gl/types';
20

21
import {snapToMarks} from './plot';
22
import {isPlainObject} from './utils';
23

24
export type FieldFormatter = (value: any, field?: Field) => string;
25

26
// We need threat latitude differently otherwise mercator project view throws
27
// a projection matrix error
28
// Uncaught Error: Pixel project matrix not invertible
29
// at WebMercatorViewport16.Viewport6 (viewport.js:81:13)
30
export const MAX_LATITUDE = 89.9;
15✔
31
export const MIN_LATITUDE = -89.9;
15✔
32
export const MAX_LONGITUDE = 180;
15✔
33
export const MIN_LONGITUDE = -180;
15✔
34

35
/**
36
 * Validates a latitude value.
37
 * Ensures that the latitude is within the defined minimum and maximum latitude bounds.
38
 * If the value is out of bounds, it returns the nearest bound value.
39
 * @param latitude - The latitude value to validate.
40
 * @returns The validated latitude value.
41
 */
42
export function validateLatitude(latitude: number | undefined): number {
43
  return validateCoordinate(latitude ?? 0, MIN_LATITUDE, MAX_LATITUDE);
260!
44
}
45

46
/**
47
 * Validates a longitude value.
48
 * Ensures that the longitude is within the defined minimum and maximum longitude bounds.
49
 * If the value is out of bounds, it returns the nearest bound value.
50
 * @param longitude - The longitude value to validate.
51
 * @returns The validated longitude value.
52
 */
53
export function validateLongitude(longitude: number | undefined): number {
54
  return validateCoordinate(longitude ?? 0, MIN_LONGITUDE, MAX_LONGITUDE);
260!
55
}
56

57
/**
58
 * Validates a coordinate value.
59
 * Ensures that the value is within the specified minimum and maximum bounds.
60
 * If the value is out of bounds, it returns the nearest bound value.
61
 * @param value - The coordinate value to validate.
62
 * @param minValue - The minimum bound for the value.
63
 * @param maxValue - The maximum bound for the value.
64
 * @returns The validated coordinate value.
65
 */
66
export function validateCoordinate(value: number, minValue: number, maxValue: number): number {
67
  if (value <= minValue) {
520✔
68
    return minValue;
7✔
69
  }
70
  if (value >= maxValue) {
513!
71
    return maxValue;
×
72
  }
73

74
  return value;
513✔
75
}
76

77
/**
78
 * simple getting unique values of an array
79
 *
80
 * @param values
81
 * @returns unique values
82
 */
83
export function unique<T>(values: T[]) {
84
  const results: T[] = [];
57✔
85
  const uniqueSet = new Set(values);
57✔
86
  uniqueSet.forEach(v => {
57✔
87
    if (notNullorUndefined(v)) {
210✔
88
      results.push(v);
182✔
89
    }
90
  });
91
  return results;
57✔
92
}
93

94
export function getLatLngBounds(
95
  points: number[][],
96
  idx: number,
97
  limit: [number, number]
98
): [number, number] | null {
99
  const lats = points
666✔
100
    .map(d => Number(Array.isArray(d)) && d[idx])
6,756✔
101
    .filter(Number.isFinite)
102
    .sort(numberSort);
103

104
  if (!lats.length) {
666!
105
    return null;
×
106
  }
107

108
  // clamp to limit
109
  return [Math.max(lats[0], limit[0]), Math.min(lats[lats.length - 1], limit[1])];
666✔
110
}
111

112
export function clamp([min, max]: [number, number], val = 0): number {
×
113
  return val <= min ? min : val >= max ? max : val;
25✔
114
}
115

116
export function getSampleData(data, sampleSize = 500, getValue = d => d) {
×
117
  const sampleStep = Math.max(Math.floor(data.length / sampleSize), 1);
×
118
  const output: any[] = [];
×
119
  for (let i = 0; i < data.length; i += sampleStep) {
×
120
    output.push(getValue(data[i]));
×
121
  }
122

123
  return output;
×
124
}
125

126
/**
127
 * Convert different time format to unix milliseconds
128
 */
129
export function timeToUnixMilli(value: string | number | Date, format: string): Millisecond | null {
130
  if (notNullorUndefined(value)) {
2,342✔
131
    if (typeof value === 'string') {
2,289✔
132
      return moment.utc(value, format).valueOf();
648✔
133
    }
134
    if (typeof value === 'number') {
1,641!
135
      return format === 'x' ? value * 1000 : value;
1,641✔
136
    }
137
    if (value instanceof Date) {
×
138
      return value.valueOf();
×
139
    }
140
  }
141
  return null;
53✔
142
}
143

144
/**
145
 * Whether d is a number, this filtered out NaN as well
146
 */
147
export function isNumber(d: unknown): d is number {
148
  return Number.isFinite(d);
805✔
149
}
150

151
/**
152
 * whether object has property
153
 * @param {string} prop
154
 * @returns {boolean} - yes or no
155
 */
156
export function hasOwnProperty<X extends object, Y extends PropertyKey>(
157
  obj: X,
158
  prop: Y
159
): obj is X & Record<Y, unknown> {
160
  return Object.prototype.hasOwnProperty.call(obj, prop);
9✔
161
}
162

163
export function numberSort(a: number, b: number): number {
164
  return a - b;
17,076✔
165
}
166

167
export function getSortingFunction(fieldType: string): typeof numberSort | undefined {
168
  switch (fieldType) {
131✔
169
    case ALL_FIELD_TYPES.real:
170
    case ALL_FIELD_TYPES.integer:
171
    case ALL_FIELD_TYPES.timestamp:
172
      return numberSort;
96✔
173
    default:
174
      return undefined;
35✔
175
  }
176
}
177

178
/**
179
 * round number with exact number of decimals
180
 * return as a string
181
 */
182
export function preciseRound(num: number, decimals: number): string {
183
  const t = Math.pow(10, decimals);
82✔
184
  return (
82✔
185
    Math.round(
186
      num * t + (decimals > 0 ? 1 : 0) * (Math.sign(num) * (10 / Math.pow(100, decimals)))
82✔
187
    ) / t
188
  ).toFixed(decimals);
189
}
190

191
/**
192
 * round a giving number at most 4 decimal places
193
 * e.g. 10 -> 10, 1.12345 -> 1.2345, 2.0 -> 2
194
 */
195
export function roundToFour(num: number): number {
196
  // @ts-expect-error
197
  return Number(`${Math.round(`${num}e+4`)}e-4`);
5✔
198
}
199
/**
200
 * get number of decimals to round to for slider from step
201
 * @param step
202
 * @returns- number of decimal
203
 */
204
export function getRoundingDecimalFromStep(step: number): number {
205
  if (isNaN(step)) {
30!
206
    assert('step is not a number');
×
207
    assert(step);
×
208
  }
209

210
  const stepStr = step.toString();
30✔
211

212
  // in case the step is a very small number e.g. 1e-7, return decimal e.g. 7 directly
213
  const splitExponential = stepStr.split('e-');
30✔
214
  if (splitExponential.length === 2) {
30✔
215
    const coeffZero = splitExponential[0].split('.');
2✔
216
    const coeffDecimal = coeffZero.length === 1 ? 0 : coeffZero[1].length;
2✔
217
    return parseInt(splitExponential[1], 10) + coeffDecimal;
2✔
218
  }
219

220
  const splitZero = stepStr.split('.');
28✔
221
  if (splitZero.length === 1) {
28✔
222
    return 0;
9✔
223
  }
224
  return splitZero[1].length;
19✔
225
}
226

227
/**
228
 * If marks is provided, snap to marks, if not normalize to step
229
 * @param val
230
 * @param minValue
231
 * @param step
232
 * @param marks
233
 */
234
export function normalizeSliderValue(
235
  val: number,
236
  minValue: number | undefined,
237
  step: number,
238
  marks?: number[] | null
239
): number {
240
  if (marks && marks.length) {
8✔
241
    // Use in slider, given a number and an array of numbers, return the nears number from the array
242
    return snapToMarks(val, marks);
1✔
243
  }
244

245
  return roundValToStep(minValue, step, val);
7✔
246
}
247

248
/**
249
 * round the value to step for the slider
250
 * @param minValue
251
 * @param step
252
 * @param val
253
 * @returns - rounded number
254
 */
255
export function roundValToStep(minValue: number | undefined, step: number, val: number): number {
256
  if (!isNumber(step) || !isNumber(minValue)) {
22✔
257
    return val;
2✔
258
  }
259

260
  const decimal = getRoundingDecimalFromStep(step);
20✔
261
  const steps = Math.floor((val - minValue) / step);
20✔
262
  let remain = val - (steps * step + minValue);
20✔
263

264
  // has to round because javascript turns 0.1 into 0.9999999999999987
265
  remain = Number(preciseRound(remain, 8));
20✔
266

267
  let closest: number;
268
  if (remain === 0) {
20✔
269
    closest = val;
5✔
270
  } else if (remain < step / 2) {
15✔
271
    closest = steps * step + minValue;
4✔
272
  } else {
273
    closest = (steps + 1) * step + minValue;
11✔
274
  }
275

276
  // precise round return a string rounded to the defined decimal
277
  const rounded = preciseRound(closest, decimal);
20✔
278

279
  return Number(rounded);
20✔
280
}
281

282
/**
283
 * Get the value format based on field and format options
284
 * Used in render tooltip value
285
 */
286
export const defaultFormatter: FieldFormatter = v => (notNullorUndefined(v) ? String(v) : '');
1,710✔
287

288
export const floatFormatter = v => (isNumber(v) ? String(roundToFour(v)) : '');
15!
289

290
/**
291
 * Transforms a WKB in Uint8Array form into a hex WKB string.
292
 * @param uint8Array WKB in Uint8Array form.
293
 * @returns hex WKB string.
294
 */
295
export function uint8ArrayToHex(data: Uint8Array): string {
296
  return Array.from(data)
×
297
    .map(byte => (byte as any).toString(16).padStart(2, '0'))
×
298
    .join('');
299
}
300

301
export const FIELD_DISPLAY_FORMAT: {
302
  [key: string]: FieldFormatter;
303
} = {
15✔
304
  [ALL_FIELD_TYPES.string]: defaultFormatter,
305
  [ALL_FIELD_TYPES.timestamp]: defaultFormatter,
306
  [ALL_FIELD_TYPES.integer]: defaultFormatter,
307
  [ALL_FIELD_TYPES.real]: defaultFormatter,
308
  [ALL_FIELD_TYPES.boolean]: defaultFormatter,
309
  [ALL_FIELD_TYPES.date]: defaultFormatter,
310
  [ALL_FIELD_TYPES.geojson]: d =>
311
    typeof d === 'string'
23✔
312
      ? d
313
      : isPlainObject(d)
20!
314
      ? JSON.stringify(d)
315
      : Array.isArray(d)
×
316
      ? `[${String(d)}]`
317
      : '',
318
  [ALL_FIELD_TYPES.geoarrow]: (data, field) => {
NEW
319
    if (data instanceof arrow.Vector) {
×
NEW
320
      try {
×
NEW
321
        const encoding = field?.metadata?.get('ARROW:extension:name');
×
NEW
322
        if (encoding) {
×
NEW
323
          const geometry = parseGeometryFromArrow(data, encoding);
×
NEW
324
          return JSON.stringify(geometry);
×
325
        }
326
      } catch (error) {
327
        // ignore for now
328
      }
NEW
329
    } else if (data instanceof Uint8Array) {
×
NEW
330
      return uint8ArrayToHex(data);
×
331
    }
NEW
332
    return data;
×
333
  },
334
  [ALL_FIELD_TYPES.object]: (value: any) => {
335
    try {
5✔
336
      return JSON.stringify(value);
5✔
337
    } catch (e) {
338
      return String(value);
×
339
    }
340
  },
341
  [ALL_FIELD_TYPES.array]: d => JSON.stringify(d),
24✔
342
  [ALL_FIELD_TYPES.h3]: defaultFormatter
343
};
344

345
/**
346
 * Parse field value and type and return a string representation
347
 */
348
export const parseFieldValue = (value: any, type: string, field?: Field): string => {
15✔
349
  if (!notNullorUndefined(value)) {
665✔
350
    return '';
110✔
351
  }
352
  // BigInt values cannot be serialized with JSON.stringify() directly
353
  // We need to explicitly convert them to strings using .toString()
354
  // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json
355
  if (typeof value === 'bigint') {
555!
356
    return value.toString();
×
357
  }
358
  return FIELD_DISPLAY_FORMAT[type] ? FIELD_DISPLAY_FORMAT[type](value, field) : String(value);
555!
359
};
360

361
/**
362
 * Get the value format based on field and format options
363
 * Used in render tooltip value
364
 * @param format
365
 * @param field
366
 */
367
export function getFormatter(
368
  format?: string | Record<string, string> | null,
369
  field?: Field
370
): FieldFormatter {
371
  if (!format) {
473✔
372
    return defaultFormatter;
1✔
373
  }
374
  const tooltipFormat = Object.values(TOOLTIP_FORMATS).find(f => f[TOOLTIP_KEY] === format);
11,470✔
375

376
  if (tooltipFormat) {
472✔
377
    return applyDefaultFormat(tooltipFormat as TooltipFormat);
465✔
378
  } else if (typeof format === 'string' && field) {
7✔
379
    return applyCustomFormat(format, field);
6✔
380
  }
381

382
  return defaultFormatter;
1✔
383
}
384

385
export function getColumnFormatter(
386
  field: Pick<Field, 'type'> & Partial<Pick<Field, 'format' | 'displayFormat'>>
387
): FieldFormatter {
388
  const {format, displayFormat} = field;
1,566✔
389

390
  if (!format && !displayFormat) {
1,566✔
391
    return FIELD_DISPLAY_FORMAT[field.type];
621✔
392
  }
393
  const tooltipFormat = Object.values(TOOLTIP_FORMATS).find(f => f[TOOLTIP_KEY] === displayFormat);
27,540✔
394

395
  if (tooltipFormat) {
945✔
396
    return applyDefaultFormat(tooltipFormat);
81✔
397
  } else if (typeof displayFormat === 'string' && field) {
864!
398
    return applyCustomFormat(displayFormat, field);
×
399
  } else if (typeof displayFormat === 'object') {
864!
400
    return applyValueMap(displayFormat);
×
401
  }
402

403
  return defaultFormatter;
864✔
404
}
405

406
export function applyValueMap(format) {
407
  return v => format[v];
×
408
}
409

410
export function applyDefaultFormat(tooltipFormat: TooltipFormat): (v: any) => string {
411
  if (!tooltipFormat || !tooltipFormat.format) {
546!
412
    return defaultFormatter;
×
413
  }
414

415
  switch (tooltipFormat.type) {
546!
416
    case TOOLTIP_FORMAT_TYPES.DECIMAL:
417
      return d3Format(tooltipFormat.format);
83✔
418
    case TOOLTIP_FORMAT_TYPES.DATE:
419
    case TOOLTIP_FORMAT_TYPES.DATE_TIME:
420
      return datetimeFormatter(null)(tooltipFormat.format);
458✔
421
    case TOOLTIP_FORMAT_TYPES.PERCENTAGE:
422
      return v => `${d3Format(TOOLTIP_FORMATS.DECIMAL_DECIMAL_FIXED_2.format)(v)}%`;
1✔
423
    case TOOLTIP_FORMAT_TYPES.BOOLEAN:
424
      return getBooleanFormatter(tooltipFormat.format);
4✔
425
    default:
426
      return defaultFormatter;
×
427
  }
428
}
429

430
export function getBooleanFormatter(format: string): FieldFormatter {
431
  switch (format) {
4!
432
    case '01':
433
      return (v: boolean) => (v ? '1' : '0');
2✔
434
    case 'yn':
435
      return (v: boolean) => (v ? 'yes' : 'no');
2✔
436
    default:
437
      return defaultFormatter;
×
438
  }
439
}
440
// Allow user to specify custom tooltip format via config
441
export function applyCustomFormat(format, field: {type?: string}): FieldFormatter {
442
  switch (field.type) {
6!
443
    case ALL_FIELD_TYPES.real:
444
    case ALL_FIELD_TYPES.integer:
445
      return d3Format(format);
5✔
446
    case ALL_FIELD_TYPES.date:
447
    case ALL_FIELD_TYPES.timestamp:
448
      return datetimeFormatter(null)(format);
1✔
449
    default:
450
      return v => v;
×
451
  }
452
}
453

454
function formatLargeNumber(n) {
455
  // SI-prefix with 4 significant digits
456
  return d3Format('.4~s')(n);
24✔
457
}
458

459
export function formatNumber(n: number, type?: string): string {
460
  switch (type) {
217✔
461
    case ALL_FIELD_TYPES.integer:
462
      if (n < 0) {
137✔
463
        return `-${formatNumber(-n, 'integer')}`;
1✔
464
      }
465
      if (n < 1000) {
136✔
466
        return `${Math.round(n)}`;
36✔
467
      }
468
      if (n < 10 * 1000) {
100✔
469
        return d3Format(',')(Math.round(n));
77✔
470
      }
471
      return formatLargeNumber(n);
23✔
472
    case ALL_FIELD_TYPES.real:
473
      if (n < 0) {
77!
474
        return `-${formatNumber(-n, 'number')}`;
×
475
      }
476
      if (n < 1000) {
77✔
477
        return d3Format('.4~r')(n);
52✔
478
      }
479
      if (n < 10 * 1000) {
25✔
480
        return d3Format(',.2~f')(n);
24✔
481
      }
482
      return formatLargeNumber(n);
1✔
483

484
    default:
485
      return formatNumber(n, 'real');
3✔
486
  }
487
}
488

489
const transformation = {
15✔
490
  Y: Math.pow(10, 24),
491
  Z: Math.pow(10, 21),
492
  E: Math.pow(10, 18),
493
  P: Math.pow(10, 15),
494
  T: Math.pow(10, 12),
495
  G: Math.pow(10, 9),
496
  M: Math.pow(10, 6),
497
  k: Math.pow(10, 3),
498
  h: Math.pow(10, 2),
499
  da: Math.pow(10, 1),
500
  d: Math.pow(10, -1),
501
  c: Math.pow(10, -2),
502
  m: Math.pow(10, -3),
503
  μ: Math.pow(10, -6),
504
  n: Math.pow(10, -9),
505
  p: Math.pow(10, -12),
506
  f: Math.pow(10, -15),
507
  a: Math.pow(10, -18),
508
  z: Math.pow(10, -21),
509
  y: Math.pow(10, -24)
510
};
511

512
/**
513
 * Convert a formatted number from string back to number
514
 */
515
export function reverseFormatNumber(str: string): number {
516
  let returnValue: number | null = null;
110✔
517
  const strNum = str.trim().replace(/,/g, '');
110✔
518
  Object.entries(transformation).forEach(d => {
110✔
519
    if (strNum.includes(d[0])) {
2,200✔
520
      returnValue = parseFloat(strNum) * d[1];
21✔
521
      return true;
21✔
522
    }
523
    return false;
2,179✔
524
  });
525

526
  // if no transformer found, convert to nuber regardless
527
  return returnValue === null ? Number(strNum) : returnValue;
110✔
528
}
529

530
/**
531
 * Format epoch milliseconds with a format string
532
 * @type timezone
533
 */
534
export function datetimeFormatter(
535
  timezone?: string | null
536
): (format?: string) => (ts: number) => string {
537
  return timezone
480✔
538
    ? format => ts => moment.utc(ts).tz(timezone).format(format)
18✔
539
    : // return empty string instead of 'Invalid date' if ts is undefined/null
540
      format => ts => ts ? moment.utc(ts).format(format) : '';
493✔
541
}
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