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

keplergl / kepler.gl / 26338434855

23 May 2026 04:58PM UTC coverage: 57.394% (-0.4%) from 57.76%
26338434855

Pull #3451

github

web-flow
Merge 14079e6a7 into 9bbc9cd57
Pull Request #3451: feat: improvements to the trip layer

7255 of 15177 branches covered (47.8%)

Branch coverage included in aggregate %.

96 of 262 new or added lines in 19 files covered. (36.64%)

61 existing lines in 4 files now uncovered.

14758 of 23177 relevant lines covered (63.68%)

77.43 hits per line

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

86.11
/src/layers/src/base-layer.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import {COORDINATE_SYSTEM} from '@deck.gl/core';
5
import {GeoArrowTextLayer} from '@kepler.gl/deckgl-arrow-layers';
6
import {DataFilterExtension} from '@deck.gl/extensions';
7
import {TextLayer} from '@deck.gl/layers';
8
import {console as Console} from 'global/window';
9
import keymirror from 'keymirror';
10
import React from 'react';
11
import * as arrow from 'apache-arrow';
12
import DefaultLayerIcon from './default-layer-icon';
13
import {diffUpdateTriggers} from './layer-update';
14
import {getSatisfiedColumnMode, FindDefaultLayerPropsReturnValue} from './layer-utils';
15

16
import {
17
  CHANNEL_SCALES,
18
  CHANNEL_SCALE_SUPPORTED_FIELDS,
19
  DEFAULT_COLOR_UI,
20
  DEFAULT_HIGHLIGHT_COLOR,
21
  DEFAULT_LAYER_LABEL,
22
  DEFAULT_TEXT_LABEL,
23
  DataVizColors,
24
  FIELD_OPTS,
25
  LAYER_VIS_CONFIGS,
26
  MAX_GPU_FILTERS,
27
  NO_VALUE_COLOR,
28
  PROJECTED_PIXEL_SIZE_MULTIPLIER,
29
  SCALE_FUNC,
30
  SCALE_TYPES,
31
  TEXT_OUTLINE_MULTIPLIER,
32
  UNKNOWN_COLOR_KEY
33
} from '@kepler.gl/constants';
34
import {
35
  DataContainerInterface,
36
  DomainQuantiles,
37
  getApplicationConfig,
38
  getLatLngBounds,
39
  getSampleContainerData,
40
  hasColorMap,
41
  hexToRgb,
42
  isPlainObject,
43
  isDomainStops,
44
  updateColorRangeByMatchingPalette,
45
  isArrowTable
46
} from '@kepler.gl/utils';
47
import {generateHashId, toArray, notNullorUndefined} from '@kepler.gl/common-utils';
48
import {Datasets, GpuFilter, KeplerTable} from '@kepler.gl/table';
49
import {
50
  AggregatedBin,
51
  ColorRange,
52
  ColorUI,
53
  Field,
54
  Filter,
55
  GetVisChannelScaleReturnType,
56
  LayerVisConfigSettings,
57
  MapState,
58
  AnimationConfig,
59
  KeplerLayer,
60
  LayerBaseConfig,
61
  LayerColumns,
62
  LayerColumn,
63
  ColumnPairs,
64
  ColumnLabels,
65
  SupportedColumnMode,
66
  FieldPair,
67
  NestedPartial,
68
  RGBColor,
69
  ValueOf,
70
  VisualChannel,
71
  VisualChannels,
72
  VisualChannelDomain,
73
  VisualChannelField,
74
  VisualChannelScale
75
} from '@kepler.gl/types';
76
import {
77
  getScaleFunction,
78
  initializeLayerColorMap,
79
  getCategoricalColorScale,
80
  updateCustomColorRangeByColorUI
81
} from '@kepler.gl/utils';
82
import memoize from 'lodash/memoize';
83
import {
84
  initializeCustomPalette,
85
  isDomainQuantile,
86
  getDomainStepsbyZoom,
87
  getThresholdsFromQuantiles
88
} from '@kepler.gl/utils';
89

90
export type {
91
  AggregatedBin,
92
  LayerBaseConfig,
93
  VisualChannel,
94
  VisualChannels,
95
  VisualChannelDomain,
96
  VisualChannelField,
97
  VisualChannelScale
98
};
99

100
export type LayerBaseConfigPartial = {dataId: LayerBaseConfig['dataId']} & Partial<LayerBaseConfig>;
101

102
export type LayerColorConfig = {
103
  colorField: VisualChannelField;
104
  colorDomain: VisualChannelDomain;
105
  colorScale: VisualChannelScale;
106
};
107
export type LayerSizeConfig = {
108
  // color by size, domain is set by filters, field, scale type
109
  sizeDomain: VisualChannelDomain;
110
  sizeScale: VisualChannelScale;
111
  sizeField: VisualChannelField;
112
};
113
export type LayerHeightConfig = {
114
  heightField: VisualChannelField;
115
  heightDomain: VisualChannelDomain;
116
  heightScale: VisualChannelScale;
117
};
118
export type LayerStrokeColorConfig = {
119
  strokeColorField: VisualChannelField;
120
  strokeColorDomain: VisualChannelDomain;
121
  strokeColorScale: VisualChannelScale;
122
};
123
export type LayerCoverageConfig = {
124
  coverageField: VisualChannelField;
125
  coverageDomain: VisualChannelDomain;
126
  coverageScale: VisualChannelScale;
127
};
128
export type LayerRadiusConfig = {
129
  radiusField: VisualChannelField;
130
  radiusDomain: VisualChannelDomain;
131
  radiusScale: VisualChannelScale;
132
};
133
export type LayerWeightConfig = {
134
  weightField: VisualChannelField;
135
};
136

137
export type VisualChannelDescription = {
138
  label: string;
139
  measure: string | undefined;
140
};
141

142
type ColumnValidator = (column: LayerColumn, columns: LayerColumns, allFields: Field[]) => boolean;
143

144
export type UpdateTriggers = {
145
  [key: string]: UpdateTrigger;
146
};
147
export type UpdateTrigger = {
148
  [key: string]: any;
149
};
150
export type LayerBounds = [number, number, number, number];
151

152
/**
153
 * Approx. number of points to sample in a large data set
154
 */
155
export const LAYER_ID_LENGTH = 6;
13✔
156

157
const MAX_SAMPLE_SIZE = 5000;
13✔
158
const defaultDomain: [number, number] = [0, 1];
13✔
159
const dataFilterExtension = new DataFilterExtension({
13✔
160
  filterSize: MAX_GPU_FILTERS,
161
  countItems: getApplicationConfig().useOnFilteredItemsChange ?? false
13!
162
});
163

164
// eslint-disable-next-line @typescript-eslint/no-unused-vars
165
const defaultDataAccessor = dc => d => d;
68✔
166
const identity = d => d;
13✔
167
// Can't use fiedValueAccesor because need the raw data to render tooltip
168
// SHAN: Revisit here
169
export const defaultGetFieldValue = (field, d) => field.valueAccessor(d);
137✔
170

171
export const OVERLAY_TYPE_CONST = keymirror({
13✔
172
  deckgl: null,
173
  mapboxgl: null
174
});
175

176
export const layerColors = Object.values(DataVizColors).map(hexToRgb);
13✔
177
function* generateColor(): Generator<RGBColor> {
178
  let index = 0;
3✔
179
  while (index < layerColors.length + 1) {
3✔
180
    if (index === layerColors.length) {
1,218✔
181
      index = 0;
60✔
182
    }
183
    yield layerColors[index++];
1,218✔
184
  }
185
}
186

187
export type LayerInfoModal = {
188
  id: string;
189
  template: React.FC<void>;
190
  modalProps: {
191
    title: string;
192
  };
193
};
194

195
export const colorMaker = generateColor();
13✔
196

197
export type BaseLayerConstructorProps = {
198
  id?: string;
199
} & LayerBaseConfigPartial;
200

