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

keplergl / kepler.gl / 13494567712

24 Feb 2025 09:15AM UTC coverage: 66.16%. Remained the same
13494567712

Pull #3001

github

web-flow
Merge ddea80e02 into b98a39def
Pull Request #3001: [chore] fixes to lint warnings in Github's File Changes

6025 of 10616 branches covered (56.75%)

Branch coverage included in aggregate %.

8 of 10 new or added lines in 6 files covered. (80.0%)

61 existing lines in 1 file now uncovered.

12372 of 17191 relevant lines covered (71.97%)

88.18 hits per line

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

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

4
import {bisectLeft, quantileSorted as d3Quantile, extent} from 'd3-array';
5
import uniq from 'lodash.uniq';
6
import moment from 'moment';
7

8
import {notNullorUndefined, toArray} from '@kepler.gl/common-utils';
9
import {ALL_FIELD_TYPES, SCALE_FUNC, SCALE_TYPES, NO_VALUE_COLOR} from '@kepler.gl/constants';
10
// import {FilterProps, KeplerTable} from '@kepler.gl/layers';
11
import {
12
  AggregatedBin,
13
  ColorMap,
14
  ColorRange,
15
  HexColor,
16
  KeplerLayer as Layer,
17
  MapState,
18
  VisualChannel,
19
  VisualChannelDomain,
20
  RGBColor,
21
  RGBAColor,
22
  ColorUI,
23
  Field
24
} from '@kepler.gl/types';
25

26
import {isRgbColor, rgbToHex, hexToRgb} from './color-utils';
27
import {DataContainerInterface} from './data-container-interface';
28
import {formatNumber, isNumber, reverseFormatNumber, unique} from './data-utils';
29
import {getTimeWidgetHintFormatter} from './filter-utils';
30
import {isPlainObject} from './utils';
31

32
export type ColorBreak = {
33
  data: HexColor;
34
  label: string;
35
  range: number[];
36
  inputs: number[];
37
};
38
export type ColorBreakOrdinal = {
39
  data: HexColor;
40
  label: string | number | string[] | number[] | null;
41
};
42

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

45
// TODO isolate types - depends on @kepler.gl/layers
46
type FilterProps = any;
47
type KeplerTable = any;
48

49
export type LabelFormat = (n: number, type?: string) => string;
50
type dataValueAccessor = <T>(param: T) => T;
51
type dataContainerValueAccessor = (d: {index: number}, dc: DataContainerInterface) => any;
52
type sort = (a: any, b: any) => any;
53
/**
54
 * return quantile domain for an array of data
55
 */
56
export function getQuantileDomain(
57
  data: any[],
58
  valueAccessor?: dataValueAccessor,
59
  sortFunc?: sort
60
): number[] {
61
  const values = typeof valueAccessor === 'function' ? data.map(valueAccessor) : data;
50✔
62

63
  return values.filter(notNullorUndefined).sort(sortFunc);
50✔
64
}
65

66
/**
67
 * return ordinal domain for a data container
68
 */
69
export function getOrdinalDomain(
70
  dataContainer: DataContainerInterface,
71
  valueAccessor: dataContainerValueAccessor
72
): string[] {
73
  const values = dataContainer.mapIndex(valueAccessor);
50✔
74

75
  return unique(values).filter(notNullorUndefined).sort();
50✔
76
}
77

78
/**
79
 * return linear domain for an array of data
80
 */
81
export function getLinearDomain(
82
  data: number[],
83
  valueAccessor?: dataValueAccessor
84
): [number, number] {
85
  const range = typeof valueAccessor === 'function' ? extent(data, valueAccessor) : extent(data);
132✔
86
  return range.map((d: undefined | number, i: number) => (d === undefined ? i : d)) as [
264✔
87
    number,
88
    number
89
  ];
90
}
91

92
/**
93
 * return linear domain for an array of data. A log scale domain cannot contain 0
94
 */
