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

keplergl / kepler.gl / 13319127520

13 Feb 2025 11:31PM UTC coverage: 66.434% (-0.007%) from 66.441%
13319127520

push

github

web-flow
[fix] improvements for layer type change logic (#2995)

- when switching between Geojson > Point > Heatmap layers the columns end up in an invalid state when using geoarrow point fileds which are also recognized as geojson data.

- add an optional `verifyField` function to verify whether a column is valid for a column mode.
- a layer can end up with unsupported `columnMode` after layer type is switched, so add extra check and try to use one of the default layer configurations in case of unsupported `columnMode`.
- `findDefaultLayerProps` also returns `altProps` with layer configurations that shouldn't be created by default, but are still valid choices.
- fix for Geojson layer not reacting to column when in `COLUMN_MODE_TABLE` mode.

---------

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

6021 of 10570 branches covered (56.96%)

Branch coverage included in aggregate %.

26 of 36 new or added lines in 6 files covered. (72.22%)

64 existing lines in 3 files now uncovered.

12366 of 17107 relevant lines covered (72.29%)

89.22 hits per line

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

85.33
/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
  getLatLngBounds,
38
  getSampleContainerData,
39
  hasColorMap,
40
  hexToRgb,
41
  isPlainObject,
42
  isDomainStops,
43
  updateColorRangeByMatchingPalette
44
} from '@kepler.gl/utils';
45
import {generateHashId, toArray, notNullorUndefined} from '@kepler.gl/common-utils';
46
import {Datasets, GpuFilter, KeplerTable} from '@kepler.gl/table';
47
import {
48
  AggregatedBin,
49
  ColorRange,
50
  ColorUI,
51
  Field,
52
  Filter,
53
  GetVisChannelScaleReturnType,
54
  LayerVisConfigSettings,
55
  MapState,
56
  AnimationConfig,
57
  KeplerLayer,
58
  LayerBaseConfig,
59
  LayerColumns,
60
  LayerColumn,
61
  ColumnPairs,
62
  ColumnLabels,
63
  SupportedColumnMode,
64
  FieldPair,
65
  NestedPartial,
66
  RGBColor,
67
  ValueOf,
68
  VisualChannel,
69
  VisualChannels,
70
  VisualChannelDomain,
71
  VisualChannelField,
72
  VisualChannelScale
73
} from '@kepler.gl/types';
74
import {
75
  getScaleFunction,
76
  initializeLayerColorMap,
77
  getCategoricalColorScale,
78
  updateCustomColorRangeByColorUI
79
} from '@kepler.gl/utils';
80
import memoize from 'lodash.memoize';
81
import {
82
  initializeCustomPalette,
83
  isDomainQuantile,
84
  getDomainStepsbyZoom,
85
  getThresholdsFromQuantiles
86
} from '@kepler.gl/utils';
87

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

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

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

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

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

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

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

155
const MAX_SAMPLE_SIZE = 5000;
13✔
156
const defaultDomain: [number, number] = [0, 1];
13✔
157
const dataFilterExtension = new DataFilterExtension({
13✔
158
  filterSize: MAX_GPU_FILTERS,
159
  // @ts-expect-error not typed
160
  countItems: true
161
});
162

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

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

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

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

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

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

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

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

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

221
    // visConfigSettings
222
    this.visConfigSettings = {};
802✔
223

224
    this.config = this.getDefaultLayerConfig(props);
802✔
225

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

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

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

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

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

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

259
  get isAggregated() {
260
    return false;
13✔
261
  }
262

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

274
  get optionalColumns(): string[] {
275
    const {supportedColumnModes} = this;
634✔
276
    if (supportedColumnModes) {
634✔
277
      return supportedColumnModes.reduce<string[]>(
362✔
278
        (acc, obj) => (obj.optionalColumns ? acc.concat(obj.optionalColumns) : acc),
876✔
279
        []
280
      );
281
    }
282
    return [];
272✔
283
  }
284

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

432
    return this.getAllPossibleColumnPairs(requiredColumns);
84✔
433
  }
434

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

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

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

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

463
      if (pts[index] + 1 < counts[index]) {
112✔
464
        pts[index] = pts[index] + 1;
109✔
465
        return true;
109✔
466
      }
467

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

472
    return pairs;
87✔
473
  }
474

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