201
class Layer implements KeplerLayer {
202
  id: string;
203
  meta: Record<string, any>;
204
  visConfigSettings: {
205
    [key: string]: ValueOf<LayerVisConfigSettings>;
206
  };
207
  config: LayerBaseConfig & Partial<LayerColorConfig & LayerSizeConfig>;
208
  // TODO: define _oldDataUpdateTriggers
209
  _oldDataUpdateTriggers: any;
210

211
  isValid: boolean;
212
  errorMessage: string | null;
213
  filteredItemCount: {
214
    [deckLayerId: string]: number;
215
  };
216

217
  constructor(props: BaseLayerConstructorProps) {
218
    this.id = props.id || generateHashId(LAYER_ID_LENGTH);
948✔
219
    // meta
220
    this.meta = {};
948✔
221

222
    // visConfigSettings
223
    this.visConfigSettings = {};
948✔
224

225
    this.config = this.getDefaultLayerConfig(props);
948✔
226

227
    // set columnMode from supported columns
228
    if (!this.config.columnMode) {
948✔
229
      const {supportedColumnModes} = this;
404✔
230
      if (supportedColumnModes?.length) {
404!
231
        this.config.columnMode = supportedColumnModes[0]?.key;
×
232
      }
233
    }
234
    // then set column, columnMode should already been set
235
    this.config.columns = this.getLayerColumns(props.columns);
948✔
236

237
    // false indicates that the layer caused an error, and was disabled
238
    this.isValid = true;
948✔
239
    this.errorMessage = null;
948✔
240
    // item count
241
    this.filteredItemCount = {};
948✔
242
  }
243

244
  get layerIcon(): React.ElementType {
245
    return DefaultLayerIcon;
×
246
  }
247

248
  get overlayType(): keyof typeof OVERLAY_TYPE_CONST {
249
    return OVERLAY_TYPE_CONST.deckgl;
18✔
250
  }
251

252
  get type(): string | null {
253
    return null;
17✔
254
  }
255

256
  get name(): string | null {
257
    return this.type;
243✔
258
  }
259

260
  get isAggregated() {
261
    return false;
15✔
262
  }
263

264
  get requiredLayerColumns(): string[] {
265
    const {supportedColumnModes} = this;
697✔
266
    if (supportedColumnModes) {
697✔
267
      return supportedColumnModes.reduce<string[]>(
648✔
268
        (acc, obj) => (obj.requiredColumns ? acc.concat(obj.requiredColumns) : acc),
1,735!
269
        []
270
      );
271
    }
272
    return [];
49✔
273
  }
274

275
  get optionalColumns(): string[] {
276
    const {supportedColumnModes} = this;
775✔
277
    if (supportedColumnModes) {
775✔
278
      return supportedColumnModes.reduce<string[]>(
399✔
279
        (acc, obj) => (obj.optionalColumns ? acc.concat(obj.optionalColumns) : acc),
988✔
280
        []
281
      );
282
    }
283
    return [];
376✔
284
  }
285

286
  get noneLayerDataAffectingProps() {
287
    return ['label', 'opacity', 'thickness', 'isVisible', 'hidden'];
40✔
288
  }
289

290
  get visualChannels(): VisualChannels {
291
    return {
7,577✔
292
      color: {
293
        property: 'color',
294
        field: 'colorField',
295
        scale: 'colorScale',
296
        domain: 'colorDomain',
297
        range: 'colorRange',
298
        key: 'color',
299
        channelScaleType: CHANNEL_SCALES.color,
300
        nullValue: NO_VALUE_COLOR,
301
        defaultValue: config => config.color
17✔
302
      },
303
      size: {
304
        property: 'size',
305
        field: 'sizeField',
306
        scale: 'sizeScale',
307
        domain: 'sizeDomain',
308
        range: 'sizeRange',
309
        key: 'size',
310
        channelScaleType: CHANNEL_SCALES.size,
311
        nullValue: 0,
312
        defaultValue: 1
313
      }
314
    };
315
  }
316

317
  get columnValidators(): {[key: string]: ColumnValidator} {
318
    return {};
956✔
319
  }
320
  /*
321
   * Column pairs maps layer column to a specific field pairs,
322
   * By default, it is set to null
323
   */
324
  get columnPairs(): ColumnPairs | null {
325
    return null;
×
326
  }
327

328
  /**
329
   * Column labels if its different than column key
330
   */
331
  get columnLabels(): ColumnLabels | null {
332
    return null;
15✔
333
  }
334

335
  /*
336
   * Default point column pairs, can be used for point based layers: point, icon etc.
337
   */
338
  get defaultPointColumnPairs(): ColumnPairs {
339
    return {
30✔
340
      lat: {pair: ['lng', 'altitude'], fieldPairKey: 'lat'},
341
      lng: {pair: ['lat', 'altitude'], fieldPairKey: 'lng'},
342
      altitude: {pair: ['lng', 'lat'], fieldPairKey: 'altitude'}
343
    };
344
  }
345

346
  /*
347
   * Default link column pairs, can be used for link based layers: arc, line etc
348
   */
349
  get defaultLinkColumnPairs(): ColumnPairs {
350
    return {
3✔
351
      lat: {pair: ['lng', 'alt'], fieldPairKey: 'lat'},
352
      lng: {pair: ['lat', 'alt'], fieldPairKey: 'lng'},
353
      alt: {pair: ['lng', 'lat'], fieldPairKey: 'altitude'},
354

355
      lat0: {pair: 'lng0', fieldPairKey: 'lat'},
356
      lng0: {pair: 'lat0', fieldPairKey: 'lng'},
357
      alt0: {pair: ['lng0', 'lat0'], fieldPairKey: 'altitude'},
358

359
      lat1: {pair: 'lng1', fieldPairKey: 'lat'},
360
      lng1: {pair: 'lat1', fieldPairKey: 'lng'},
361
      alt1: {pair: ['lng1', 'lat1'], fieldPairKey: 'altitude'}
362
    };
363
  }
364

365
  /**
366
   * Return a React component for to render layer instructions in a modal
367
   * @returns {object} - an object
368
   * @example
369
   *  return {
370
   *    id: 'iconInfo',
371
   *    template: IconInfoModal,
372
   *    modalProps: {
373
   *      title: 'How to draw icons'
374
   *   };
375
   * }
376
   */
377
  get layerInfoModal(): LayerInfoModal | Record<string, LayerInfoModal> | null {
378
    return null;
24✔
379
  }
380

381
  /**
382
   * Returns which column modes this layer supports
383
   */
384
  get supportedColumnModes(): SupportedColumnMode[] | null {
385
    return null;
832✔
386
  }
387

388
  get supportedDatasetTypes(): string[] | null {
389
    return null;
3✔
390
  }
391

392
  /*
393
   * Given a dataset, automatically find props to create layer based on it
394
   * and return the props and previous found layers.
395
   * By default, no layers will be found
396
   */
397
  static findDefaultLayerProps(
398
    dataset: KeplerTable,
399
    foundLayers?: any[]
400
  ): FindDefaultLayerPropsReturnValue {
401
    return {props: [], foundLayers};
382✔
402
  }
403

404
  /**
405
   * Given a array of preset required column names
406
   * found field that has the same name to set as layer column
407
   *
408
   * @param {object} defaultFields
409
   * @param {object[]} allFields
410
   * @returns {object[] | null} all possible required layer column pairs
411
   */
412
  static findDefaultColumnField(defaultFields, allFields) {
413
    // find all matched fields for each required col
414
    const requiredColumns = Object.keys(defaultFields).reduce((prev, key) => {
383✔
415
      const requiredFields = allFields.filter(
383✔
416
        f => f.name === defaultFields[key] || defaultFields[key].includes(f.name)
2,921✔
417
      );
418

419
      prev[key] = requiredFields.length
383✔
420
        ? requiredFields.map(f => ({
152✔
421
            value: f.name,
422
            fieldIdx: f.fieldIdx
423
          }))
424
        : null;
425
      return prev;
383✔
426
    }, {});
427

428
    if (!Object.values(requiredColumns).every(Boolean)) {
383✔
429
      // if any field missing, return null
430
      return null;
258✔
431
    }
432

433
    return this.getAllPossibleColumnPairs(requiredColumns);
125✔
434
  }
435

436
  static getAllPossibleColumnPairs(requiredColumns) {
437
    // for multiple matched field for one required column, return multiple
438
    // combinations, e. g. if column a has 2 matched, column b has 3 matched
439
    // 6 possible column pairs will be returned
440
    const allKeys = Object.keys(requiredColumns);
128✔
441
    const pointers = allKeys.map((k, i) => (i === allKeys.length - 1 ? -1 : 0));
130✔
442
    const countPerKey = allKeys.map(k => requiredColumns[k].length);
130✔
443
    // TODO: Better typings
444
    const pairs: any[] = [];
128✔
445

446
    /* eslint-disable no-loop-func */
447
    while (incrementPointers(pointers, countPerKey, pointers.length - 1)) {
128✔
448
      const newPair = pointers.reduce((prev, cuur, i) => {
159✔
449
        prev[allKeys[i]] = requiredColumns[allKeys[i]][cuur];
165✔
450
        return prev;
165✔
451
      }, {});
452

453
      pairs.push(newPair);
159✔
454
    }
455
    /* eslint-enable no-loop-func */
456

457
    // recursively increment pointers
458
    function incrementPointers(pts, counts, index) {
459
      if (index === 0 && pts[0] === counts[0] - 1) {
290✔
460
        // nothing to increment
461
        return false;
128✔
462
      }
463

464
      if (pts[index] + 1 < counts[index]) {
162✔
465
        pts[index] = pts[index] + 1;
159✔
466
        return true;
159✔
467
      }
468

469
      pts[index] = 0;
3✔
470
      return incrementPointers(pts, counts, index - 1);
3✔
471
    }
472

473
    return pairs;
128✔
474
  }
475

476
  static hexToRgb(c) {
477
    return hexToRgb(c);
×
478
  }
479

480
  getDefaultLayerConfig(
481
    props: LayerBaseConfigPartial
482
  ): LayerBaseConfig & Partial<LayerColorConfig & LayerSizeConfig> {
483
    return {
1,344✔
484
      dataId: props.dataId,
485
      label: props.label || DEFAULT_LAYER_LABEL,
2,300✔
486
      color: props.color || colorMaker.next().value,
2,518✔
487
      // set columns later
488
      columns: {},
489
      isVisible: props.isVisible ?? true,
2,379✔
490
      isConfigActive: props.isConfigActive ?? false,
2,674✔
491
      highlightColor: props.highlightColor || DEFAULT_HIGHLIGHT_COLOR,
2,648✔
492
      hidden: props.hidden ?? false,
2,626✔
493

494
      // TODO: refactor this into separate visual Channel config
495
      // color by field, domain is set by filters, field, scale type
496
      colorField: null,
497
      colorDomain: [0, 1],
498
      colorScale: SCALE_TYPES.quantile,
499

500
      // color by size, domain is set by filters, field, scale type
501
      sizeDomain: [0, 1],
502
      sizeScale: SCALE_TYPES.linear,
503
      sizeField: null,
504

505
      visConfig: {},
506

507
      textLabel: [DEFAULT_TEXT_LABEL],
508

509
      colorUI: {
510
        color: DEFAULT_COLOR_UI,
511
        colorRange: DEFAULT_COLOR_UI
512
      },
513
      animation: {enabled: false},
514
      ...(props.columnMode ? {columnMode: props.columnMode} : {})
1,344✔
515
    };
516
  }
517

518
  /**
519
   * Get the description of a visualChannel config
520
   * @param key
521
   * @returns
522
   */
523
  getVisualChannelDescription(key: string): VisualChannelDescription {
524
    // e.g. label: Color, measure: Vehicle Type
525
    const channel = this.visualChannels[key];
50✔
526
    if (!channel) return {label: '', measure: undefined};
50!
527
    const rangeSettings = this.visConfigSettings[channel.range];
50✔
528
    const fieldSettings = this.config[channel.field];
50✔
529
    const label = rangeSettings?.label;
50✔
530
    return {
50✔
531
      label: typeof label === 'function' ? label(this.config) : label || '',
100!
532
      measure: fieldSettings
50✔
533
        ? fieldSettings.displayName || fieldSettings.name
4!
534
        : channel.defaultMeasure
535
    };
536
  }
537

538
  /**
539
   * Assign a field to layer column, return column config
540
   */
541
  assignColumn(key: string, field: {name: string; fieldIdx: number}): LayerColumns {
542
    // field value could be null for optional columns
543
    const update = field
×
544
      ? {
545
          value: field.name,
546
          fieldIdx: field.fieldIdx
547
        }
548
      : {value: null, fieldIdx: -1};
549

550
    return {
×
551
      ...this.config.columns,
552
      [key]: {
553
        ...this.config.columns?.[key],
554
        ...update
555
      }
556
    };
557
  }
558

559
  /**
560
   * Assign a field pair to column config, return column config
561
   */
562
  assignColumnPairs(key: string, fieldPairs: FieldPair): LayerColumns {
563
    if (!this.columnPairs || !this.columnPairs?.[key]) {
1!
564
      // should not end in this state
565
      return this.config.columns;
×
566
    }
567
    // key = 'lat'
568
    const {pair, fieldPairKey} = this.columnPairs?.[key] || {};
1!
569

570
    if (typeof fieldPairKey === 'string' && !fieldPairs[fieldPairKey]) {
1!
571
      // do not allow `key: undefined` to creep into the `updatedColumn` object
572
      return this.config.columns;
×
573
    }
574

575
    // pair = ['lng', 'alt] | 'lng'
576
    const updatedColumn = {
1✔
577
      ...this.config.columns,
578
      // @ts-expect-error fieldPairKey can be string[] here?
579
      [key]: fieldPairs[fieldPairKey]
580
    };
581

582
    const partnerKeys = toArray(pair);
1✔
583
    for (const partnerKey of partnerKeys) {
1✔
584
      if (
2✔
585
        this.config.columns[partnerKey] &&
6✔
586
        this.columnPairs?.[partnerKey] &&
587
        // @ts-ignore
588
        fieldPairs[this.columnPairs?.[partnerKey].fieldPairKey]
589
      ) {
590
        // @ts-ignore
591
        updatedColumn[partnerKey] = fieldPairs[this.columnPairs?.[partnerKey].fieldPairKey];
1✔
592
      }
593
    }
594

595
    return updatedColumn;
1✔
596
  }
597

598
  /**
599
   * Calculate a radius zoom multiplier to render points, so they are visible in all zoom level
600
   * @param {object} mapState
601
   * @param {number} mapState.zoom - actual zoom
602
   * @param {number | void} mapState.zoomOffset - zoomOffset when render in the plot container for export image
603
   * @returns {number}
604
   */
605
  getZoomFactor({zoom, zoomOffset = 0}) {
47✔
606
    return Math.pow(2, Math.max(14 - zoom + zoomOffset, 0));
56✔
607
  }
608

609
  /**
610
   * Calculate a elevation zoom multiplier to render points, so they are visible in all zoom level
611
   * @param {object} mapState
612
   * @param {number} mapState.zoom - actual zoom
613
   * @param {number=} mapState.zoomOffset - zoomOffset when render in the plot container for export image
614
   * @returns {number}
615
   */
616
  getElevationZoomFactor({zoom, zoomOffset = 0}: {zoom: number; zoomOffset?: number}): number {
8✔
617
    // enableElevationZoomFactor is used to support existing maps
618
    const {fixedHeight, enableElevationZoomFactor} = this.config.visConfig;
8✔
619
    return fixedHeight || enableElevationZoomFactor === false
8!
620
      ? 1
621
      : Math.pow(2, Math.max(8 - zoom + zoomOffset, 0));
622
  }
623

624
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
625
  formatLayerData(datasets: Datasets, oldLayerData?: unknown, animationConfig?: AnimationConfig) {
626
    return {};
×
627
  }
628

629
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
630
  renderLayer(...args: any[]): any[] {
631
    return [];
×
632
  }
633

634
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
635
  getHoverData(
636
    object: any,
637
    dataContainer: DataContainerInterface,
638
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
639
    fields?: Field[],
640
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
641
    animationConfig?: AnimationConfig,
642
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
643
    hoverInfo?: {index: number}
644
  ): any {
645
    if (!object) {
×
646
      return null;
×
647
    }
648

649
    // By default, each entry of layerData should have an index of a row in the original data container.
650
    // Each layer can implement its own getHoverData method
651
    return dataContainer.row(object.index);
×
652
  }
653

654
  getFilteredItemCount(): number | null {
655
    // use first layer
656
    if (Object.keys(this.filteredItemCount).length) {
×
657
      const firstLayer = Object.keys(this.filteredItemCount)[0];
×
658
      return this.filteredItemCount[firstLayer];
×
659
    }
660
    return null;
×
661
  }
662
  /**
663
   * When change layer type, try to copy over layer configs as much as possible
664
   * @param configToCopy - config to copy over
665
   * @param visConfigSettings - visConfig settings of config to copy
666
   * @param datasets - current datasets.
667
   * @param defaultLayerProps - default layer creation configurations for current layer and datasets.
668
   */
669
  assignConfigToLayer(
670
    configToCopy: LayerBaseConfig & Partial<LayerColorConfig & LayerSizeConfig>,
671
    visConfigSettings: {[key: string]: ValueOf<LayerVisConfigSettings>},
672
    datasets?: Datasets,
673
    defaultLayerProps?: FindDefaultLayerPropsReturnValue | null
674
  ) {
675
    // don't deep merge visualChannel field
676
    // don't deep merge color range, reversed: is not a key by default
677
    const shallowCopy = ['colorRange', 'strokeColorRange'].concat(
6✔
678
      Object.values(this.visualChannels).map(v => v.field)
21✔
679
    );
680

681
    // don't copy over domain and animation
682
    const notToCopy = ['animation'].concat(Object.values(this.visualChannels).map(v => v.domain));
21✔
683
    // if range is for the same property group copy it, otherwise, not to copy
684
    Object.values(this.visualChannels).forEach(v => {
6✔
685
      if (
21!
686
        configToCopy.visConfig[v.range] &&
39✔
687
        this.visConfigSettings[v.range] &&
688
        visConfigSettings[v.range].group !== this.visConfigSettings[v.range].group
689
      ) {
690
        notToCopy.push(v.range);
×
691
      }
692
    });
693

694
    // don't copy over visualChannel range
695
    const currentConfig = this.config;
6✔
696
    const copied = this.copyLayerConfig(currentConfig, configToCopy, {
6✔
697
      shallowCopy,
698
      notToCopy
699
    });
700

701
    // update columNode based on new columns
702
    if (this.config.columnMode && this.supportedColumnModes) {
6✔
703
      const dataset = datasets?.[this.config.dataId];
3✔
704
      // try to find a mode with all requied columns from the source config
705
      let satisfiedColumnMode = getSatisfiedColumnMode(
3✔
706
        this.supportedColumnModes,
707
        copied.columns,
708
        dataset?.fields
709
      );
710

711
      // if no suitable column mode found or no such columMode exists for the layer
712
      // then try use one of the automatically detected layer configs
713
      if (!satisfiedColumnMode) {
3✔
714
        const options = [
1✔
715
          ...(defaultLayerProps?.props || []),
1!
716
          ...(defaultLayerProps?.altProps || [])
2✔
717
        ];
718
        if (options.length) {
1!
719
          // Use the first of the default configurations
720
          const defaultColumnConfig = options[0].columns;
×
721

722
          satisfiedColumnMode = getSatisfiedColumnMode(
×
723
            this.supportedColumnModes,
724
            defaultColumnConfig,
725
            dataset?.fields
726
          );
727

728
          if (satisfiedColumnMode) {
×
729
            copied.columns = {
×
730
              ...copied.columns,
731
              ...defaultColumnConfig
732
            };
733
          }
734
        }
735
      }
736

737
      copied.columnMode = satisfiedColumnMode?.key || copied.columnMode;
3✔
738
    }
739

740
    this.updateLayerConfig(copied);
6✔
741
    // validate visualChannel field type and scale types
742
    Object.keys(this.visualChannels).forEach(channel => {
6✔
743
      this.validateVisualChannel(channel);
21✔
744
    });
745
  }
746

747
  /*
748
   * Recursively copy config over to an empty layer
749
   * when received saved config, or copy config over from a different layer type
750
   * make sure to only copy over value to existing keys
751
   * @param {object} currentConfig - existing config to be override
752
   * @param {object} configToCopy - new Config to copy over
753
   * @param {string[]} shallowCopy - array of properties to not to be deep copied
754
   * @param {string[]} notToCopy - array of properties not to copy
755
   * @returns {object} - copied config
756
   */
757
  copyLayerConfig(
758
    currentConfig,
759
    configToCopy,
760
    {shallowCopy = [], notToCopy = []}: {shallowCopy?: string[]; notToCopy?: string[]} = {}
157!
761
  ) {
762
    const copied: {columnMode?: string; columns?: LayerColumns} = {};
221✔
763
    Object.keys(currentConfig).forEach(key => {
221✔
764
      if (
2,421✔
765
        isPlainObject(currentConfig[key]) &&
3,024✔
766
        isPlainObject(configToCopy[key]) &&
767
        !shallowCopy.includes(key) &&
768
        !notToCopy.includes(key)
769
      ) {
770
        // recursively assign object value
771
        copied[key] = this.copyLayerConfig(currentConfig[key], configToCopy[key], {
58✔
772
          shallowCopy,
773
          notToCopy
774
        });
775
      } else if (notNullorUndefined(configToCopy[key]) && !notToCopy.includes(key)) {
2,363✔
776
        // copy
777
        copied[key] = configToCopy[key];
1,044✔
778
      } else {
779
        // keep existing
780
        copied[key] = currentConfig[key];
1,319✔
781
      }
782
    });
783

784
    return copied;
221✔
785
  }
786

787
  registerVisConfig(layerVisConfigs: {
788
    [key: string]: keyof LayerVisConfigSettings | ValueOf<LayerVisConfigSettings>;
789
  }) {
790
    Object.keys(layerVisConfigs).forEach(item => {
1,061✔
791
      const configItem = layerVisConfigs[item];
12,429✔
792
      if (typeof configItem === 'string' && LAYER_VIS_CONFIGS[configItem]) {
12,429✔
793
        // if assigned one of default LAYER_CONFIGS
794
        this.config.visConfig[item] = LAYER_VIS_CONFIGS[configItem].defaultValue;
8,851✔
795
        this.visConfigSettings[item] = LAYER_VIS_CONFIGS[configItem];
8,851✔
796
      } else if (
3,578✔
797
        typeof configItem === 'object' &&
7,064✔
798
        ['type', 'defaultValue'].every(p => Object.prototype.hasOwnProperty.call(configItem, p))
6,972✔
799
      ) {
800
        // if provided customized visConfig, and has type && defaultValue
801
        // TODO: further check if customized visConfig is valid
802
        this.config.visConfig[item] = configItem.defaultValue;
3,486✔
803
        this.visConfigSettings[item] = configItem;
3,486✔
804
      }
805
    });
806
  }
807

808
  getLayerColumns(propsColumns = {}) {
897✔
809
    const columnValidators = this.columnValidators || {};
1,108!
810
    const required = this.requiredLayerColumns.reduce(
1,108✔
811
      (accu, key) => ({
4,073✔
812
        ...accu,
813
        [key]: columnValidators[key]
4,073✔
814
          ? {
815
              value: propsColumns[key]?.value ?? null,
1,612✔
816
              fieldIdx: propsColumns[key]?.fieldIdx ?? -1,
1,566✔
817
              validator: columnValidators[key]
818
            }
819
          : {value: propsColumns[key]?.value ?? null, fieldIdx: propsColumns[key]?.fieldIdx ?? -1}
12,041✔
820
      }),
821
      {}
822
    );
823
    const optional = this.optionalColumns.reduce(
1,108✔
824
      (accu, key) => ({
1,273✔
825
        ...accu,
826
        [key]: {
827
          value: propsColumns[key]?.value ?? null,
2,543✔
828
          fieldIdx: propsColumns[key]?.fieldIdx ?? -1,
2,371✔
829
          optional: true
830
        }
831
      }),
832
      {}
833
    );
834

835
    const columns = {...required, ...optional};
1,108✔
836

837
    return columns;
1,108✔
838
  }
839

840
  updateLayerConfig<
841
    LayerConfig extends LayerBaseConfig &
842
      Partial<LayerColorConfig & LayerSizeConfig> = LayerBaseConfig
843
  >(newConfig: Partial<LayerConfig>): Layer {
844
    this.config = {...this.config, ...newConfig};
2,011✔
845
    return this;
2,011✔
846
  }
847

848
  updateLayerVisConfig(newVisConfig) {
849
    this.config.visConfig = {...this.config.visConfig, ...newVisConfig};
140✔
850
    return this;
140✔
851
  }
852

853
  updateLayerColorUI(prop: string, newConfig: NestedPartial<ColorUI>): Layer {
854
    const {colorUI: previous, visConfig} = this.config;
53✔
855

856
    if (!isPlainObject(newConfig) || typeof prop !== 'string') {
53!
857
      return this;
×
858
    }
859

860
    const colorUIProp = Object.entries(newConfig).reduce((accu, [key, value]) => {
53✔
861
      return {
60✔
862
        ...accu,
863
        [key]:
864
          isPlainObject(accu[key]) && isPlainObject(value)
161✔
865
            ? {...accu[key], ...(value as Record<string, unknown>)}
866
            : value
867
      };
868
    }, previous[prop] || DEFAULT_COLOR_UI);
53!
869

870
    const colorUI = {
53✔
871
      ...previous,
872
      [prop]: colorUIProp
873
    };
874

875
    this.updateLayerConfig({colorUI});
53✔
876
    // if colorUI[prop] is colorRange
877
    const isColorRange = visConfig[prop] && visConfig[prop].colors;
53✔
878

879
    if (isColorRange) {
53✔
880
      // if open dropdown and prop is color range
881
      // Automatically set colorRangeConfig's step and reversed
882
      this.updateColorUIByColorRange(newConfig, prop);
49✔
883

884
      // if changes in UI is made to 'reversed', 'steps' or steps
885
      // update current layer colorRange
886
      this.updateColorRangeByColorUI(newConfig, previous, prop);
49✔
887

888
      // if set colorRangeConfig to custom
889
      // initiate customPalette to be edited in the ui
890
      this.updateCustomPalette(newConfig, previous, prop);
49✔
891
    }
892

893
    return this;
53✔
894
  }
895

896
  // if set colorRangeConfig to custom palette or custom breaks
897
  // initiate customPalette to be edited in the ui
898
  updateCustomPalette(newConfig, previous, prop) {
899
    if (!newConfig.colorRangeConfig?.custom && !newConfig.colorRangeConfig?.customBreaks) {
49✔
900
      return;
38✔
901
    }
902

903
    if (newConfig.customPalette) {
11✔
904
      // if new config also set customPalette, no need to initiate new
905
      return;
2✔
906
    }
907
    const {colorUI, visConfig} = this.config;
9✔
908

909
    if (!visConfig[prop]) return;
9!
910
    // make copy of current color range to customPalette
911
    let customPalette = {
9✔
912
      ...visConfig[prop]
913
    };
914

915
    if (newConfig.colorRangeConfig.customBreaks && !customPalette.colorMap) {
9✔
916
      // find visualChanel
917
      const visualChannels = this.visualChannels;
1✔
918
      const channelKey = Object.keys(visualChannels).find(
1✔
919
        key => visualChannels[key].range === prop
1✔
920
      );
921
      if (!channelKey) {
1!
922
        // should never happn
923
        Console.warn(`updateColorUI: Can't find visual channel which range is ${prop}`);
×
924
        return;
×
925
      }
926
      // add name|type|category to updateCustomPalette if customBreaks, so that
927
      // colors will not be override as well when inverse palette with custom break
928
      // initiate colorMap from current scale
929

930
      const colorMap = initializeLayerColorMap(this, visualChannels[channelKey]);
1✔
931
      customPalette = initializeCustomPalette(visConfig[prop], colorMap);
1✔
932
    } else if (newConfig.colorRangeConfig.custom) {
8!
933
      customPalette = initializeCustomPalette(visConfig[prop]);
8✔
934
    }
935

936
    this.updateLayerConfig({
9✔
937
      colorUI: {
938
        ...colorUI,
939
        [prop]: {
940
          ...colorUI[prop],
941
          customPalette
942
        }
943
      }
944
    });
945
  }
946

947
  /**
948
   * if open dropdown and prop is color range
949
   * Automatically set colorRangeConfig's step and reversed
950
   * @param {*} newConfig
951
   * @param {*} prop
952
   */
953
  updateColorUIByColorRange(newConfig, prop) {
954
    const {colorUI, visConfig} = this.config;
49✔
955

956
    // when custom palette adds/removes step, the number in "Steps" input control
957
    // should be updated as well
958
    const isCustom = newConfig.customPalette?.category === 'Custom';
49✔
959
    const customStepsChanged = isCustom
49✔
960
      ? newConfig.customPalette.colors.length !== visConfig[prop].colors.length
961
      : false;
962

963
    if (typeof newConfig.showDropdown !== 'number' && !customStepsChanged) return;
49✔
964

965
    this.updateLayerConfig({
10✔
966
      colorUI: {
967
        ...colorUI,
968
        [prop]: {
969
          ...colorUI[prop],
970
          colorRangeConfig: {
971
            ...colorUI[prop].colorRangeConfig,
972
            steps: customStepsChanged
10✔
973
              ? colorUI[prop].customPalette.colors.length
974
              : visConfig[prop].colors.length,
975
            reversed: Boolean(visConfig[prop].reversed)
976
          }
977
        }
978
      }
979
    });
980
  }
981

982
  updateColorRangeByColorUI(newConfig, previous, prop) {
983
    // only update colorRange if changes in UI is made to 'reversed', 'steps' or steps
984
    const shouldUpdate =
985
      newConfig.colorRangeConfig &&
49✔
986
      ['reversed', 'steps', 'colorBlindSafe', 'type'].some(
987
        key =>
988
          Object.prototype.hasOwnProperty.call(newConfig.colorRangeConfig, key) &&
128✔
989
          newConfig.colorRangeConfig[key] !==
990
            (previous[prop] || DEFAULT_COLOR_UI).colorRangeConfig[key]
54!
991
      );
992
    if (!shouldUpdate) return;
49✔
993

994
    const {colorUI, visConfig} = this.config;
6✔
995

996
    // for custom palette, one can only 'reverse' the colors in custom palette.
997
    // changing 'steps', 'colorBindSafe', 'type' should fall back to predefined palette.
998
    const isCustomColorReversed =
999
      visConfig.colorRange.category === 'Custom' &&
6✔
1000
      newConfig.colorRangeConfig &&
1001
      Object.prototype.hasOwnProperty.call(newConfig.colorRangeConfig, 'reversed');
1002

1003
    const update = isCustomColorReversed
6✔
1004
      ? updateCustomColorRangeByColorUI(visConfig[prop], colorUI[prop].colorRangeConfig)
1005
      : updateColorRangeByMatchingPalette(visConfig[prop], colorUI[prop].colorRangeConfig);
1006

1007
    if (update) {
6!
1008
      this.updateLayerVisConfig({[prop]: update});
6✔
1009
    }
1010
  }
1011
  hasColumnValue(column?: LayerColumn) {
1012
    return Boolean(column && column.value && column.fieldIdx > -1);
1,090✔
1013
  }
1014
  hasRequiredColumn(column?: LayerColumn) {
1015
    return Boolean(column && (column.optional || this.hasColumnValue(column)));
129✔
1016
  }
1017
  /**
1018
   * Check whether layer has all columns
1019
   * @returns yes or no
1020
   */
1021
  hasAllColumns(): boolean {
1022
    const {columns, columnMode} = this.config;
554✔
1023
    // if layer has different column mode, check if have all required columns of current column Mode
1024
    if (columnMode) {
554✔
1025
      const currentColumnModes = (this.supportedColumnModes || []).find(
480✔
1026
        colMode => colMode.key === columnMode
491✔
1027
      );
1028
      return Boolean(
480✔
1029
        currentColumnModes !== undefined &&
958✔
1030
          currentColumnModes.requiredColumns?.every(colKey => this.hasColumnValue(columns[colKey]))
826✔
1031
      );
1032
    }
1033
    return Boolean(
74✔
1034
      columns &&
148✔
1035
        Object.values(columns).every((column?: LayerColumn) => this.hasRequiredColumn(column))
129✔
1036
    );
1037
  }
1038

1039
  /**
1040
   * Check whether layer has data
1041
   *
1042
   * @param {Array | Object} layerData
1043
   * @returns {boolean} yes or no
1044
   */
1045
  hasLayerData(layerData: {data: unknown[] | arrow.Table}) {
1046
    if (!layerData) {
38!
1047
      return false;
×
1048
    }
1049

1050
    return Boolean(
38✔
1051
      layerData.data &&
76!
1052
        ((layerData.data as unknown[]).length || (layerData.data as arrow.Table).numRows)
1053
    );
1054
  }
1055

1056
  isValidToSave(): boolean {
1057
    return Boolean(this.type && this.hasAllColumns());
143✔
1058
  }
1059

1060
  shouldRenderLayer(data): boolean {
1061
    return (
38✔
1062
      Boolean(this.type) &&
152✔
1063
      this.hasAllColumns() &&
1064
      this.hasLayerData(data) &&
1065
      typeof this.renderLayer === 'function'
1066
    );
1067
  }
1068

1069
  getColorScale(
1070
    colorScale: string,
1071
    colorDomain: VisualChannelDomain,
1072
    colorRange: ColorRange
1073
  ): GetVisChannelScaleReturnType {
1074
    if (colorScale === SCALE_TYPES.customOrdinal) {
136!
1075
      return getCategoricalColorScale(colorDomain, colorRange);
×
1076
    }
1077

1078
    if (hasColorMap(colorRange) && colorScale === SCALE_TYPES.custom) {
136✔
1079
      const cMap = new Map();
7✔
1080
      colorRange.colorMap?.forEach(([k, v]) => {
7✔
1081
        cMap.set(k, typeof v === 'string' ? hexToRgb(v) : v);
25!
1082
      });
1083

1084
      const scaleType = colorScale === SCALE_TYPES.custom ? colorScale : SCALE_TYPES.ordinal;
7!
1085

1086
      const scale = getScaleFunction(scaleType, cMap.values(), cMap.keys(), false);
7✔
1087
      scale.unknown(cMap.get(UNKNOWN_COLOR_KEY) || NO_VALUE_COLOR);
7✔
1088

1089
      return scale as GetVisChannelScaleReturnType;
7✔
1090
    }
1091
    return this.getVisChannelScale(colorScale, colorDomain, colorRange.colors.map(hexToRgb));
129✔
1092
  }
1093

1094
  accessVSFieldValue(_field, _indexKey) {
1095
    return defaultGetFieldValue;
157✔
1096
  }
1097
  /**
1098
   * Mapping from visual channels to deck.gl accesors
1099
   * @param param Parameters
1100
   * @param param.dataAccessor Access kepler.gl layer data from deck.gl layer
1101
   * @param param.dataContainer DataContainer to use use with dataAccessor
1102
   * @return {Object} attributeAccessors - deck.gl layer attribute accessors
1103
   */
1104
  getAttributeAccessors({
1105
    dataAccessor = defaultDataAccessor,
347✔
1106
    dataContainer,
1107
    indexKey
1108
  }: {
1109
    dataAccessor?: typeof defaultDataAccessor;
1110
    dataContainer: DataContainerInterface;
1111
    indexKey?: number | null;
1112
  }) {
1113
    const attributeAccessors: {[key: string]: any} = {};
455✔
1114

1115
    Object.keys(this.visualChannels).forEach(channel => {
455✔
1116
      const {
1117
        field,
1118
        fixed,
1119
        scale,
1120
        domain,
1121
        range,
1122
        accessor,
1123
        defaultValue,
1124
        getAttributeValue,
1125
        nullValue,
1126
        channelScaleType
1127
      } = this.visualChannels[channel];
1,589✔
1128

1129
      if (accessor) {
1,589✔
1130
        const shouldGetScale = this.config[field];
1,587✔
1131

1132
        if (shouldGetScale) {
1,587✔
1133
          const isFixed = fixed && this.config.visConfig[fixed];
145✔
1134

1135
          const scaleFunction =
1136
            channelScaleType === CHANNEL_SCALES.color
145✔
1137
              ? this.getColorScale(
1138
                  this.config[scale],
1139
                  this.config[domain],
1140
                  this.config.visConfig[range]
1141
                )
1142
              : this.getVisChannelScale(
1143
                  this.config[scale],
1144
                  this.config[domain],
1145
                  this.config.visConfig[range],
1146
                  isFixed
1147
                );
1148

1149
          const getFieldValue = this.accessVSFieldValue(this.config[field], indexKey);
145✔
1150

1151
          if (scaleFunction) {
145!
1152
            attributeAccessors[accessor] = scaleFunction.byZoom
145✔
1153
              ? memoize(z => {
1154
                  const scaleFunc = scaleFunction(z);
1✔
1155
                  return d =>
1✔
1156
                    this.getEncodedChannelValue(
6✔
1157
                      scaleFunc,
1158
                      dataAccessor(dataContainer)(d),
1159
                      this.config[field],
1160
                      nullValue,
1161
                      getFieldValue
1162
                    );
1163
                })
1164
              : d =>
1165
                  this.getEncodedChannelValue(
76✔
1166
                    scaleFunction,
1167
                    dataAccessor(dataContainer)(d),
1168
                    this.config[field],
1169
                    nullValue,
1170
                    getFieldValue
1171
                  );
1172

1173
            // set getFillColorByZoom to true
1174
            if (scaleFunction.byZoom) {
145✔
1175
              attributeAccessors[`${accessor}ByZoom`] = true;
1✔
1176
            }
1177
          }
1178
        } else if (typeof getAttributeValue === 'function') {
1,442✔
1179
          attributeAccessors[accessor] = getAttributeValue(this.config);
439✔
1180
        } else {
1181
          attributeAccessors[accessor] =
1,003✔
1182
            typeof defaultValue === 'function' ? defaultValue(this.config) : defaultValue;
1,003✔
1183
        }
1184

1185
        if (!attributeAccessors[accessor]) {
1,587✔
1186
          Console.warn(`Failed to provide accessor function for ${accessor || channel}`);
78!
1187
        }
1188
      }
1189
    });
1190

1191
    return attributeAccessors;
455✔
1192
  }
1193

1194
  getVisChannelScale(
1195
    scale: string,
1196
    domain: VisualChannelDomain | DomainQuantiles,
1197
    range: any,
1198
    fixed?: boolean
1199
  ): GetVisChannelScaleReturnType {
1200
    // if quantile is provided per zoom
1201
    if (isDomainQuantile(domain) && scale === SCALE_TYPES.quantile) {
158!
1202
      const zSteps = domain.z;
×
1203

1204
      const getScale = function getScaleByZoom(z) {
×
1205
        const scaleDomain = getDomainStepsbyZoom(domain.quantiles, zSteps, z);
×
1206
        const thresholds = getThresholdsFromQuantiles(scaleDomain, range.length);
×
1207

1208
        return getScaleFunction('threshold', range, thresholds, false);
×
1209
      };
1210

1211
      getScale.byZoom = true;
×
1212
      return getScale;
×
1213
    } else if (isDomainStops(domain)) {
158✔
1214
      // color is based on zoom
1215
      const zSteps = domain.z;
2✔
1216
      // get scale function by z
1217
      // {
1218
      //  z: [z, z, z],
1219
      //  stops: [[min, max], [min, max]],
1220
      //  interpolation: 'interpolate'
1221
      // }
1222

1223
      const getScale = function getScaleByZoom(z) {
2✔
1224
        const scaleDomain = getDomainStepsbyZoom(domain.stops, zSteps, z);
4✔
1225

1226
        return getScaleFunction(scale, range, scaleDomain, fixed);
4✔
1227
      };
1228

1229
      getScale.byZoom = true;
2✔
1230
      return getScale;
2✔
1231
    }
1232

1233
    return SCALE_FUNC[fixed ? 'linear' : scale]()
156✔
1234
      .domain(domain)
1235
      .range(fixed ? domain : range);
156✔
1236
  }
1237

1238
  /**
1239
   * Get longitude and latitude bounds of the data.
1240
   */
1241
  getPointsBounds(
1242
    dataContainer: DataContainerInterface,
1243
    getPosition: (x: any, dc: DataContainerInterface) => number[] = identity
×
1244
  ): number[] | null {
1245
    // no need to loop through the entire dataset
1246
    // get a sample of data to calculate bounds
1247
    const sampleData =
1248
      dataContainer.numRows() > MAX_SAMPLE_SIZE
339!
1249
        ? getSampleContainerData(dataContainer, MAX_SAMPLE_SIZE)
1250
        : dataContainer;
1251

1252
    const points = getPosition ? sampleData.mapIndex(getPosition) : [];
339!
1253

1254
    const latBounds = getLatLngBounds(points, 1, [-90, 90]);
339✔
1255
    const lngBounds = getLatLngBounds(points, 0, [-180, 180]);
339✔
1256

1257
    if (!latBounds || !lngBounds) {
339✔
1258
      return null;
2✔
1259
    }
1260

1261
    return [lngBounds[0], latBounds[0], lngBounds[1], latBounds[1]];
337✔
1262
  }
1263

1264
  getChangedTriggers(dataUpdateTriggers) {
1265
    const triggerChanged = diffUpdateTriggers(dataUpdateTriggers, this._oldDataUpdateTriggers);
499✔
1266
    this._oldDataUpdateTriggers = dataUpdateTriggers;
499✔
1267

1268
    return triggerChanged;
499✔
1269
  }
1270

1271
  getEncodedChannelValue(
1272
    scale: (value) => any,
1273
    data: any[],
1274
    field: VisualChannelField,
1275
    nullValue = NO_VALUE_COLOR,
5✔
1276
    getValue = defaultGetFieldValue
×
1277
  ) {
1278
    const value = getValue(field, data);
82✔
1279

1280
    if (!notNullorUndefined(value)) {
82✔
1281
      return nullValue;
4✔
1282
    }
1283

1284
    let attributeValue;
1285
    if (Array.isArray(value)) {
78!
1286
      attributeValue = value.map(scale);
×
1287
    } else {
1288
      attributeValue = scale(value);
78✔
1289
    }
1290

1291
    if (!notNullorUndefined(attributeValue)) {
78!
1292
      attributeValue = nullValue;
×
1293
    }
1294

1295
    return attributeValue;
78✔
1296
  }
1297

1298
  updateMeta(meta: Layer['meta']) {
1299
    this.meta = {...this.meta, ...meta};
362✔
1300
  }
1301

1302
  getDataUpdateTriggers({filteredIndex, id, dataContainer}: KeplerTable): any {
1303
    const {columns} = this.config;
499✔
1304

1305
    return {
499✔
1306
      getData: {datasetId: id, dataContainer, columns, filteredIndex},
1307
      getMeta: {datasetId: id, dataContainer, columns},
1308
      ...(this.config.textLabel || []).reduce(
499!
1309
        (accu, tl, i) => ({
508✔
1310
          ...accu,
1311
          [`getLabelCharacterSet-${i}`]: tl.field ? tl.field.name : null
508✔
1312
        }),
1313
        {}
1314
      )
1315
    };
1316
  }
1317

1318
  updateData(datasets: Datasets, oldLayerData: any) {
1319
    if (!this.config.dataId) {
493!
1320
      return {};
×
1321
    }
1322
    const layerDataset = datasets[this.config.dataId];
493✔
1323
    const {dataContainer} = layerDataset;
493✔
1324

1325
    const getPosition = this.getPositionAccessor(dataContainer, layerDataset);
493✔
1326
    const dataUpdateTriggers = this.getDataUpdateTriggers(layerDataset);
493✔
1327
    const triggerChanged = this.getChangedTriggers(dataUpdateTriggers);
493✔
1328

1329
    if (triggerChanged && (triggerChanged.getMeta || triggerChanged.getData)) {
493✔
1330
      this.updateLayerMeta(layerDataset, getPosition);
349✔
1331

1332
      // reset filteredItemCount
1333
      this.filteredItemCount = {};
349✔
1334
    }
1335

1336
    let data = [];
493✔
1337

1338
    if (!(triggerChanged && triggerChanged.getData) && oldLayerData && oldLayerData.data) {
493✔
1339
      // same data
1340
      data = oldLayerData.data;
143✔
1341
    } else {
1342
      data = this.calculateDataAttribute(layerDataset, getPosition);
350✔
1343
    }
1344

1345
    return {data, triggerChanged};
493✔
1346
  }
1347

1348
  /**
1349
   * helper function to update one layer domain when state.data changed
1350
   * if state.data change is due ot update filter, newFiler will be passed
1351
   * called by updateAllLayerDomainData
1352
   * @param datasets
1353
   * @param newFilter
1354
   * @returns layer
1355
   */
1356
  updateLayerDomain(datasets: Datasets, newFilter?: Filter): Layer {
1357
    const table = this.getDataset(datasets);
330✔
1358
    if (!table) {
330!
1359
      return this;
×
1360
    }
1361
    Object.values(this.visualChannels).forEach(channel => {
330✔
1362
      const {scale} = channel;
1,188✔
1363
      const scaleType = this.config[scale];
1,188✔
1364
      // ordinal domain is based on dataContainer, if only filter changed
1365
      // no need to update ordinal domain
1366
      if (!newFilter || scaleType !== SCALE_TYPES.ordinal) {
1,188✔
1367
        const {domain} = channel;
1,184✔
1368
        const updatedDomain = this.calculateLayerDomain(table, channel);
1,184✔
1369
        this.updateLayerConfig({[domain]: updatedDomain});
1,184✔
1370
      }
1371
    });
1372

1373
    return this;
330✔
1374
  }
1375

1376
  getDataset(datasets) {
1377
    return this.config.dataId ? datasets[this.config.dataId] : null;
330!
1378
  }
1379

1380
  /**
1381
   * Validate visual channel field and scales based on supported field & scale type
1382
   * @param channel
1383
   */
1384
  validateVisualChannel(channel: string) {
1385
    this.validateFieldType(channel);
497✔
1386
    this.validateScale(channel);
497✔
1387
  }
1388

1389
  /**
1390
   * Validate field type based on channelScaleType
1391
   */
1392
  validateFieldType(channel: string) {
1393
    const visualChannel = this.visualChannels[channel];
550✔
1394
    const {field, channelScaleType, supportedFieldTypes} = visualChannel;
550✔
1395

1396
    if (this.config[field]) {
550✔
1397
      // if field is selected, check if field type is supported
1398
      const channelSupportedFieldTypes =
1399
        supportedFieldTypes || CHANNEL_SCALE_SUPPORTED_FIELDS[channelScaleType];
123✔
1400

1401
      if (!channelSupportedFieldTypes.includes(this.config[field].type)) {
123!
1402
        // field type is not supported, set it back to null
1403
        // set scale back to default
1404
        this.updateLayerConfig({[field]: null});
×
1405
      }
1406
    }
1407
  }
1408

1409
  /**
1410
   * Validate scale type based on aggregation
1411
   */
1412
  validateScale(channel) {
1413
    const visualChannel = this.visualChannels[channel];
550✔
1414
    const {scale} = visualChannel;
550✔
1415
    if (!scale) {
550!
1416
      // visualChannel doesn't have scale
1417
      return;
×
1418
    }
1419
    const scaleOptions = this.getScaleOptions(channel);
550✔
1420
    // check if current selected scale is
1421
    // supported, if not, change to default
1422
    if (!scaleOptions.includes(this.config[scale])) {
550✔
1423
      this.updateLayerConfig({[scale]: scaleOptions[0]});
42✔
1424
    }
1425
  }
1426

1427
  /**
1428
   * Get scale options based on current field
1429
   * @param {string} channel
1430
   * @returns {string[]}
1431
   */
1432
  getScaleOptions(channel: string): string[] {
1433
    const visualChannel = this.visualChannels[channel];
505✔
1434
    const {field, scale, channelScaleType} = visualChannel;
505✔
1435

1436
    return this.config[field]
505✔
1437
      ? FIELD_OPTS[this.config[field].type].scale[channelScaleType]
1438
      : [this.getDefaultLayerConfig({dataId: ''})[scale]];
1439
  }
1440

1441
  updateLayerVisualChannel(dataset: KeplerTable, channel: string) {
1442
    const visualChannel = this.visualChannels[channel];
33✔
1443
    this.validateVisualChannel(channel);
33✔
1444
    // calculate layer channel domain
1445
    const updatedDomain = this.calculateLayerDomain(dataset, visualChannel);
33✔
1446
    this.updateLayerConfig({[visualChannel.domain]: updatedDomain});
33✔
1447
  }
1448

1449
  getVisualChannelUpdateTriggers(): UpdateTriggers {
1450
    const updateTriggers: UpdateTriggers = {};
43✔
1451
    Object.values(this.visualChannels).forEach(visualChannel => {
43✔
1452
      // field range scale domain
1453
      const {accessor, field, scale, domain, range, defaultValue, fixed} = visualChannel;
156✔
1454

1455
      if (accessor) {
156!
1456
        updateTriggers[accessor] = {
156✔
1457
          [field]: this.config[field],
1458
          [scale]: this.config[scale],
1459
          [domain]: this.config[domain],
1460
          [range]: this.config.visConfig[range],
1461
          defaultValue:
1462
            typeof defaultValue === 'function' ? defaultValue(this.config) : defaultValue,
156✔
1463
          ...(fixed ? {[fixed]: this.config.visConfig[fixed]} : {})
156✔
1464
        };
1465
      }
1466
    });
1467
    return updateTriggers;
43✔
1468
  }
1469

1470
  calculateLayerDomain(dataset, visualChannel) {
1471
    const {scale} = visualChannel;
1,217✔
1472
    const scaleType = this.config[scale];
1,217✔
1473

1474
    const field = this.config[visualChannel.field];
1,217✔
1475
    if (!field) {
1,217✔
1476
      // if colorField or sizeField were set back to null
1477
      return defaultDomain;
1,089✔
1478
    }
1479

1480
    return dataset.getColumnLayerDomain(field, scaleType) || defaultDomain;
128!
1481
  }
1482

1483
  hasHoveredObject(objectInfo) {
1484
    return this.isLayerHovered(objectInfo) && objectInfo.object ? objectInfo.object : null;
47✔
1485
  }
1486

1487
  isLayerHovered(objectInfo): boolean {
1488
    return objectInfo?.picked && objectInfo?.layer?.props?.id === this.id;
94✔
1489
  }
1490

1491
  getRadiusScaleByZoom(mapState: MapState, fixedRadius?: boolean) {
1492
    const radiusChannel = Object.values(this.visualChannels).find(vc => vc.property === 'radius');
140✔
1493

1494
    if (!radiusChannel) {
40!
1495
      return 1;
×
1496
    }
1497

1498
    const field = radiusChannel.field;
40✔
1499
    const fixed = fixedRadius === undefined ? this.config.visConfig.fixedRadius : fixedRadius;
40✔
1500
    const {radius} = this.config.visConfig;
40✔
1501

1502
    return fixed ? 1 : (this.config[field] ? 1 : radius) * this.getZoomFactor(mapState);
40!
1503
  }
1504

1505
  shouldCalculateLayerData(props: string[]) {
1506
    return props.some(p => !this.noneLayerDataAffectingProps.includes(p));
40✔
1507
  }
1508

1509
  getBrushingExtensionProps(interactionConfig, brushingTarget?) {
1510
    const {brush} = interactionConfig;
28✔
1511

1512
    return {
28✔
1513
      // brushing
1514
      autoHighlight: !brush.enabled,
1515
      brushingRadius: brush.config.size * 1000,
1516
      brushingTarget: brushingTarget || 'source',
52✔
1517
      brushingEnabled: brush.enabled
1518
    };
1519
  }
1520

1521
  getDefaultDeckLayerProps({
1522
    idx,
1523
    gpuFilter,
1524
    mapState,
1525
    layerCallbacks,
1526
    visible
1527
  }: {
1528
    idx: number;
1529
    gpuFilter: GpuFilter;
1530
    mapState: MapState;
1531
    layerCallbacks: any;
1532
    visible: boolean;
1533
  }) {
1534
    return {
57✔
1535
      id: this.id,
1536
      idx,
1537
      coordinateSystem: COORDINATE_SYSTEM.LNGLAT,
1538
      pickable: true,
1539
      wrapLongitude: true,
1540
      parameters: {depthTest: Boolean(mapState.dragRotate || this.config.visConfig.enable3d)},
114✔
1541
      hidden: this.config.hidden,
1542
      // visconfig
1543
      opacity: this.config.visConfig.opacity,
1544
      highlightColor: this.config.highlightColor,
1545
      // data filtering
1546
      extensions: [dataFilterExtension],
1547
      filterRange: gpuFilter ? gpuFilter.filterRange : undefined,
57✔
1548
      onFilteredItemsChange: gpuFilter ? layerCallbacks?.onFilteredItemsChange : undefined,
57✔
1549

1550
      // layer should be visible and if splitMap, shown in to one of panel
1551
      visible: this.config.isVisible && visible
114✔
1552
    };
1553
  }
1554

1555
  getDefaultHoverLayerProps() {
1556
    return {
1✔
1557
      id: `${this.id}-hovered`,
1558
      pickable: false,
1559
      wrapLongitude: true,
1560
      coordinateSystem: COORDINATE_SYSTEM.LNGLAT
1561
    };
1562
  }
1563

1564
  renderTextLabelLayer(
1565
    {
1566
      getPosition,
1567
      getFiltered,
1568
      getPixelOffset,
1569
      backgroundProps,
1570
      updateTriggers,
1571
      animationConfig,
1572
      sharedProps
1573
    }: {
1574
      getPosition?: ((d: any) => number[]) | arrow.Vector;
1575
      getFiltered?: (data: {index: number}, objectInfo: {index: number}) => number;
1576
      getPixelOffset: (textLabel: any) => number[] | ((d: any) => number[]);
1577
      backgroundProps?: {background: boolean; backgroundPadding?: number[]; getBackgroundColor?: any};
1578
      updateTriggers: {
1579
        [key: string]: any;
1580
      };
1581
      animationConfig?: any;
1582
      sharedProps: any;
1583
    },
1584
    renderOpts
1585
  ) {
1586
    const {data, mapState} = renderOpts;
1587
    const {textLabel} = this.config;
1588

1589
    const TextLayerClass = isArrowTable(data.data) ? GeoArrowTextLayer : TextLayer;
1590

37✔
1591
    return data.textLabels.reduce((accu, d, i) => {
37✔
1592
      if (d.getText) {
1593
        const background = textLabel[i].background || backgroundProps?.background;
37!
1594
        const getText = animationConfig
1595
          ? f => d.getText(f, animationConfig)
37✔
1596
          : d.getText;
39✔
1597

6✔
1598
        accu.push(
6!
1599
          // @ts-expect-error
1600
          new TextLayerClass({
6✔
1601
            ...sharedProps,
1602
            id: `${this.id}-label-${textLabel[i].field?.name}`,
1603
            data: data.data,
1604
            visible: this.config.isVisible,
1605
            getText,
1606
            getPosition,
1607
            getFiltered,
1608
            characterSet: d.characterSet,
1609
            getPixelOffset: getPixelOffset(textLabel[i]),
1610
            getSize: PROJECTED_PIXEL_SIZE_MULTIPLIER,
1611
            sizeScale: textLabel[i].size,
1612
            fontWeight: textLabel[i].weight ?? DEFAULT_TEXT_LABEL.weight,
1613
            getTextAnchor: textLabel[i].anchor,
1614
            getAlignmentBaseline: textLabel[i].alignment,
6!
1615
            getColor: textLabel[i].color,
1616
            outlineWidth: textLabel[i].outlineWidth * TEXT_OUTLINE_MULTIPLIER,
1617
            outlineColor: textLabel[i].outlineColor,
1618
            background,
1619
            ...(backgroundProps?.backgroundPadding
1620
              ? {backgroundPadding: backgroundProps.backgroundPadding}
1621
              : null),
6!
1622
            getBackgroundColor:
1623
              backgroundProps?.getBackgroundColor ?? textLabel[i].backgroundColor,
1624
            fontSettings: {
12✔
1625
              sdf: textLabel[i].outlineWidth > 0
1626
            },
1627
            parameters: {
1628
              // text will always show on top of all layers
1629
              depthTest: false
1630
            },
1631

1632
            getFilterValue: data.getFilterValue,
1633
            updateTriggers: {
1634
              ...updateTriggers,
1635
              getText: {
1636
                field: textLabel[i].field?.name,
1637
                ...(updateTriggers.getText || {})
1638
              },
12✔
1639
              getPixelOffset: {
1640
                ...updateTriggers.getRadius,
1641
                mapState,
1642
                anchor: textLabel[i].anchor,
1643
                alignment: textLabel[i].alignment
1644
              },
1645
              getTextAnchor: textLabel[i].anchor,
1646
              getAlignmentBaseline: textLabel[i].alignment,
1647
              getColor: textLabel[i].color
1648
            },
1649
            _subLayerProps: {
1650
              ...(background
1651
                ? {
6!
1652
                    background: {
1653
                      parameters: {
1654
                        cull: false
1655
                      }
1656
                    }
1657
                  }
1658
                : null)
1659
            }
1660
          })
1661
        );
1662
      }
1663
      return accu;
1664
    }, []);
39✔
1665
  }
1666

1667
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1668
  calculateDataAttribute(keplerTable: KeplerTable, getPosition): any {
1669
    // implemented in subclasses
1670
    return [];
UNCOV
1671
  }
×
1672

1673
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1674
  updateLayerMeta(dataset: KeplerTable, getPosition) {
1675
    // implemented in subclasses
1676
  }
1677

1678
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1679
  getPositionAccessor(
1680
    _dataContainer?: DataContainerInterface,
1681
    // TODO refactor for the next major version to pass only dataset
1682
    _dataset?: KeplerTable
1683
  ): (...args: any[]) => any {
1684
    // implemented in subclasses
1685
    return () => null;
UNCOV
1686
  }
×
1687

1688
  getLegendVisualChannels(): {[key: string]: VisualChannel} {
1689
    return this.visualChannels;
1690
  }
18✔
1691
}
1692

1693
export default Layer;
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