95
export function getLogDomain(data: any[], valueAccessor: dataValueAccessor): [number, number] {
96
  const [d0, d1] = getLinearDomain(data, valueAccessor);
3✔
97
  return [d0 === 0 ? 1e-5 : d0, d1];
3✔
98
}
99

100
export type DomainStops = {
101
  stops: number[];
102
  z: number[];
103
};
104

105
/**
106
 * whether field domain is stops
107
 */
108
export function isDomainStops(domain: unknown): domain is DomainStops {
109
  return isPlainObject(domain) && Array.isArray(domain.stops) && Array.isArray(domain.z);
161✔
110
}
111

112
export type DomainQuantiles = {
113
  quantiles: number[];
114
  z: number[];
115
};
116

117
/**
118
 * whether field domain is quantiles
119
 */
120
export function isDomainQuantile(domain: any): domain is DomainQuantiles {
121
  return isPlainObject(domain) && Array.isArray(domain.quantiles) && Array.isArray(domain.z);
161!
122
}
123

124
/**
125
 * get the domain at zoom
126
 */
127
export function getThresholdsFromQuantiles(
128
  quantiles: number[],
129
  buckets: number
130
): (number | undefined)[] {
131
  const thresholds = [];
3✔
132
  if (!Number.isFinite(buckets) || buckets < 1) {
3✔
133
    return [quantiles[0], quantiles[quantiles.length - 1]];
1✔
134
  }
135
  for (let i = 1; i < buckets; i++) {
2✔
136
    // position in sorted array
137
    const position = i / buckets;
2✔
138
    // @ts-ignore
139
    thresholds.push(d3Quantile(quantiles, position));
2✔
140
  }
141

142
  return thresholds;
2✔
143
}
144

145
/**
146
 * get the domain at zoom
147
 */
148
export function getDomainStepsbyZoom(domain: any[], steps: number[], z: number): any {
149
  const i = bisectLeft(steps, z);
13✔
150

151
  if (steps[i] === z) {
13✔
152
    // If z is an integer value exactly matching a step, return the corresponding domain
153
    return domain[i];
3✔
154
  }
155
  // Otherwise, return the next coarsest domain
156
  return domain[Math.max(i - 1, 0)];
10✔
157
}
158

159
/**
160
 * Get d3 scale function
161
 */
162
export function getScaleFunction(
163
  scale: string,
164
  range: any[] | IterableIterator<any>,
165
  domain: (number | undefined)[] | string[] | IterableIterator<any>,
166
  fixed?: boolean
167
): D3ScaleFunction {
168
  const scaleFunction = SCALE_FUNC[fixed ? 'linear' : scale]()
11!
169
    .domain(domain)
170
    .range(fixed ? domain : range);
11!
171
  scaleFunction.scaleType = fixed ? 'linear' : scale;
11!
172
  return scaleFunction;
11✔
173
}
174

175
/**
176
 * Get threshold scale color labels
177
 */
178
function getThresholdLabels(
179
  scale: D3ScaleFunction,
180
  labelFormat: LabelFormat
181
): Omit<ColorBreak, 'data'>[] {
182
  const genLength = scale.range().length;
4✔
183
  return scale.range().map((d, i) => {
4✔
184
    const invert = scale.invertExtent(d);
15✔
185
    const inputs = [
15✔
186
      i === 0 ? null : reverseFormatNumber(labelFormat(invert[0])),
15✔
187
      i === genLength - 1 ? null : reverseFormatNumber(labelFormat(invert[1]))
15✔
188
    ];
189
    return {
15✔
190
      // raw value
191
      range: invert,
192
      // formatted value
193
      inputs,
194
      label:
195
        i === 0
15✔
196
          ? `Less than ${labelFormat(invert[1])}`
197
          : i === genLength - 1
11✔
198
          ? `${labelFormat(invert[0])} or more`
199
          : `${labelFormat(invert[0])} to ${labelFormat(invert[1])}`
200
    };
201
  });
202
}
203

204
/**
205
 * Get linear / quant scale color labels
206
 */