479
  getDefaultLayerConfig(
480
    props: LayerBaseConfigPartial
481
  ): LayerBaseConfig & Partial<LayerColorConfig & LayerSizeConfig> {
482
    return {
1,158✔
483
      dataId: props.dataId,
484
      label: props.label || DEFAULT_LAYER_LABEL,
1,945✔
485
      color: props.color || colorMaker.next().value,
2,146✔
486
      // set columns later
487
      columns: {},
488
      isVisible: props.isVisible ?? true,
2,018✔
489
      isConfigActive: props.isConfigActive ?? false,
2,302✔
490
      highlightColor: props.highlightColor || DEFAULT_HIGHLIGHT_COLOR,
2,277✔
491
      hidden: props.hidden ?? false,
2,255✔
492

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

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

504
      visConfig: {},
505

506
      textLabel: [DEFAULT_TEXT_LABEL],
507

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

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

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

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

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

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

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

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

594
    return updatedColumn;
1✔
595
  }
596

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

783
    return copied;
221✔
784
  }
785

786
  registerVisConfig(layerVisConfigs: {
787
    [key: string]: keyof LayerVisConfigSettings | ValueOf<LayerVisConfigSettings>;
788
  }) {
789
    Object.keys(layerVisConfigs).forEach(item => {
877✔
790
      const configItem = layerVisConfigs[item];
9,367✔
791
      if (typeof configItem === 'string' && LAYER_VIS_CONFIGS[configItem]) {
9,367✔
792
        // if assigned one of default LAYER_CONFIGS
793
        this.config.visConfig[item] = LAYER_VIS_CONFIGS[configItem].defaultValue;
8,038✔
794
        this.visConfigSettings[item] = LAYER_VIS_CONFIGS[configItem];
8,038✔
795
      } else if (
1,329✔
796
        typeof configItem === 'object' &&
2,604✔
797
        ['type', 'defaultValue'].every(p => Object.prototype.hasOwnProperty.call(configItem, p))
2,550✔
798
      ) {
799
        // if provided customized visConfig, and has type && defaultValue
800
        // TODO: further check if customized visConfig is valid
801
        this.config.visConfig[item] = configItem.defaultValue;
1,275✔
802
        this.visConfigSettings[item] = configItem;
1,275✔
803
      }
804
    });
805
  }
806

807
  getLayerColumns(propsColumns = {}) {
758✔
808
    const columnValidators = this.columnValidators || {};
962!
809
    const required = this.requiredLayerColumns.reduce(
962✔
810
      (accu, key) => ({
3,795✔
811
        ...accu,
812
        [key]: columnValidators[key]
3,795✔
813
          ? {
814
              value: propsColumns[key]?.value ?? null,
1,612✔
815
              fieldIdx: propsColumns[key]?.fieldIdx ?? -1,
1,566✔
816
              validator: columnValidators[key]
817
            }
818
          : {value: propsColumns[key]?.value ?? null, fieldIdx: propsColumns[key]?.fieldIdx ?? -1}
10,967✔
819
      }),
820
      {}
821
    );
822
    const optional = this.optionalColumns.reduce(
962✔
823
      (accu, key) => ({
1,051✔
824
        ...accu,
825
        [key]: {
826
          value: propsColumns[key]?.value ?? null,
2,099✔
827
          fieldIdx: propsColumns[key]?.fieldIdx ?? -1,
1,934✔
828
          optional: true
829
        }
830
      }),
831
      {}
832
    );
833

834
    const columns = {...required, ...optional};
962✔
835

836
    return columns;
962✔
837
  }
838

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

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

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

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

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

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

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

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

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

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

892
    return this;
51✔
893
  }
894

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1128
      if (accessor) {
1,483!
1129
        const shouldGetScale = this.config[field];
1,483✔
1130

1131
        if (shouldGetScale) {
1,483✔
1132
          const isFixed = fixed && this.config.visConfig[fixed];
149✔
1133

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

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

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

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

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

1190
    return attributeAccessors;
443✔
1191
  }
1192

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

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

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

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

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

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

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

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

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

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

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

1256
    if (!latBounds || !lngBounds) {
333!
1257
      return null;
×
1258
    }
1259

1260
    return [lngBounds[0], latBounds[0], lngBounds[1], latBounds[1]];
333✔
1261
  }
1262

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

1267
    return triggerChanged;
486✔
1268
  }
1269

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

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

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

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

1294
    return attributeValue;
78✔
1295
  }
1296

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

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

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

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

