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

keplergl / kepler.gl / 12217941250

08 Dec 2024 02:04AM UTC coverage: 69.289%. Remained the same
12217941250

Pull #2827

github

web-flow
Merge 70f9afd34 into f476a1c4c
Pull Request #2827: [chore] ts fixes

5448 of 9109 branches covered (59.81%)

Branch coverage included in aggregate %.

4 of 9 new or added lines in 3 files covered. (44.44%)

18 existing lines in 3 files now uncovered.

11381 of 15179 relevant lines covered (74.98%)

95.1 hits per line

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

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

4
import {notNullorUndefined} from '@kepler.gl/common-utils';
5
import {ALL_FIELD_TYPES, ColorMap, ColorRange, SCALE_FUNC, SCALE_TYPES} from '@kepler.gl/constants';
6
import {Layer, VisualChannel, VisualChannelDomain} from '@kepler.gl/layers';
7
import {HexColor, MapState} from '@kepler.gl/types';
8
import {bisectLeft, quantileSorted as d3Quantile, extent} from 'd3-array';
9
import moment from 'moment';
10
import {isRgbColor, rgbToHex} from './color-utils';
11
import {DataContainerInterface} from './data-container-interface';
12
import {formatNumber, reverseFormatNumber, unique} from './data-utils';
13
import {getTimeWidgetHintFormatter} from './filter-utils';
14
import {isPlainObject} from './utils';
15

16
export type ColorBreak = {
17
  data: HexColor;
18
  label: string;
19
  range: number[];
20
  inputs: number[];
21
};
22
export type ColorBreakOrdinal = {
23
  data: HexColor;
24
  label: string;
25
};
26

27
export type D3ScaleFunction = Record<string, any> & ((x: any) => any);
28

29
export type LabelFormat = (n: number) => string;
30
type dataValueAccessor = <T>(param: T) => T;
31
type dataContainerValueAccessor = (d: {index: number}, dc: DataContainerInterface) => any;
32
type sort = (a: any, b: any) => any;
33
/**
34
 * return quantile domain for an array of data
35
 */
36
export function getQuantileDomain(
37
  data: any[],
38
  valueAccessor?: dataValueAccessor,
39
  sortFunc?: sort
40
): number[] {
41
  const values = typeof valueAccessor === 'function' ? data.map(valueAccessor) : data;
50✔
42

43
  return values.filter(notNullorUndefined).sort(sortFunc);
50✔
44
}
45

46
/**
47
 * return ordinal domain for a data container
48
 */
49
export function getOrdinalDomain(
50
  dataContainer: DataContainerInterface,
51
  valueAccessor: dataContainerValueAccessor
52
): string[] {
53
  const values = dataContainer.mapIndex(valueAccessor);
53✔
54

55
  return unique(values).filter(notNullorUndefined).sort();
53✔
56
}
57

58
/**
59
 * return linear domain for an array of data
60
 */
61
export function getLinearDomain(
62
  data: number[],
63
  valueAccessor?: dataValueAccessor
64
): [number, number] {
65
  const range = typeof valueAccessor === 'function' ? extent(data, valueAccessor) : extent(data);
129✔
66
  return range.map((d: undefined | number, i: number) => (d === undefined ? i : d)) as [
258✔
67
    number,
68
    number
69
  ];
70
}
71

72
/**
73
 * return linear domain for an array of data. A log scale domain cannot contain 0
74
 */
75
export function getLogDomain(data: any[], valueAccessor: dataValueAccessor): [number, number] {
76
  const [d0, d1] = getLinearDomain(data, valueAccessor);
3✔
77
  return [d0 === 0 ? 1e-5 : d0, d1];
3✔
78
}
79

80
/**
81
 * whether field domain is stops
82
 */
83
export function isDomainStops(domain: any): boolean {
UNCOV
84
  return isPlainObject(domain) && Array.isArray(domain.stops) && Array.isArray(domain.z);
×
85
}
86

87
/**
88
 * whether field domain is quantiles
89
 */
90
export function isDomainQuantile(domain: any): boolean {
UNCOV
91
  return isPlainObject(domain) && Array.isArray(domain.quantiles) && Array.isArray(domain.z);
×
92
}
93

94
/**
95
 * get the domain at zoom
96
 * @type {typeof import('./data-scale-utils').getThresholdsFromQuantiles}
97
 */
98
export function getThresholdsFromQuantiles(
99
  quantiles: number[],
100
  buckets: number
101
): (number | undefined)[] {
UNCOV
102
  const thresholds = [];
×
UNCOV
103
  if (!Number.isFinite(buckets) || buckets < 1) {
×
104
    return [quantiles[0], quantiles[quantiles.length - 1]];
×
105
  }
106
  for (let i = 1; i < buckets; i++) {
×
107
    // position in sorted array
108
    const position = i / buckets;
×
109
    // @ts-ignore
110
    thresholds.push(d3Quantile(quantiles, position));
×
111
  }
112

UNCOV
113
  return thresholds;
×
114
}
115

116
/**
117
 * get the domain at zoom
118
 * @type {typeof import('./data-scale-utils').getDomainStepsbyZoom}
119
 */