207
function getScaleLabels(
208
  scale: D3ScaleFunction,
209
  labelFormat: LabelFormat
210
): Omit<ColorBreak, 'data'>[] {
211
  return scale.range().map((d, i) => {
12✔
212
    // @ts-ignore
213
    const invert = scale.invertExtent(d);
44✔
214
    const inputs = [
44✔
215
      reverseFormatNumber(labelFormat(invert[0])),
216
      reverseFormatNumber(labelFormat(invert[1]))
217
    ];
218

219
    return {
44✔
220
      label: `${labelFormat(invert[0])} to ${labelFormat(invert[1])}`,
221
      // raw value
222
      range: invert,
223
      // formatted value
224
      inputs
225
    };
226
  });
227
}
228

229
const customScaleLabelFormat = n => (n ? formatNumber(n, 'real') : 'no value');
44!
230
/**
231
 * Get linear / quant scale color breaks
232
 */
233
export function getQuantLegends(scale: D3ScaleFunction, labelFormat: LabelFormat): ColorBreak[] {
234
  if (typeof scale.invertExtent !== 'function') {
16!
235
    return [];
×
236
  }
237
  const thresholdLabelFormat = (n, type) =>
16✔
238
    n && labelFormat ? labelFormat(n) : n ? formatNumber(n, type) : 'no value';
×
239
  const labels =
240
    scale.scaleType === 'threshold'
16!
241
      ? getThresholdLabels(scale, thresholdLabelFormat)
242
      : scale.scaleType === 'custom'
16✔
243
      ? getThresholdLabels(scale, customScaleLabelFormat)
244
      : getScaleLabels(scale, labelFormat);
245

246
  const data = scale.range();
16✔
247

248
  return labels.map((label, index) => ({
59✔
249
    data: Array.isArray(data[index]) ? rgbToHex(data[index]) : data[index],
59✔
250
    ...label
251
  }));
252
}
253

254
/**
255
 * Get ordinal color scale legends
256
 */
257
export function getOrdinalLegends(scale: D3ScaleFunction): ColorBreakOrdinal[] {
258
  const domain = scale.domain();
4✔
259
  const labels = scale.domain();
4✔
260
  const data = domain.map(scale);
4✔
261

262
  return data.map((datum, index) => ({
12✔
263
    data: isRgbColor(datum) ? rgbToHex(datum) : datum,
12!
264
    label: labels[index]
265
  }));
266
}
267

268
const defaultFormat = d => d;
15✔
269

270
const getTimeLabelFormat = domain => {
15✔
271
  const formatter = getTimeWidgetHintFormatter(domain);
×
272
  return val => moment.utc(val).format(formatter);
×
273
};
274

275
export function getQuantLabelFormat(domain, fieldType) {
276
  // quant scale can only be assigned to linear Fields: real, timestamp, integer
277
  return fieldType === ALL_FIELD_TYPES.timestamp
15!
278
    ? getTimeLabelFormat(domain)
279
    : !fieldType
15!
280
    ? defaultFormat
281
    : n => (isNumber(n) ? formatNumber(n, fieldType) : 'no value');
168✔
282
}
283

284
/**
285
 * Get legends for scale
286
 */
287
export function getLegendOfScale({
288
  scale,
289
  scaleType,
290
  labelFormat,
291
  fieldType
292
}: {
293
  scale?: D3ScaleFunction | null;
294
  scaleType: string;
295
  labelFormat?: LabelFormat;
296
  fieldType: string | null | undefined;
297
}): ColorBreak[] | ColorBreakOrdinal[] {
298
  if (!scale || scale.byZoom) {
19✔
299
    return [];
1✔
300
  }
301
  if (
18✔
302
    scaleType === SCALE_TYPES.ordinal ||
46✔
303
    scaleType === SCALE_TYPES.customOrdinal ||
304
    fieldType === ALL_FIELD_TYPES.string
305
  ) {
306
    return getOrdinalLegends(scale);
4✔
307
  }
308

309
  const formatLabel = labelFormat || getQuantLabelFormat(scale.domain(), fieldType);
14✔
310

311
  return getQuantLegends(scale, formatLabel);
14✔
312
}
313