1324
    const getPosition = this.getPositionAccessor(dataContainer);
486✔
1325
    const dataUpdateTriggers = this.getDataUpdateTriggers(layerDataset);
486✔
1326
    const triggerChanged = this.getChangedTriggers(dataUpdateTriggers);
486✔
1327

1328
    if (triggerChanged && (triggerChanged.getMeta || triggerChanged.getData)) {
486✔
1329
      this.updateLayerMeta(layerDataset, getPosition);
346✔
1330

1331
      // reset filteredItemCount
1332
      this.filteredItemCount = {};
346✔
1333
    }
1334

1335
    let data = [];
486✔
1336

1337
    if (!(triggerChanged && triggerChanged.getData) && oldLayerData && oldLayerData.data) {
486✔
1338
      // same data
1339
      data = oldLayerData.data;
139✔
1340
    } else {
1341
      data = this.calculateDataAttribute(layerDataset, getPosition);
347✔
1342
    }
1343

1344
    return {data, triggerChanged};
486✔
1345
  }
1346

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

1372
    return this;
321✔
1373
  }
1374

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1586
    const TextLayerClass = data.data instanceof arrow.Table ? GeoArrowTextLayer : TextLayer;
24!
1587

1588
    return data.textLabels.reduce((accu, d, i) => {
24✔
1589
      if (d.getText) {
26✔
1590
        const background = textLabel[i].background || backgroundProps?.background;
6✔
1591

1592
        accu.push(
6✔
1593
          // @ts-expect-error
1594
          new TextLayerClass({
1595
            ...sharedProps,
1596
            id: `${this.id}-label-${textLabel[i].field?.name}`,
1597
            data: data.data,
1598
            visible: this.config.isVisible,
1599
            getText: d.getText,
1600
            getPosition,
1601
            getFiltered,
1602
            characterSet: d.characterSet,
1603
            getPixelOffset: getPixelOffset(textLabel[i]),
1604
            getSize: PROJECTED_PIXEL_SIZE_MULTIPLIER,
1605
            sizeScale: textLabel[i].size,
1606
            getTextAnchor: textLabel[i].anchor,
1607
            getAlignmentBaseline: textLabel[i].alignment,
1608
            getColor: textLabel[i].color,
1609
            outlineWidth: textLabel[i].outlineWidth * TEXT_OUTLINE_MULTIPLIER,
1610
            outlineColor: textLabel[i].outlineColor,
1611
            background,
1612
            getBackgroundColor: textLabel[i].backgroundColor,
1613
            fontSettings: {
1614
              sdf: textLabel[i].outlineWidth > 0
1615
            },
1616
            parameters: {
1617
              // text will always show on top of all layers
1618
              depthTest: false
1619
            },
1620

1621
            getFilterValue: data.getFilterValue,
1622
            updateTriggers: {
1623
              ...updateTriggers,
1624
              getText: textLabel[i].field?.name,
1625
              getPixelOffset: {
1626
                ...updateTriggers.getRadius,
1627
                mapState,
1628
                anchor: textLabel[i].anchor,
1629
                alignment: textLabel[i].alignment
1630
              },
1631
              getTextAnchor: textLabel[i].anchor,
1632
              getAlignmentBaseline: textLabel[i].alignment,
1633
              getColor: textLabel[i].color
1634
            },
1635
            _subLayerProps: {
1636
              ...(background
6!
1637
                ? {
1638
                    background: {
1639
                      parameters: {
1640
                        cull: false
1641
                      }
1642
                    }
1643
                  }
1644
                : null)
1645
            }
1646
          })
1647
        );
1648
      }
1649
      return accu;
26✔
1650
    }, []);
1651
  }
1652

1653
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1654
  calculateDataAttribute(keplerTable: KeplerTable, getPosition): any {
1655
    // implemented in subclasses
1656
    return [];
×
1657
  }
1658

1659
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1660
  updateLayerMeta(dataset: KeplerTable, getPosition) {
1661
    // implemented in subclasses
1662
  }
1663

1664
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1665
  getPositionAccessor(dataContainer?: DataContainerInterface): (...args: any[]) => any {
1666
    // implemented in subclasses
1667
    return () => null;
×
1668
  }
1669

1670
  getLegendVisualChannels(): {[key: string]: VisualChannel} {
1671
    return this.visualChannels;
16✔
1672
  }
1673
}
1674

1675
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