120
export function getDomainStepsbyZoom(domain: any[], steps: number[], z: number): any {
UNCOV
121
  const i = bisectLeft(steps, z);
×
122

123
  if (i === 0) {
×
UNCOV
124
    return domain[0];
×
125
  }
126
  return domain[i - 1];
×
127
}
128

129
/**
130
 * Get d3 scale function
131
 * @type {typeof import('./data-scale-utils').getScaleFunction}
132
 */
133
export function getScaleFunction(
134
  scale: string,
135
  range: any[] | IterableIterator<any>,
136
  domain: (number | undefined)[] | string[] | IterableIterator<any>,
137
  fixed?: boolean
138
): D3ScaleFunction {
139
  const scaleFunction = SCALE_FUNC[fixed ? 'linear' : scale]()
4!
140
    .domain(domain)
141
    .range(fixed ? domain : range);
4!
142
  scaleFunction.scaleType = fixed ? 'linear' : scale;
4!
143
  return scaleFunction;
4✔
144
}
145

146
/**
147
 * Get threshold scale color labels
148
 */
149
function getThresholdLabels(
150
  scale: D3ScaleFunction,
151
  labelFormat: LabelFormat
152
): Omit<ColorBreak, 'data'>[] {
153
  const genLength = scale.range().length;
1✔
154
  return scale.range().map((d, i) => {
1✔
155
    const invert = scale.invertExtent(d);
3✔
156
    const inputs = [
3✔
157
      i === 0 ? null : reverseFormatNumber(labelFormat(invert[0])),
3✔
158
      i === genLength - 1 ? null : reverseFormatNumber(labelFormat(invert[1]))
3✔
159
    ];
160
    return {
3✔
161
      // raw value
162
      range: invert,
163
      // formatted value
164
      inputs,
165
      label:
166
        i === 0
3✔
167
          ? `Less than ${labelFormat(invert[1])}`
168
          : i === genLength - 1
2✔
169
          ? `${labelFormat(invert[0])} or more`
170
          : `${labelFormat(invert[0])} to ${labelFormat(invert[1])}`
171
    };
172
  });
173
}
174

175
/**
176
 * Get linear / quant scale color labels
177
 */
178
function getScaleLabels(
179
  scale: D3ScaleFunction,
180
  labelFormat: LabelFormat
181
): Omit<ColorBreak, 'data'>[] {
182
  return scale.range().map((d, i) => {
3✔
183
    // @ts-ignore
184
    const invert = scale.invertExtent(d);
14✔
185
    const inputs = [
14✔
186
      reverseFormatNumber(labelFormat(invert[0])),
187
      reverseFormatNumber(labelFormat(invert[1]))
188
    ];
189

190
    return {
14✔
191
      label: `${labelFormat(invert[0])} to ${labelFormat(invert[1])}`,
192
      // raw value
193
      range: invert,
194
      // formatted value
195
      inputs
196
    };
197
  });
198
}
199

200
const customScaleLabelFormat = d => String(d);
11✔
201
/**
202
 * Get linear / quant scale color breaks
203
 */
204
export function getQuantLegends(scale: D3ScaleFunction, labelFormat: LabelFormat): ColorBreak[] {
205
  if (typeof scale.invertExtent !== 'function') {
4!
UNCOV
206
    return [];
×
207
  }
208
  const labels =
209
    scale.scaleType === 'threshold' || scale.scaleType === 'custom'
4✔
210
      ? getThresholdLabels(scale, customScaleLabelFormat)
211
      : getScaleLabels(scale, labelFormat);
212

213
  const data = scale.range();
4✔
214

215
  return labels.map((label, index) => ({
17✔
216
    data: Array.isArray(data[index]) ? rgbToHex(data[index]) : data[index],
17!
217
    ...label
218
  }));
219
}
220

221
/**
222
 * Get ordinal color scale legends
223
 */
224
export function getOrdinalLegends(scale: D3ScaleFunction): ColorBreakOrdinal[] {
225
  const domain = scale.domain();
4✔
226
  const labels = scale.domain();
4✔
227
  const data = domain.map(scale);
4✔
228

229
  return data.map((datum, index) => ({
12✔
230
    data: isRgbColor(datum) ? rgbToHex(datum) : datum,
12!
231
    label: labels[index]
232
  }));
233
}
234

235
const defaultFormat = d => d;
11✔
236

237
const getTimeLabelFormat = domain => {
11✔
UNCOV
238
  const formatter = getTimeWidgetHintFormatter(domain);
×
UNCOV
239
  return val => moment.utc(val).format(formatter);
×
240
};
241

242
export function getQuantLabelFormat(domain, fieldType) {
243
  // quant scale can only be assigned to linear Fields: real, timestamp, integer
244
  return fieldType === ALL_FIELD_TYPES.timestamp
4!
245
    ? getTimeLabelFormat(domain)
246
    : !fieldType
4!
247
    ? defaultFormat
248
    : n => formatNumber(n, fieldType);
56✔
249
}
250

