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

keplergl / kepler.gl / 11803587897

12 Nov 2024 06:36PM UTC coverage: 68.723% (-0.3%) from 69.055%
11803587897

push

github

web-flow
[feat] Support custom breaks in color scale (#2739)

-Add SCALE_TYPE.custom option
-Save color breaks in colorRange.colorMap
-add customBreaks: true to layer.colorUI.colorRange.colorRangeConfig to render color breaks panel
-Make color scale selector a seperate component
-Create factory for color-selector, dimension-scale-selector, color-range-selector, custom-palette

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

5345 of 9031 branches covered (59.19%)

Branch coverage included in aggregate %.

303 of 440 new or added lines in 24 files covered. (68.86%)

2 existing lines in 2 files now uncovered.

11231 of 15089 relevant lines covered (74.43%)

90.84 hits per line

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

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

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

8
import {
9
  ALL_FIELD_TYPES,
10
  TOOLTIP_FORMATS,
11
  TOOLTIP_FORMAT_TYPES,
12
  TOOLTIP_KEY,
13
  TooltipFormat
14
} from '@kepler.gl/constants';
15
import {Field, Millisecond} from '@kepler.gl/types';
16

17
import {snapToMarks} from './plot';
18
import {isPlainObject} from './utils';
19

20
export type FieldFormatter = (value: any) => string;
21

22
/**
23
 * simple getting unique values of an array
24
 *
25
 * @param values
26
 * @returns unique values
27
 */
28
export function unique<T>(values: T[]) {
29
  const results: T[] = [];
52✔
30
  const uniqueSet = new Set(values);
52✔
31
  uniqueSet.forEach(v => {
52✔
32
    if (notNullorUndefined(v)) {
192✔
33
      results.push(v);
164✔
34
    }
35
  });
36
  return results;
52✔
37
}
38

39
export function getLatLngBounds(
40
  points: number[][],
41
  idx: number,
42
  limit: [number, number]
43
): [number, number] | null {
44
  const lats = points
666✔
45
    .map(d => Number(Array.isArray(d)) && d[idx])
7,132✔
46
    .filter(Number.isFinite)
47
    .sort(numberSort);
48

49
  if (!lats.length) {
666!
50
    return null;
×
51
  }
52

53
  // clamp to limit
54
  return [Math.max(lats[0], limit[0]), Math.min(lats[lats.length - 1], limit[1])];
666✔
55
}
56

57
export function clamp([min, max]: [number, number], val = 0): number {
×
58
  return val <= min ? min : val >= max ? max : val;
25✔
59
}
60

61
export function getSampleData(data, sampleSize = 500, getValue = d => d) {
×
62
  const sampleStep = Math.max(Math.floor(data.length / sampleSize), 1);
×
63
  const output: any[] = [];
×
64
  for (let i = 0; i < data.length; i += sampleStep) {
×
65
    output.push(getValue(data[i]));
×
66
  }
67

68
  return output;
×
69
}
70

71
/**
72
 * Convert different time format to unix milliseconds
73
 */
74
export function timeToUnixMilli(value: string | number | Date, format: string): Millisecond | null {
75
  if (notNullorUndefined(value)) {
2,395✔
76
    if (typeof value === 'string') {
2,346✔
77
      return moment.utc(value, format).valueOf();
600✔
78
    }
79
    if (typeof value === 'number') {
1,746!
80
      return format === 'x' ? value * 1000 : value;
1,746✔
81
    }
82
    if (value instanceof Date) {
×
83
      return value.valueOf();
×
84
    }
85
  }
86
  return null;
49✔
87
}
88

89
/**
90
 * whether null or undefined
91
 */
92
export function notNullorUndefined<T extends NonNullable<any>>(d: T | null | undefined): d is T {
93
  return d !== undefined && d !== null;
30,910✔
94
}
95

96
/**
97
 * Whether d is a number, this filtered out NaN as well
98
 */
99
export function isNumber(d: unknown): d is number {
100
  return Number.isFinite(d);
56✔
101
}
102

103
/**
104
 * whether object has property
105
 * @param {string} prop
106
 * @returns {boolean} - yes or no
107
 */
108
export function hasOwnProperty<X extends object, Y extends PropertyKey>(
109
  obj: X,
110
  prop: Y
111
): obj is X & Record<Y, unknown> {
112
  return Object.prototype.hasOwnProperty.call(obj, prop);
9✔
113
}
114

115
export function numberSort(a: number, b: number): number {
116
  return a - b;
21,596✔
117
}
118

119
export function getSortingFunction(fieldType: string): typeof numberSort | undefined {
120
  switch (fieldType) {
110✔
121
    case ALL_FIELD_TYPES.real:
122
    case ALL_FIELD_TYPES.integer:
123
    case ALL_FIELD_TYPES.timestamp:
124
      return numberSort;
81✔
125
    default:
126
      return undefined;
29✔
127
  }
128
}
129

130
/**
131
 * round number with exact number of decimals
132
 * return as a string
133
 */
134
export function preciseRound(num: number, decimals: number): string {
135
  const t = Math.pow(10, decimals);
76✔
136
  return (
76✔
137
    Math.round(
138
      num * t + (decimals > 0 ? 1 : 0) * (Math.sign(num) * (10 / Math.pow(100, decimals)))
76✔
139
    ) / t
140
  ).toFixed(decimals);
141
}
142

143
/**
144
 * round a giving number at most 4 decimal places
145
 * e.g. 10 -> 10, 1.12345 -> 1.2345, 2.0 -> 2
146
 */
147
export function roundToFour(num: number): number {
148
  // @ts-expect-error
149
  return Number(`${Math.round(`${num}e+4`)}e-4`);
5✔
150
}
151
/**
152
 * get number of decimals to round to for slider from step
153
 * @param step
154
 * @returns- number of decimal
155
 */
156
export function getRoundingDecimalFromStep(step: number): number {
157
  if (isNaN(step)) {
30!
158
    assert('step is not a number');
×
159
    assert(step);
×
160
  }
161

162
  const stepStr = step.toString();
30✔
163

164
  // in case the step is a very small number e.g. 1e-7, return decimal e.g. 7 directly
165
  const splitExponential = stepStr.split('e-');
30✔
166
  if (splitExponential.length === 2) {
30✔
167
    const coeffZero = splitExponential[0].split('.');
2✔
168
    const coeffDecimal = coeffZero.length === 1 ? 0 : coeffZero[1].length;
2✔
169
    return parseInt(splitExponential[1], 10) + coeffDecimal;
2✔
170
  }
171

172
  const splitZero = stepStr.split('.');
28✔
173
  if (splitZero.length === 1) {
28✔
174
    return 0;
9✔
175
  }
176
  return splitZero[1].length;
19✔
177
}
178

179
/**
180
 * If marks is provided, snap to marks, if not normalize to step
181
 * @param val
182
 * @param minValue
183
 * @param step
184
 * @param marks
185
 */
186
export function normalizeSliderValue(
187
  val: number,
188
  minValue: number,
189
  step: number,
190
  marks?: number[]
191
): number {
192
  if (marks && marks.length) {
8✔
193
    // Use in slider, given a number and an array of numbers, return the nears number from the array
194
    return snapToMarks(val, marks);
1✔
195
  }
196

197
  return roundValToStep(minValue, step, val);
7✔
198
}
199

200
/**
201
 * round the value to step for the slider
202
 * @param minValue
203
 * @param step
204
 * @param val
205
 * @returns - rounded number
206
 */
207
export function roundValToStep(minValue: number, step: number, val: number): number {
208
  if (!isNumber(step) || !isNumber(minValue)) {
22✔
209
    return val;
2✔
210
  }
211

212
  const decimal = getRoundingDecimalFromStep(step);
20✔
213
  const steps = Math.floor((val - minValue) / step);
20✔
214
  let remain = val - (steps * step + minValue);
20✔
215

216
  // has to round because javascript turns 0.1 into 0.9999999999999987
217
  remain = Number(preciseRound(remain, 8));
20✔
218

219
  let closest: number;
220
  if (remain === 0) {
20✔
221
    closest = val;
5✔
222
  } else if (remain < step / 2) {
15✔
223
    closest = steps * step + minValue;
4✔
224
  } else {
225
    closest = (steps + 1) * step + minValue;
11✔
226
  }
227

228
  // precise round return a string rounded to the defined decimal
229
  const rounded = preciseRound(closest, decimal);
20✔
230

231
  return Number(rounded);
20✔
232
}
233

234
/**
235
 * Get the value format based on field and format options
236
 * Used in render tooltip value
237
 */
238
export const defaultFormatter: FieldFormatter = v => (notNullorUndefined(v) ? String(v) : '');
1,713✔
239

240
export const floatFormatter = v => (isNumber(v) ? String(roundToFour(v)) : '');
11!
241

242
export const FIELD_DISPLAY_FORMAT: {
243
  [key: string]: FieldFormatter;
244
} = {
11✔
245
  [ALL_FIELD_TYPES.string]: defaultFormatter,
246
  [ALL_FIELD_TYPES.timestamp]: defaultFormatter,
247
  [ALL_FIELD_TYPES.integer]: defaultFormatter,
248
  [ALL_FIELD_TYPES.real]: defaultFormatter,
249
  [ALL_FIELD_TYPES.boolean]: defaultFormatter,
250
  [ALL_FIELD_TYPES.date]: defaultFormatter,
251
  [ALL_FIELD_TYPES.geojson]: d =>
252
    typeof d === 'string'
23✔
253
      ? d
254
      : isPlainObject(d)
20!
255
      ? JSON.stringify(d)
256
      : Array.isArray(d)
×
257
      ? `[${String(d)}]`
258
      : '',
259
  [ALL_FIELD_TYPES.geoarrow]: d => d,
×
260
  [ALL_FIELD_TYPES.object]: JSON.stringify,
261
  [ALL_FIELD_TYPES.array]: JSON.stringify
262
};
263

264
/**
265
 * Parse field value and type and return a string representation
266
 */
267
export const parseFieldValue = (value: any, type: string): string => {
11✔
268
  if (!notNullorUndefined(value)) {
670✔
269
    return '';
112✔
270
  }
271
  return FIELD_DISPLAY_FORMAT[type] ? FIELD_DISPLAY_FORMAT[type](value) : String(value);
558!
272
};
273

274
/**
275
 * Get the value format based on field and format options
276
 * Used in render tooltip value
277
 * @param format
278
 * @param field
279
 */
280
export function getFormatter(
281
  format: string | Record<string, string> | null,
282
  field?: Field
283
): FieldFormatter {
284
  if (!format) {
473✔
285
    return defaultFormatter;
1✔
286
  }
287
  const tooltipFormat = Object.values(TOOLTIP_FORMATS).find(f => f[TOOLTIP_KEY] === format);
11,470✔
288

289
  if (tooltipFormat) {
472✔
290
    return applyDefaultFormat(tooltipFormat as TooltipFormat);
465✔
291
  } else if (typeof format === 'string' && field) {
7✔
292
    return applyCustomFormat(format, field);
6✔
293
  }
294

295
  return defaultFormatter;
1✔
296
}
297

298
export function getColumnFormatter(
299
  field: Pick<Field, 'type'> & Partial<Pick<Field, 'format' | 'displayFormat'>>
300
): FieldFormatter {
301
  const {format, displayFormat} = field;
1,566✔
302

303
  if (!format && !displayFormat) {
1,566✔
304
    return FIELD_DISPLAY_FORMAT[field.type];
621✔
305
  }
306
  const tooltipFormat = Object.values(TOOLTIP_FORMATS).find(f => f[TOOLTIP_KEY] === displayFormat);
27,540✔
307

308
  if (tooltipFormat) {
945✔
309
    return applyDefaultFormat(tooltipFormat);
81✔
310
  } else if (typeof displayFormat === 'string' && field) {
864!
311
    return applyCustomFormat(displayFormat, field);
×
312
  } else if (typeof displayFormat === 'object') {
864!
313
    return applyValueMap(displayFormat);
×
314
  }
315

316
  return defaultFormatter;
864✔
317
}
318

319
export function applyValueMap(format) {
320
  return v => format[v];
×
321
}
322

323
export function applyDefaultFormat(tooltipFormat: TooltipFormat): (v: any) => string {
324
  if (!tooltipFormat || !tooltipFormat.format) {
546!
325
    return defaultFormatter;
×
326
  }
327

328
  switch (tooltipFormat.type) {
546!
329
    case TOOLTIP_FORMAT_TYPES.DECIMAL:
330
      return d3Format(tooltipFormat.format);
83✔
331
    case TOOLTIP_FORMAT_TYPES.DATE:
332
    case TOOLTIP_FORMAT_TYPES.DATE_TIME:
333
      return datetimeFormatter(null)(tooltipFormat.format);
458✔
334
    case TOOLTIP_FORMAT_TYPES.PERCENTAGE:
335
      return v => `${d3Format(TOOLTIP_FORMATS.DECIMAL_DECIMAL_FIXED_2.format)(v)}%`;
1✔
336
    case TOOLTIP_FORMAT_TYPES.BOOLEAN:
337
      return getBooleanFormatter(tooltipFormat.format);
4✔
338
    default:
339
      return defaultFormatter;
×
340
  }
341
}
342

343
export function getBooleanFormatter(format: string): FieldFormatter {
344
  switch (format) {
4!
345
    case '01':
346
      return (v: boolean) => (v ? '1' : '0');
2✔
347
    case 'yn':
348
      return (v: boolean) => (v ? 'yes' : 'no');
2✔
349
    default:
350
      return defaultFormatter;
×
351
  }
352
}
353
// Allow user to specify custom tooltip format via config
354
export function applyCustomFormat(format, field: {type?: string}): FieldFormatter {
355
  switch (field.type) {
6!
356
    case ALL_FIELD_TYPES.real:
357
    case ALL_FIELD_TYPES.integer:
358
      return d3Format(format);
5✔
359
    case ALL_FIELD_TYPES.date:
360
    case ALL_FIELD_TYPES.timestamp:
361
      return datetimeFormatter(null)(format);
1✔
362
    default:
363
      return v => v;
×
364
  }
365
}
366

367
function formatLargeNumber(n) {
368
  // SI-prefix with 4 significant digits
369
  return d3Format('.4~s')(n);
2✔
370
}
371

372
export function formatNumber(n: number, type?: string): string {
373
  switch (type) {
47✔
374
    case ALL_FIELD_TYPES.integer:
375
      if (n < 0) {
13✔
376
        return `-${formatNumber(-n, 'integer')}`;
1✔
377
      }
378
      if (n < 1000) {
12✔
379
        return `${Math.round(n)}`;
10✔
380
      }
381
      if (n < 10 * 1000) {
2✔
382
        return d3Format(',')(Math.round(n));
1✔
383
      }
384
      return formatLargeNumber(n);
1✔
385
    case ALL_FIELD_TYPES.real:
386
      if (n < 0) {
31!
387
        return `-${formatNumber(-n, 'number')}`;
×
388
      }
389
      if (n < 1000) {
31✔
390
        return d3Format('.4~r')(n);
30✔
391
      }
392
      if (n < 10 * 1000) {
1!
393
        return d3Format(',.2~f')(n);
×
394
      }
395
      return formatLargeNumber(n);
1✔
396

397
    default:
398
      return formatNumber(n, 'real');
3✔
399
  }
400
}
401

402
const transformation = {
11✔
403
  Y: Math.pow(10, 24),
404
  Z: Math.pow(10, 21),
405
  E: Math.pow(10, 18),
406
  P: Math.pow(10, 15),
407
  T: Math.pow(10, 12),
408
  G: Math.pow(10, 9),
409
  M: Math.pow(10, 6),
410
  k: Math.pow(10, 3),
411
  h: Math.pow(10, 2),
412
  da: Math.pow(10, 1),
413
  d: Math.pow(10, -1),
414
  c: Math.pow(10, -2),
415
  m: Math.pow(10, -3),
416
  μ: Math.pow(10, -6),
417
  n: Math.pow(10, -9),
418
  p: Math.pow(10, -12),
419
  f: Math.pow(10, -15),
420
  a: Math.pow(10, -18),
421
  z: Math.pow(10, -21),
422
  y: Math.pow(10, -24)
423
};
424

425
/**
426
 * Convert a formatted number from string back to number
427
 */
428
export function reverseFormatNumber(str: string): number {
429
  let returnValue: number | null = null;
16✔
430
  const strNum = str.trim().replace(/,/g, '');
16✔
431
  Object.entries(transformation).forEach(d => {
16✔
432
    if (strNum.includes(d[0])) {
320!
NEW
433
      returnValue = parseFloat(strNum) * d[1];
×
NEW
434
      return true;
×
435
    }
436
    return false;
320✔
437
  });
438

439
  // if no transformer found, convert to nuber regardless
440
  return returnValue === null ? Number(strNum) : returnValue;
16!
441
}
442

443
/**
444
 * Format epoch milliseconds with a format string
445
 * @type timezone
446
 */
447
export function datetimeFormatter(
448
  timezone?: string | null
449
): (format?: string) => (ts: number) => string {
450
  return timezone
480✔
451
    ? format => ts => moment.utc(ts).tz(timezone).format(format)
18✔
452
    : // return empty string instead of 'Invalid date' if ts is undefined/null
453
      format => ts => ts ? moment.utc(ts).format(format) : '';
493✔
454
}
455

456
export function notNullOrUndefined(d: any): boolean {
457
  return d !== undefined && d !== null;
88✔
458
}
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