314
/**
315
 * Get color scale function
316
 */
317
export function getLayerColorScale({
318
  range,
319
  domain,
320
  scaleType,
321
  layer
322
}: {
323
  range: ColorRange | null | undefined;
324
  domain: VisualChannelDomain;
325
  scaleType: string;
326
  layer: Layer;
327
  isFixed?: boolean;
328
}): D3ScaleFunction | null {
329
  if (range && domain && scaleType) {
19✔
330
    return layer.getColorScale(scaleType, domain, range);
18✔
331
  }
332
  return null;
1✔
333
}
334

335
/**
336
 * Convert colorRange.colorMap into color breaks UI input
337
 */
338
export function initializeLayerColorMap(layer: Layer, visualChannel: VisualChannel): ColorMap {
339
  const domain = layer.config[visualChannel.domain];
1✔
340
  const range = layer.config.visConfig[visualChannel.range];
1✔
341
  const scaleType = layer.config[visualChannel.scale];
1✔
342
  const field = layer.config[visualChannel.field];
1✔
343

344
  const scale = getLayerColorScale({
1✔
345
    range,
346
    domain,
347
    scaleType,
348
    layer
349
  });
350

351
  const colorBreaks = getLegendOfScale({
1✔
352
    scale: scale?.byZoom ? scale(0) : scale,
1!
353
    scaleType,
354
    fieldType: field.type
355
  });
356
  return colorBreaksToColorMap(colorBreaks);
1✔
357
}
358

359
/**
360
 * Get visual chanel scale function if it's based on zoom
361
 */
362
export function getVisualChannelScaleByZoom({
363
  scale,
364
  layer,
365
  mapState
366
}: {
367
  scale: D3ScaleFunction | null;
368
  layer: Layer;
369
  mapState?: MapState;
370
}): D3ScaleFunction | null {
371
  if (scale?.byZoom) {
5!
372
    const z = layer.meta?.getZoom ? layer.meta.getZoom(mapState) : mapState?.zoom;
×
373
    scale = Number.isFinite(z) ? scale(z) : null;
×
374
  }
375
  return scale;
5✔
376
}
377

378
/**
379
 * Get categorical colorMap from colors and domain (unique values)
380
 */
381
export function getCategoricalColorMap(
382
  colors: string[],
383
  domain: (string | number | string[] | number[] | null)[]
384
): any {
385
  // colorMap: [string | string[], hexstring]
386
  const colorToUniqueValues = {};
×
387
  const uniqueValues = unique(domain).filter(notNullorUndefined).sort();
×
388
  // each unique value assign to a color, the rest unique values assign to last color
389
  const lastColor = colors[colors.length - 1];
×
390
  for (let i = 0; i < uniqueValues.length; ++i) {
×
391
    if (i < colors.length) {
×
392
      colorToUniqueValues[colors[i]] = uniqueValues[i];
×
393
    } else {
394
      colorToUniqueValues[lastColor] = [
×
395
        ...(Array.isArray(colorToUniqueValues[lastColor])
×
396
          ? colorToUniqueValues[lastColor]
397
          : [colorToUniqueValues[lastColor]]),
398
        uniqueValues[i]
399
      ];
400
    }
401
  }
402

403
  const colorMap = colors.map(color => {
×
404
    if (color in colorToUniqueValues) {
×
405
      return [colorToUniqueValues[color], color];
×
406
    }
407
    return [null, color];
×
408
  });
409

410
  return colorMap;
×
411
}
412

413
/**
414
 * Get categorical colorBreaks from colorMap
415
 */