251
/**
252
 * Get legends for scale
253
 */
254
export function getLegendOfScale({
255
  scale,
256
  scaleType,
257
  labelFormat,
258
  fieldType
259
}: {
260
  scale?: D3ScaleFunction | null;
261
  scaleType: string;
262
  labelFormat?: LabelFormat;
263
  fieldType: string | null | undefined;
264
}): ColorBreak[] | ColorBreakOrdinal[] {
265
  if (!scale || scale.byZoom) {
9✔
266
    return [];
1✔
267
  }
268
  if (scaleType === SCALE_TYPES.ordinal) {
8✔
269
    return getOrdinalLegends(scale);
4✔
270
  }
271

272
  const formatLabel = labelFormat || getQuantLabelFormat(scale.domain(), fieldType);
4✔
273

274
  return getQuantLegends(scale, formatLabel);
4✔
275
}
276

277
/**
278
 * Get color scale function
279
 */
280
export function getLayerColorScale({
281
  range,
282
  domain,
283
  scaleType,
284
  layer
285
}: {
286
  range: ColorRange | null | undefined;
287
  domain: VisualChannelDomain;
288
  scaleType: string;
289
  layer: Layer;
290
  isFixed?: boolean;
291
}): D3ScaleFunction | null {
292
  if (range && domain && scaleType) {
9✔
293
    return layer.getColorScale(scaleType, domain, range);
8✔
294
  }
295
  return null;
1✔
296
}
297

298
/**
299
 * Convert colorRange.colorMap into color breaks UI input
300
 */
301
export function initializeLayerColorMap(layer: Layer, visualChannel: VisualChannel): ColorMap {
302
  const domain = layer.config[visualChannel.domain];
3✔
303
  const range = layer.config.visConfig[visualChannel.range];
3✔
304
  const scaleType = layer.config[visualChannel.scale];
3✔
305
  const field = layer.config[visualChannel.field];
3✔
306

307
  const scale = getLayerColorScale({
3✔
308
    range,
309
    domain,
310
    scaleType,
311
    layer
312
  });
313

314
  const colorBreaks = getLegendOfScale({scale, scaleType, fieldType: field.type});
3✔
315

316
  return colorBreaksToColorMap(colorBreaks);
3✔
317
}
318

319
/**
320
 * Get visual chanel scale function if it's based on zoom
321
 */
322
export function getVisualChannelScaleByZoom({
323
  scale,
324
  layer,
325
  mapState
326
}: {
327
  scale: D3ScaleFunction | null;
328
  layer: Layer;
329
  mapState?: MapState;
330
}): D3ScaleFunction | null {
331
  if (scale?.byZoom) {
4!
UNCOV
332
    const z = layer.meta?.getZoom ? layer.meta.getZoom(mapState) : mapState?.zoom;
×
UNCOV
333
    scale = Number.isFinite(z) ? scale(z) : null;
×
334
  }
335
  return scale;
4✔
336
}
337

338
/**
339
 * Convert color breaks UI input into colorRange.colorMap
340
 */
341
export function colorBreaksToColorMap(colorBreaks: ColorBreak[] | ColorBreakOrdinal[]): ColorMap {
342
  const colorMap = colorBreaks.map((colorBreak, i) => {
3✔
343
    // [value, hex]
344
    return [
10✔
345
      colorBreak.inputs
10✔
346
        ? i === colorBreaks.length - 1
4✔
347
          ? null // last
348
          : colorBreak.inputs[1]
349
        : colorBreak.label,
350
      colorBreak.data
351
    ];
352
  });
353

354
  // @ts-ignore tuple
355
  return colorMap;
3✔
356
}
357

358
/**
359
 * Convert colorRange.colorMap into color breaks UI input
360
 */
361
export function colorMapToColorBreaks(colorMap?: ColorMap): ColorBreak[] | null {
362
  if (!colorMap) {
2!
UNCOV
363
    return null;
×
364
  }
365
  const colorBreaks = colorMap.map(([value, color], i) => {
2✔
366
    const range =
367
      i === 0
8✔
368
        ? // first
369
          [-Infinity, value]
370
        : // last
371
        i === colorMap.length - 1
6✔
372
        ? [colorMap[i - 1][0], Infinity]
373
        : // else
374
          [colorMap[i - 1][0], value];
375
    return {
8✔
376
      data: color,
377
      range,
378
      inputs: range,
379
      label:
380
        // first
381
        i === 0
8✔
382
          ? `Less than ${value}`
383
          : // last
384
          i === colorMap.length - 1
6✔
385
          ? `${colorMap[i - 1][0]} or more`
386
          : `${colorMap[i - 1][0]} to ${value}`
387
    };
388
  });
389

390
  // @ts-ignore implement conversion for ordinal
391
  return colorBreaks;
2✔
392
}
393

394
/**
395
 * Whether color breaks is for numeric field
396
 */
397
export function isNumericColorBreaks(colorBreaks: unknown): colorBreaks is ColorBreak[] {
398
  return Array.isArray(colorBreaks) && colorBreaks.length && colorBreaks[0].inputs;
30✔
399
}
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