416
export function colorMapToCategoricalColorBreaks(
417
  colorMap?: ColorMap | null
418
): ColorBreakOrdinal[] | null {
419
  if (!colorMap) {
×
420
    return null;
×
421
  }
NEW
422
  const colorBreaks = colorMap.map(([value, color], i) => {
×
423
    return {
×
424
      data: color,
425
      label: value
426
    };
427
  });
428

429
  return colorBreaks;
×
430
}
431

432
/**
433
 * create categorical colorMap from colorBreaks
434
 */
435
export function colorBreaksToCategoricalColorMap(colorBreaks: ColorBreakOrdinal[]): ColorMap {
436
  // colorMap: [string | string[], hexstring]
437
  const colors = uniq(colorBreaks.map(cb => cb.data));
×
438
  const values = uniq(colorBreaks.map(cb => cb.label));
×
439

440
  return getCategoricalColorMap(colors, values);
×
441
}
442

443
/**
444
 * Convert color breaks UI input into colorRange.colorMap
445
 */
446
export function colorBreaksToColorMap(colorBreaks: ColorBreak[] | ColorBreakOrdinal[]): ColorMap {
447
  const colorMap = colorBreaks.map((colorBreak, i) => {
4✔
448
    // [value, hex]
449
    return [
14✔
450
      colorBreak.inputs
14✔
451
        ? i === colorBreaks.length - 1
11✔
452
          ? null // last
453
          : colorBreak.inputs[1]
454
        : colorBreak.label,
455
      colorBreak.data
456
    ];
457
  });
458

459
  // @ts-ignore tuple
460
  return colorMap;
4✔
461
}
462

463
/**
464
 * Convert colorRange.colorMap into color breaks UI input
465
 */
466
export function colorMapToColorBreaks(colorMap?: ColorMap | null): ColorBreak[] | null {
467
  if (!colorMap) {
4!
468
    return null;
×
469
  }
470
  const colorBreaks = colorMap.map(([value, color], i) => {
4✔
471
    const range =
472
      i === 0
16✔
473
        ? // first
474
          [-Infinity, value]
475
        : // last
476
        i === colorMap.length - 1
12✔
477
        ? [colorMap[i - 1][0], Infinity]
478
        : // else
479
          [colorMap[i - 1][0], value];
480
    return {
16✔
481
      data: color,
482
      range,
483
      inputs: range,
484
      label:
485
        // first
486
        i === 0
16✔
487
          ? `Less than ${value}`
488
          : // last
489
          i === colorMap.length - 1
12✔
490
          ? `${colorMap[i - 1][0]} or more`
491
          : `${colorMap[i - 1][0]} to ${value}`
492
    };
493
  });
494

495
  // @ts-ignore implement conversion for ordinal
496
  return colorBreaks;
4✔
497
}
498

499
/**
500
 * Whether color breaks is for numeric field
501
 */
502
export function isNumericColorBreaks(colorBreaks: unknown): colorBreaks is ColorBreak[] {
503
  return Boolean(Array.isArray(colorBreaks) && colorBreaks.length && colorBreaks[0].inputs);
91✔
504
}
505

506
// return domainMin, domainMax, histogramMean
507
export function getHistogramDomain({
508
  aggregatedBins,
509
  columnStats,
510
  dataset,
511
  fieldValueAccessor
512
}: {
513
  aggregatedBins?: AggregatedBin[];
514
  columnStats?: FilterProps['columnStats'];
515
  dataset?: KeplerTable;
516
  fieldValueAccessor: (idx: unknown) => number;
517
}) {
518
  let domainMin = Number.POSITIVE_INFINITY;
11✔
519
  let domainMax = Number.NEGATIVE_INFINITY;
11✔
520
  let nValid = 0;
11✔
521
  let domainSum = 0;
11✔
522

523
  if (aggregatedBins) {
11✔
524
    Object.values(aggregatedBins).forEach(bin => {
1✔
525
      const val = bin.value;
4✔
526
      if (isNumber(val)) {
4!
527
        if (val < domainMin) domainMin = val;
4✔
528
        if (val > domainMax) domainMax = val;
4!
529
        domainSum += val;
4✔
530
        nValid += 1;
4✔
531
      }
532
    });
533
  } else {
534
    if (columnStats && columnStats.quantiles && columnStats.mean) {
10!
535
      // no need to recalcuate min/max/mean if its already in columnStats
536
      return [
×
537
        columnStats.quantiles[0].value,
538
        columnStats.quantiles[columnStats.quantiles.length - 1].value,
539
        columnStats.mean
540
      ];
541
    }
542
    if (dataset && fieldValueAccessor) {
10✔
543
      dataset.allIndexes.forEach((x, i) => {
9✔
544
        const val = fieldValueAccessor(x);
216✔
545
        if (isNumber(val)) {
216✔
546
          if (val < domainMin) domainMin = val;
112✔
547
          if (val > domainMax) domainMax = val;
112✔
548
          domainSum += val;
112✔
549
          nValid += 1;
112✔
550
        }
551
      });
552
    }
553
  }
554
  const histogramMean = nValid > 0 ? domainSum / nValid : 0;
11✔
555
  return [nValid > 0 ? domainMin : 0, nValid > 0 ? domainMax : 0, histogramMean];
11✔
556
}
557

558
export function resetCategoricalColorMapByIndex(colorMap: ColorMap, index: number): any {
559
  if (!colorMap) {
×
560
    return colorMap;
×
561
  }
562
  const newColorMap = colorMap.map((cm, i) => {
×
563
    if (i === index) {
×
564
      return [null, cm[1]];
×
565
    }
566
    return cm;
×
567
  });
568
  return newColorMap;
×
569
}
570

571
/**
572
 * select rest categorical values for a colorMap by its index
573
 */
574
export function selectRestCategoricalColorMapByIndex(
575
  colorMap: ColorMap | null,
576
  index: number,
577
  uniqueValues?: number[] | string[]
578
): ColorMap | undefined | null {
579
  if (!colorMap || !uniqueValues) {
×
580
    return colorMap;
×
581
  }
582

583
  // find unique values that has not been used in current colorMap
584
  const uniqValueDict = Object.fromEntries(uniqueValues.map(val => [val, false]));
×
585
  colorMap.forEach(cm => {
×
586
    toArray(cm[0]).forEach(v => {
×
587
      if (v) uniqValueDict[v] = true;
×
588
    });
589
  });
590
  const rest = Object.keys(uniqValueDict).filter(v => !uniqValueDict[v]);
×
591

592
  // use the not used unique values in the selected color map
593
  const newColorMap = colorMap.map((cm, i) => {
×
594
    if (i === index) {
×
595
      return [[...rest, ...toArray(cm[0])], cm[1]];
×
596
    }
597
    return cm;
×
598
  }) as ColorMap;
599

600
  return newColorMap;
×
601
}
602

603
/**
604
 * remove a categorical value from a colorMap by its index
605
 */
606
export function removeCategoricalValueFromColorMap(
607
  colorMap: ColorMap | null | undefined,
608
  item: number | string,
609
  index: number
610
): ColorMap | null | undefined {
611
  if (!colorMap) {
×
612
    return colorMap;
×
613
  }
614
  const newColorMap = colorMap.map((cm, i) => {
×
615
    if (i === index) {
×
616
      if (!cm[0]) {
×
617
        return [null, cm[1]];
×
618
      }
619
      const currentUniqueValues = toArray(cm[0]);
×
620
      const updatedUniqueValues = currentUniqueValues.filter(v => v !== item);
×
621
      return [updatedUniqueValues, cm[1]];
×
622
    }
623
    return cm;
×
624
  }) as ColorMap;
625

626
  return newColorMap;
×
627
}
628

629
/**
630
 * add categorical values (from multisel dropdown) to a colorMap by its index
631
 */
632
export function addCategoricalValuesToColorMap(
633
  colorMap: ColorMap,
634
  items: (string | number)[],
635
  index: number
636
): ColorMap {
637
  if (!colorMap) {
×
638
    return colorMap;
×
639
  }
640

641
  const newColorMap = colorMap.map((cm, i) => {
×
642
    if (i === index) {
×
643
      if (!cm[0]) {
×
644
        return [items, cm[1]];
×
645
      }
646
      const currentUniqueValues = toArray(cm[0]);
×
647
      const updatedUniqueValues = uniq(currentUniqueValues.concat(items));
×
648
      return [updatedUniqueValues, cm[1]];
×
649
    }
650
    // remove value from other colorMap
651
    const currentUniqueValues = cm[0];
×
652
    if (Array.isArray(currentUniqueValues)) {
×
653
      const updatedUniqueValues: string[] | number[] = (currentUniqueValues as any[]).filter(
×
654
        v => !items.includes(v)
×
655
      );
656
      return [updatedUniqueValues, cm[1]];
×
657
    } else if (currentUniqueValues && items.includes(currentUniqueValues)) {
×
658
      return [null, cm[1]];
×
659
    }
660

661
    return cm;
×
662
  }) as ColorMap;
663

664
  return newColorMap;
×
665
}
666

667
/**
668
 * get a color scale func for categorical (custom ordinal) scale
669
 */
670
export function getCategoricalColorScale(
671
  colorDomain: number[] | string[],
672
  colorRange: ColorRange,
673
  useRgb = true
×
674
): (categoryValue: string | number) => RGBColor | RGBAColor {
675
  const cMap = colorRange.colorMap
×
676
    ? colorRange.colorMap
677
    : getCategoricalColorMap(colorRange.colors, colorDomain);
678

679
  const range: number[][] = [];
×
680
  const domain: string[] = [];
×
681
  cMap.forEach(cm => {
×
682
    if (Array.isArray(cm[0])) {
×
683
      cm[0].forEach(val => {
×
684
        domain.push(val);
×
685
        range.push(useRgb ? hexToRgb(cm[1]) : cm[1]);
×
686
      });
687
    } else {
688
      domain.push(cm[0]);
×
689
      range.push(useRgb ? hexToRgb(cm[1]) : cm[1]);
×
690
    }
691
  });
692

693
  const scale = getScaleFunction(SCALE_TYPES.customOrdinal, range, domain, false);
×
694
  scale.unknown(NO_VALUE_COLOR);
×
695
  return scale as any;
×
696
}
697

698
/**
699
 * initialize customPalette by custom scale or customOrdinal scale
700
 */
701
export function initCustomPaletteByCustomScale({
702
  scale,
703
  field,
704
  ordinalDomain,
705
  range,
706
  colorBreaks
707
}: {
708
  scale: string;
709
  field: Field;
710
  ordinalDomain?: number[] | string[];
711
  range: ColorRange;
712
  colorBreaks: ColorBreakOrdinal[] | null;
713
}): ColorUI['customPalette'] {
714
  const customPaletteName = `color.customPalette.${scale}.${field.name}`;
2✔
715
  // reuse range.colorMap if the field and scale not changed
716
  const reuseColorMap = range.colorMap && range.name === customPaletteName && range.type === scale;
2!
717
  const colorMap = reuseColorMap
2!
718
    ? range.colorMap
719
    : scale === SCALE_TYPES.customOrdinal && ordinalDomain
4!
720
    ? getCategoricalColorMap(range.colors, ordinalDomain)
721
    : colorBreaks && isNumericColorBreaks(colorBreaks)
6!
722
    ? colorBreaksToColorMap(colorBreaks)
723
    : null;
724
  const colors = reuseColorMap ? range.colors : colorMap ? colorMap.map(cm => cm[1]) : range.colors;
8!
725

726
  // update custom breaks
727
  const customPalette: ColorUI['customPalette'] = {
2✔
728
    category: 'Custom',
729
    name: customPaletteName,
730
    type: scale,
731
    colorMap,
732
    colors: colors || []
2!
733
  };
734

735
  return customPalette;
2✔
736
}
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