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

keplergl / kepler.gl / 12604159536

03 Jan 2025 09:26PM UTC coverage: 66.843% (-0.5%) from 67.344%
12604159536

Pull #2892

github

web-flow
Merge 894aa31a5 into 0b67c5409
Pull Request #2892: [fix] Prevent infinite useEffects loop

5959 of 10355 branches covered (57.55%)

Branch coverage included in aggregate %.

13 of 16 new or added lines in 1 file covered. (81.25%)

484 existing lines in 25 files now uncovered.

12215 of 16834 relevant lines covered (72.56%)

89.16 hits per line

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

84.57
/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

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

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

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

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

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

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

141
export type UpdateTriggers = {
142
  [key: string]: UpdateTrigger;
143
};
144
export type UpdateTrigger = {
145
  [key: string]: any;
146
};
147
export type LayerBounds = [number, number, number, number];
148
export type FindDefaultLayerPropsReturnValue = {props: any[]; foundLayers?: any[]};
149
/**
150
 * Approx. number of points to sample in a large data set
151
 */
152
export const LAYER_ID_LENGTH = 6;
13✔
153

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

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

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

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

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

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

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

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

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

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

220
    // visConfigSettings
221
    this.visConfigSettings = {};
775✔
222

223
    this.config = this.getDefaultLayerConfig(props);
775✔
224

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

386
  get supportedDatasetTypes(): string[] | null {
387
    return null;
3✔
388
  }
389
  /*
390
   * Given a dataset, automatically find props to create layer based on it
391
   * and return the props and previous found layers.
392
   * By default, no layers will be found
393
   */
394
  static findDefaultLayerProps(
395
    dataset: KeplerTable,
396
    foundLayers?: any[]
397
  ): FindDefaultLayerPropsReturnValue {
398
    return {props: [], foundLayers};
450✔
399
  }
400

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

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

425
    if (!Object.values(requiredColumns).every(Boolean)) {
270✔
426
      // if any field missing, return null
427
      return null;
188✔
428
    }
429

430
    return this.getAllPossibleColumnPairs(requiredColumns);
82✔
431
  }
432

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

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

450
      pairs.push(newPair);
107✔
451
    }
452
    /* eslint-enable no-loop-func */
453

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

461
      if (pts[index] + 1 < counts[index]) {
110✔
462
        pts[index] = pts[index] + 1;
107✔
463
        return true;
107✔
464
      }
465

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

470
    return pairs;
85✔
471
  }
472

473
  static hexToRgb(c) {
UNCOV
474
    return hexToRgb(c);
×
475
  }
476

477
  getDefaultLayerConfig(
478
    props: LayerBaseConfigPartial
479
  ): LayerBaseConfig & Partial<LayerColorConfig & LayerSizeConfig> {
480
    return {
1,131✔
481
      dataId: props.dataId,
482
      label: props.label || DEFAULT_LAYER_LABEL,
1,891✔
483
      color: props.color || colorMaker.next().value,
2,092✔
484
      // set columns later
485
      columns: {},
486
      isVisible: props.isVisible ?? true,
1,964✔
487
      isConfigActive: props.isConfigActive ?? false,
2,248✔
488
      highlightColor: props.highlightColor || DEFAULT_HIGHLIGHT_COLOR,
2,223✔
489
      hidden: props.hidden ?? false,
2,201✔
490

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

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

502
      visConfig: {},
503

504
      textLabel: [DEFAULT_TEXT_LABEL],
505

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

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

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

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

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

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

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

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

UNCOV
592
    return updatedColumn;
×
593
  }
594

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

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

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

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

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

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

651
  getFilteredItemCount(): number | null {
652
    // use first layer
653
    if (Object.keys(this.filteredItemCount).length) {
×
654
      const firstLayer = Object.keys(this.filteredItemCount)[0];
×
UNCOV
655
      return this.filteredItemCount[firstLayer];
×
656
    }
UNCOV
657
    return null;
×
658
  }
659
  /**
660
   * When change layer type, try to copy over layer configs as much as possible
661
   * @param configToCopy - config to copy over
662
   * @param visConfigSettings - visConfig settings of config to copy
663
   */
664
  assignConfigToLayer(configToCopy, visConfigSettings) {
665
    // don't deep merge visualChannel field
666
    // don't deep merge color range, reversed: is not a key by default
667
    const shallowCopy = ['colorRange', 'strokeColorRange'].concat(
6✔
668
      Object.values(this.visualChannels).map(v => v.field)
15✔
669
    );
670

671
    // don't copy over domain and animation
672
    const notToCopy = ['animation'].concat(Object.values(this.visualChannels).map(v => v.domain));
15✔
673
    // if range is for the same property group copy it, otherwise, not to copy
674
    Object.values(this.visualChannels).forEach(v => {
6✔
675
      if (
15!
676
        configToCopy.visConfig[v.range] &&
33✔
677
        this.visConfigSettings[v.range] &&
678
        visConfigSettings[v.range].group !== this.visConfigSettings[v.range].group
679
      ) {
UNCOV
680
        notToCopy.push(v.range);
×
681
      }
682
    });
683

684
    // don't copy over visualChannel range
685
    const currentConfig = this.config;
6✔
686
    const copied = this.copyLayerConfig(currentConfig, configToCopy, {
6✔
687
      shallowCopy,
688
      notToCopy
689
    });
690

691
    // update columNode based on new columns
692
    if (this.config.columnMode && this.supportedColumnModes) {
6✔
693
      // find a mode with all requied columns
694
      const satisfiedColumnMode = this.supportedColumnModes?.find(mode => {
3✔
695
        return mode.requiredColumns?.every(requriedCol => copied.columns?.[requriedCol]?.value);
4✔
696
      });
697
      copied.columnMode = satisfiedColumnMode?.key || copied.columnMode;
3✔
698
    }
699

700
    this.updateLayerConfig(copied);
6✔
701
    // validate visualChannel field type and scale types
702
    Object.keys(this.visualChannels).forEach(channel => {
6✔
703
      this.validateVisualChannel(channel);
15✔
704
    });
705
  }
706

707
  /*
708
   * Recursively copy config over to an empty layer
709
   * when received saved config, or copy config over from a different layer type
710
   * make sure to only copy over value to existing keys
711
   * @param {object} currentConfig - existing config to be override
712
   * @param {object} configToCopy - new Config to copy over
713
   * @param {string[]} shallowCopy - array of properties to not to be deep copied
714
   * @param {string[]} notToCopy - array of properties not to copy
715
   * @returns {object} - copied config
716
   */
717
  copyLayerConfig(
718
    currentConfig,
719
    configToCopy,
720
    {shallowCopy = [], notToCopy = []}: {shallowCopy?: string[]; notToCopy?: string[]} = {}
157!
721
  ) {
722
    const copied: {columnMode?: string; columns?: LayerColumns} = {};
221✔
723
    Object.keys(currentConfig).forEach(key => {
221✔
724
      if (
2,195✔
725
        isPlainObject(currentConfig[key]) &&
2,798✔
726
        isPlainObject(configToCopy[key]) &&
727
        !shallowCopy.includes(key) &&
728
        !notToCopy.includes(key)
729
      ) {
730
        // recursively assign object value
731
        copied[key] = this.copyLayerConfig(currentConfig[key], configToCopy[key], {
58✔
732
          shallowCopy,
733
          notToCopy
734
        });
735
      } else if (notNullorUndefined(configToCopy[key]) && !notToCopy.includes(key)) {
2,137✔
736
        // copy
737
        copied[key] = configToCopy[key];
1,004✔
738
      } else {
739
        // keep existing
740
        copied[key] = currentConfig[key];
1,133✔
741
      }
742
    });
743

744
    return copied;
221✔
745
  }
746

747
  registerVisConfig(layerVisConfigs: {
748
    [key: string]: keyof LayerVisConfigSettings | ValueOf<LayerVisConfigSettings>;
749
  }) {
750
    Object.keys(layerVisConfigs).forEach(item => {
823✔
751
      const configItem = layerVisConfigs[item];
8,503✔
752
      if (typeof configItem === 'string' && LAYER_VIS_CONFIGS[configItem]) {
8,503✔
753
        // if assigned one of default LAYER_CONFIGS
754
        this.config.visConfig[item] = LAYER_VIS_CONFIGS[configItem].defaultValue;
7,714✔
755
        this.visConfigSettings[item] = LAYER_VIS_CONFIGS[configItem];
7,714✔
756
      } else if (
789!
757
        typeof configItem === 'object' &&
1,578✔
758
        ['type', 'defaultValue'].every(p => Object.prototype.hasOwnProperty.call(configItem, p))
1,578✔
759
      ) {
760
        // if provided customized visConfig, and has type && defaultValue
761
        // TODO: further check if customized visConfig is valid
762
        this.config.visConfig[item] = configItem.defaultValue;
789✔
763
        this.visConfigSettings[item] = configItem;
789✔
764
      }
765
    });
766
  }
767

768
  getLayerColumns(propsColumns = {}) {
731✔
769
    const columnValidators = this.columnValidators || {};
935!
770
    const required = this.requiredLayerColumns.reduce(
935✔
771
      (accu, key) => ({
3,795✔
772
        ...accu,
773
        [key]: columnValidators[key]
3,795✔
774
          ? {
775
              value: propsColumns[key]?.value ?? null,
1,612✔
776
              fieldIdx: propsColumns[key]?.fieldIdx ?? -1,
1,566✔
777
              validator: columnValidators[key]
778
            }
779
          : {value: propsColumns[key]?.value ?? null, fieldIdx: propsColumns[key]?.fieldIdx ?? -1}
10,967✔
780
      }),
781
      {}
782
    );
783
    const optional = this.optionalColumns.reduce(
935✔
784
      (accu, key) => ({
1,051✔
785
        ...accu,
786
        [key]: {
787
          value: propsColumns[key]?.value ?? null,
2,099✔
788
          fieldIdx: propsColumns[key]?.fieldIdx ?? -1,
1,934✔
789
          optional: true
790
        }
791
      }),
792
      {}
793
    );
794

795
    const columns = {...required, ...optional};
935✔
796

797
    return columns;
935✔
798
  }
799

800
  updateLayerConfig<
801
    LayerConfig extends LayerBaseConfig &
802
      Partial<LayerColorConfig & LayerSizeConfig> = LayerBaseConfig
803
  >(newConfig: Partial<LayerConfig>): Layer {
804
    this.config = {...this.config, ...newConfig};
1,894✔
805
    return this;
1,894✔
806
  }
807

808
  updateLayerVisConfig(newVisConfig) {
809
    this.config.visConfig = {...this.config.visConfig, ...newVisConfig};
88✔
810
    return this;
88✔
811
  }
812

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

816
    if (!isPlainObject(newConfig) || typeof prop !== 'string') {
51!
UNCOV
817
      return this;
×
818
    }
819

820
    const colorUIProp = Object.entries(newConfig).reduce((accu, [key, value]) => {
51✔
821
      return {
56✔
822
        ...accu,
823
        [key]:
824
          isPlainObject(accu[key]) && isPlainObject(value)
151✔
825
            ? {...accu[key], ...(value as Record<string, unknown>)}
826
            : value
827
      };
828
    }, previous[prop] || DEFAULT_COLOR_UI);
51!
829

830
    const colorUI = {
51✔
831
      ...previous,
832
      [prop]: colorUIProp
833
    };
834

835
    this.updateLayerConfig({colorUI});
51✔
836
    // if colorUI[prop] is colorRange
837
    const isColorRange = visConfig[prop] && visConfig[prop].colors;
51✔
838

839
    if (isColorRange) {
51✔
840
      // if open dropdown and prop is color range
841
      // Automatically set colorRangeConfig's step and reversed
842
      this.updateColorUIByColorRange(newConfig, prop);
47✔
843

844
      // if changes in UI is made to 'reversed', 'steps' or steps
845
      // update current layer colorRange
846
      this.updateColorRangeByColorUI(newConfig, previous, prop);
47✔
847

848
      // if set colorRangeConfig to custom
849
      // initiate customPalette to be edited in the ui
850
      this.updateCustomPalette(newConfig, previous, prop);
47✔
851
    }
852

853
    return this;
51✔
854
  }
855

856
  // if set colorRangeConfig to custom palette or custom breaks
857
  // initiate customPalette to be edited in the ui
858
  updateCustomPalette(newConfig, previous, prop) {
859
    if (!newConfig.colorRangeConfig?.custom && !newConfig.colorRangeConfig?.customBreaks) {
47✔
860
      return;
36✔
861
    }
862

863
    if (newConfig.customPalette) {
11✔
864
      // if new config also set customPalette, no need to initiate new
865
      return;
2✔
866
    }
867
    const {colorUI, visConfig} = this.config;
9✔
868

869
    if (!visConfig[prop]) return;
9!
870
    // make copy of current color range to customPalette
871
    let customPalette = {
9✔
872
      ...visConfig[prop]
873
    };
874

875
    if (newConfig.colorRangeConfig.customBreaks && !customPalette.colorMap) {
9✔
876
      // find visualChanel
877
      const visualChannels = this.visualChannels;
1✔
878
      const channelKey = Object.keys(visualChannels).find(
1✔
879
        key => visualChannels[key].range === prop
1✔
880
      );
881
      if (!channelKey) {
1!
882
        // should never happn
883
        Console.warn(`updateColorUI: Can't find visual channel which range is ${prop}`);
×
UNCOV
884
        return;
×
885
      }
886
      // add name|type|category to updateCustomPalette if customBreaks, so that
887
      // colors will not be override as well when inverse palette with custom break
888
      // initiate colorMap from current scale
889

890
      const colorMap = initializeLayerColorMap(this, visualChannels[channelKey]);
1✔
891
      customPalette = initializeCustomPalette(visConfig[prop], colorMap);
1✔
892
    } else if (newConfig.colorRangeConfig.custom) {
8!
893
      customPalette = initializeCustomPalette(visConfig[prop]);
8✔
894
    }
895

896
    this.updateLayerConfig({
9✔
897
      colorUI: {
898
        ...colorUI,
899
        [prop]: {
900
          ...colorUI[prop],
901
          customPalette
902
        }
903
      }
904
    });
905
  }
906

907
  /**
908
   * if open dropdown and prop is color range
909
   * Automatically set colorRangeConfig's step and reversed
910
   * @param {*} newConfig
911
   * @param {*} prop
912
   */
913
  updateColorUIByColorRange(newConfig, prop) {
914
    const {colorUI, visConfig} = this.config;
47✔
915

916
    // when custom palette adds/removes step, the number in "Steps" input control
917
    // should be updated as well
918
    const isCustom = newConfig.customPalette?.category === 'Custom';
47✔
919
    const customStepsChanged = isCustom
47✔
920
      ? newConfig.customPalette.colors.length !== visConfig[prop].colors.length
921
      : false;
922

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

925
    this.updateLayerConfig({
10✔
926
      colorUI: {
927
        ...colorUI,
928
        [prop]: {
929
          ...colorUI[prop],
930
          colorRangeConfig: {
931
            ...colorUI[prop].colorRangeConfig,
932
            steps: customStepsChanged
10✔
933
              ? colorUI[prop].customPalette.colors.length
934
              : visConfig[prop].colors.length,
935
            reversed: Boolean(visConfig[prop].reversed)
936
          }
937
        }
938
      }
939
    });
940
  }
941

942
  updateColorRangeByColorUI(newConfig, previous, prop) {
943
    // only update colorRange if changes in UI is made to 'reversed', 'steps' or steps
944
    const shouldUpdate =
945
      newConfig.colorRangeConfig &&
47✔
946
      ['reversed', 'steps', 'colorBlindSafe', 'type'].some(
947
        key =>
948
          Object.prototype.hasOwnProperty.call(newConfig.colorRangeConfig, key) &&
120✔
949
          newConfig.colorRangeConfig[key] !==
950
            (previous[prop] || DEFAULT_COLOR_UI).colorRangeConfig[key]
54!
951
      );
952
    if (!shouldUpdate) return;
47✔
953

954
    const {colorUI, visConfig} = this.config;
6✔
955

956
    // for custom palette, one can only 'reverse' the colors in custom palette.
957
    // changing 'steps', 'colorBindSafe', 'type' should fall back to predefined palette.
958
    const isCustomColorReversed =
959
      visConfig.colorRange.category === 'Custom' &&
6✔
960
      newConfig.colorRangeConfig &&
961
      Object.prototype.hasOwnProperty.call(newConfig.colorRangeConfig, 'reversed');
962

963
    const update = isCustomColorReversed
6✔
964
      ? updateCustomColorRangeByColorUI(visConfig[prop], colorUI[prop].colorRangeConfig)
965
      : updateColorRangeByMatchingPalette(visConfig[prop], colorUI[prop].colorRangeConfig);
966

967
    if (update) {
6!
968
      this.updateLayerVisConfig({[prop]: update});
6✔
969
    }
970
  }
971
  hasColumnValue(column?: LayerColumn) {
972
    return Boolean(column && column.value && column.fieldIdx > -1);
1,060✔
973
  }
974
  hasRequiredColumn(column?: LayerColumn) {
975
    return Boolean(column && (column.optional || this.hasColumnValue(column)));
137✔
976
  }
977
  /**
978
   * Check whether layer has all columns
979
   * @returns yes or no
980
   */
981
  hasAllColumns(): boolean {
982
    const {columns, columnMode} = this.config;
539✔
983
    // if layer has different column mode, check if have all required columns of current column Mode
984
    if (columnMode) {
539✔
985
      const currentColumnModes = (this.supportedColumnModes || []).find(
464✔
986
        colMode => colMode.key === columnMode
469✔
987
      );
988
      return Boolean(
464✔
989
        currentColumnModes !== undefined &&
926✔
990
          currentColumnModes.requiredColumns?.every(colKey => this.hasColumnValue(columns[colKey]))
788✔
991
      );
992
    }
993
    return Boolean(
75✔
994
      columns &&
150✔
995
        Object.values(columns).every((column?: LayerColumn) => this.hasRequiredColumn(column))
137✔
996
    );
997
  }
998

999
  /**
1000
   * Check whether layer has data
1001
   *
1002
   * @param {Array | Object} layerData
1003
   * @returns {boolean} yes or no
1004
   */
1005
  hasLayerData(layerData: {data: unknown[] | arrow.Table}) {
1006
    if (!layerData) {
38!
UNCOV
1007
      return false;
×
1008
    }
1009

1010
    return Boolean(
38✔
1011
      layerData.data &&
76!
1012
        ((layerData.data as unknown[]).length || (layerData.data as arrow.Table).numRows)
1013
    );
1014
  }
1015

1016
  isValidToSave(): boolean {
1017
    return Boolean(this.type && this.hasAllColumns());
136✔
1018
  }
1019

1020
  shouldRenderLayer(data): boolean {
1021
    return (
38✔
1022
      Boolean(this.type) &&
152✔
1023
      this.hasAllColumns() &&
1024
      this.hasLayerData(data) &&
1025
      typeof this.renderLayer === 'function'
1026
    );
1027
  }
1028

1029
  getColorScale(
1030
    colorScale: string,
1031
    colorDomain: VisualChannelDomain,
1032
    colorRange: ColorRange
1033
  ): GetVisChannelScaleReturnType {
1034
    if (colorScale === SCALE_TYPES.customOrdinal) {
140!
UNCOV
1035
      return getCategoricalColorScale(colorDomain, colorRange);
×
1036
    }
1037

1038
    if (hasColorMap(colorRange) && colorScale === SCALE_TYPES.custom) {
140✔
1039
      const cMap = new Map();
7✔
1040
      colorRange.colorMap?.forEach(([k, v]) => {
7✔
1041
        cMap.set(k, typeof v === 'string' ? hexToRgb(v) : v);
25!
1042
      });
1043

1044
      const scaleType = colorScale === SCALE_TYPES.custom ? colorScale : SCALE_TYPES.ordinal;
7!
1045

1046
      const scale = getScaleFunction(scaleType, cMap.values(), cMap.keys(), false);
7✔
1047
      scale.unknown(cMap.get(UNKNOWN_COLOR_KEY) || NO_VALUE_COLOR);
7✔
1048

1049
      return scale as GetVisChannelScaleReturnType;
7✔
1050
    }
1051
    return this.getVisChannelScale(colorScale, colorDomain, colorRange.colors.map(hexToRgb));
133✔
1052
  }
1053

1054
  accessVSFieldValue(field, indexKey) {
1055
    return defaultGetFieldValue;
146✔
1056
  }
1057
  /**
1058
   * Mapping from visual channels to deck.gl accesors
1059
   * @param param Parameters
1060
   * @param param.dataAccessor Access kepler.gl layer data from deck.gl layer
1061
   * @param param.dataContainer DataContainer to use use with dataAccessor
1062
   * @return {Object} attributeAccessors - deck.gl layer attribute accessors
1063
   */
1064
  getAttributeAccessors({
1065
    dataAccessor = defaultDataAccessor,
342✔
1066
    dataContainer,
1067
    indexKey
1068
  }: {
1069
    dataAccessor?: typeof defaultDataAccessor;
1070
    dataContainer: DataContainerInterface;
1071
    indexKey?: number | null;
1072
  }) {
1073
    const attributeAccessors: {[key: string]: any} = {};
443✔
1074

1075
    Object.keys(this.visualChannels).forEach(channel => {
443✔
1076
      const {
1077
        field,
1078
        fixed,
1079
        scale,
1080
        domain,
1081
        range,
1082
        accessor,
1083
        defaultValue,
1084
        getAttributeValue,
1085
        nullValue,
1086
        channelScaleType
1087
      } = this.visualChannels[channel];
1,483✔
1088

1089
      if (accessor) {
1,483!
1090
        const shouldGetScale = this.config[field];
1,483✔
1091

1092
        if (shouldGetScale) {
1,483✔
1093
          const isFixed = fixed && this.config.visConfig[fixed];
149✔
1094

1095
          const scaleFunction =
1096
            channelScaleType === CHANNEL_SCALES.color
149✔
1097
              ? this.getColorScale(
1098
                  this.config[scale],
1099
                  this.config[domain],
1100
                  this.config.visConfig[range]
1101
                )
1102
              : this.getVisChannelScale(
1103
                  this.config[scale],
1104
                  this.config[domain],
1105
                  this.config.visConfig[range],
1106
                  isFixed
1107
                );
1108

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

1111
          if (scaleFunction) {
149!
1112
            attributeAccessors[accessor] = scaleFunction.byZoom
149✔
1113
              ? memoize(z => {
1114
                  const scaleFunc = scaleFunction(z);
1✔
1115
                  return d =>
1✔
1116
                    this.getEncodedChannelValue(
6✔
1117
                      scaleFunc,
1118
                      dataAccessor(dataContainer)(d),
1119
                      this.config[field],
1120
                      nullValue,
1121
                      getFieldValue
1122
                    );
1123
                })
1124
              : d =>
1125
                  this.getEncodedChannelValue(
76✔
1126
                    scaleFunction,
1127
                    dataAccessor(dataContainer)(d),
1128
                    this.config[field],
1129
                    nullValue,
1130
                    getFieldValue
1131
                  );
1132

1133
            // set getFillColorByZoom to true
1134
            if (scaleFunction.byZoom) {
149✔
1135
              attributeAccessors[`${accessor}ByZoom`] = true;
1✔
1136
            }
1137
          }
1138
        } else if (typeof getAttributeValue === 'function') {
1,334✔
1139
          attributeAccessors[accessor] = getAttributeValue(this.config);
427✔
1140
        } else {
1141
          attributeAccessors[accessor] =
907✔
1142
            typeof defaultValue === 'function' ? defaultValue(this.config) : defaultValue;
907✔
1143
        }
1144

1145
        if (!attributeAccessors[accessor]) {
1,483!
UNCOV
1146
          Console.warn(`Failed to provide accessor function for ${accessor || channel}`);
×
1147
        }
1148
      }
1149
    });
1150

1151
    return attributeAccessors;
443✔
1152
  }
1153

1154
  getVisChannelScale(
1155
    scale: string,
1156
    domain: VisualChannelDomain | DomainQuantiles,
1157
    range: any,
1158
    fixed?: boolean
1159
  ): GetVisChannelScaleReturnType {
1160
    // if quantile is provided per zoom
1161
    if (isDomainQuantile(domain) && scale === SCALE_TYPES.quantile) {
161!
UNCOV
1162
      const zSteps = domain.z;
×
1163

UNCOV
1164
      const getScale = function getScaleByZoom(z) {
×
UNCOV
1165
        const scaleDomain = getDomainStepsbyZoom(domain.quantiles, zSteps, z);
×
1166
        const thresholds = getThresholdsFromQuantiles(scaleDomain, range.length);
×
1167

UNCOV
1168
        return getScaleFunction('threshold', range, thresholds, false);
×
1169
      };
1170

UNCOV
1171
      getScale.byZoom = true;
×
UNCOV
1172
      return getScale;
×
1173
    } else if (isDomainStops(domain)) {
161✔
1174
      // color is based on zoom
1175
      const zSteps = domain.z;
2✔
1176
      // get scale function by z
1177
      // {
1178
      //  z: [z, z, z],
1179
      //  stops: [[min, max], [min, max]],
1180
      //  interpolation: 'interpolate'
1181
      // }
1182

1183
      const getScale = function getScaleByZoom(z) {
2✔
1184
        const scaleDomain = getDomainStepsbyZoom(domain.stops, zSteps, z);
4✔
1185

1186
        return getScaleFunction(scale, range, scaleDomain, fixed);
4✔
1187
      };
1188

1189
      getScale.byZoom = true;
2✔
1190
      return getScale;
2✔
1191
    }
1192

1193
    return SCALE_FUNC[fixed ? 'linear' : scale]()
159✔
1194
      .domain(domain)
1195
      .range(fixed ? domain : range);
159✔
1196
  }
1197

1198
  /**
1199
   * Get longitude and latitude bounds of the data.
1200
   */
1201
  getPointsBounds(
1202
    dataContainer: DataContainerInterface,
1203
    getPosition: (x: any, dc: DataContainerInterface) => number[] = identity
×
1204
  ): number[] | null {
1205
    // no need to loop through the entire dataset
1206
    // get a sample of data to calculate bounds
1207
    const sampleData =
1208
      dataContainer.numRows() > MAX_SAMPLE_SIZE
333!
1209
        ? getSampleContainerData(dataContainer, MAX_SAMPLE_SIZE)
1210
        : dataContainer;
1211

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

1214
    const latBounds = getLatLngBounds(points, 1, [-90, 90]);
333✔
1215
    const lngBounds = getLatLngBounds(points, 0, [-180, 180]);
333✔
1216

1217
    if (!latBounds || !lngBounds) {
333!
UNCOV
1218
      return null;
×
1219
    }
1220

1221
    return [lngBounds[0], latBounds[0], lngBounds[1], latBounds[1]];
333✔
1222
  }
1223

1224
  getChangedTriggers(dataUpdateTriggers) {
1225
    const triggerChanged = diffUpdateTriggers(dataUpdateTriggers, this._oldDataUpdateTriggers);
486✔
1226
    this._oldDataUpdateTriggers = dataUpdateTriggers;
486✔
1227

1228
    return triggerChanged;
486✔
1229
  }
1230

1231
  getEncodedChannelValue(
1232
    scale: (value) => any,
1233
    data: any[],
1234
    field: VisualChannelField,
1235
    nullValue = NO_VALUE_COLOR,
5✔
1236
    getValue = defaultGetFieldValue
×
1237
  ) {
1238
    const value = getValue(field, data);
82✔
1239

1240
    if (!notNullorUndefined(value)) {
82✔
1241
      return nullValue;
4✔
1242
    }
1243

1244
    let attributeValue;
1245
    if (Array.isArray(value)) {
78!
UNCOV
1246
      attributeValue = value.map(scale);
×
1247
    } else {
1248
      attributeValue = scale(value);
78✔
1249
    }
1250

1251
    if (!notNullorUndefined(attributeValue)) {
78!
UNCOV
1252
      attributeValue = nullValue;
×
1253
    }
1254

1255
    return attributeValue;
78✔
1256
  }
1257

1258
  updateMeta(meta: Layer['meta']) {
1259
    this.meta = {...this.meta, ...meta};
347✔
1260
  }
1261

1262
  getDataUpdateTriggers({filteredIndex, id, dataContainer}: KeplerTable): any {
1263
    const {columns} = this.config;
479✔
1264

1265
    return {
479✔
1266
      getData: {datasetId: id, dataContainer, columns, filteredIndex},
1267
      getMeta: {datasetId: id, dataContainer, columns},
1268
      ...(this.config.textLabel || []).reduce(
479!
1269
        (accu, tl, i) => ({
488✔
1270
          ...accu,
1271
          [`getLabelCharacterSet-${i}`]: tl.field ? tl.field.name : null
488✔
1272
        }),
1273
        {}
1274
      )
1275
    };
1276
  }
1277

1278
  updateData(datasets: Datasets, oldLayerData: any) {
1279
    if (!this.config.dataId) {
486!
UNCOV
1280
      return {};
×
1281
    }
1282
    const layerDataset = datasets[this.config.dataId];
486✔
1283
    const {dataContainer} = layerDataset;
486✔
1284

1285
    const getPosition = this.getPositionAccessor(dataContainer);
486✔
1286
    const dataUpdateTriggers = this.getDataUpdateTriggers(layerDataset);
486✔
1287
    const triggerChanged = this.getChangedTriggers(dataUpdateTriggers);
486✔
1288

1289
    if (triggerChanged && (triggerChanged.getMeta || triggerChanged.getData)) {
486✔
1290
      this.updateLayerMeta(layerDataset, getPosition);
346✔
1291

1292
      // reset filteredItemCount
1293
      this.filteredItemCount = {};
346✔
1294
    }
1295

1296
    let data = [];
486✔
1297

1298
    if (!(triggerChanged && triggerChanged.getData) && oldLayerData && oldLayerData.data) {
486✔
1299
      // same data
1300
      data = oldLayerData.data;
139✔
1301
    } else {
1302
      data = this.calculateDataAttribute(layerDataset, getPosition);
347✔
1303
    }
1304

1305
    return {data, triggerChanged};
486✔
1306
  }
1307

1308
  /**
1309
   * helper function to update one layer domain when state.data changed
1310
   * if state.data change is due ot update filter, newFiler will be passed
1311
   * called by updateAllLayerDomainData
1312
   * @param datasets
1313
   * @param newFilter
1314
   * @returns layer
1315
   */
1316
  updateLayerDomain(datasets: Datasets, newFilter?: Filter): Layer {
1317
    const table = this.getDataset(datasets);
321✔
1318
    if (!table) {
321!
UNCOV
1319
      return this;
×
1320
    }
1321
    Object.values(this.visualChannels).forEach(channel => {
321✔
1322
      const {scale} = channel;
1,098✔
1323
      const scaleType = this.config[scale];
1,098✔
1324
      // ordinal domain is based on dataContainer, if only filter changed
1325
      // no need to update ordinal domain
1326
      if (!newFilter || scaleType !== SCALE_TYPES.ordinal) {
1,098✔
1327
        const {domain} = channel;
1,094✔
1328
        const updatedDomain = this.calculateLayerDomain(table, channel);
1,094✔
1329
        this.updateLayerConfig({[domain]: updatedDomain});
1,094✔
1330
      }
1331
    });
1332

1333
    return this;
321✔
1334
  }
1335

1336
  getDataset(datasets) {
1337
    return this.config.dataId ? datasets[this.config.dataId] : null;
321!
1338
  }
1339

1340
  /**
1341
   * Validate visual channel field and scales based on supported field & scale type
1342
   * @param channel
1343
   */
1344
  validateVisualChannel(channel: string) {
1345
    this.validateFieldType(channel);
456✔
1346
    this.validateScale(channel);
456✔
1347
  }
1348

1349
  /**
1350
   * Validate field type based on channelScaleType
1351
   */
1352
  validateFieldType(channel: string) {
1353
    const visualChannel = this.visualChannels[channel];
509✔
1354
    const {field, channelScaleType, supportedFieldTypes} = visualChannel;
509✔
1355

1356
    if (this.config[field]) {
509✔
1357
      // if field is selected, check if field type is supported
1358
      const channelSupportedFieldTypes =
1359
        supportedFieldTypes || CHANNEL_SCALE_SUPPORTED_FIELDS[channelScaleType];
122✔
1360

1361
      if (!channelSupportedFieldTypes.includes(this.config[field].type)) {
122!
1362
        // field type is not supported, set it back to null
1363
        // set scale back to default
UNCOV
1364
        this.updateLayerConfig({[field]: null});
×
1365
      }
1366
    }
1367
  }
1368

1369
  /**
1370
   * Validate scale type based on aggregation
1371
   */
1372
  validateScale(channel) {
1373
    const visualChannel = this.visualChannels[channel];
509✔
1374
    const {scale} = visualChannel;
509✔
1375
    if (!scale) {
509!
1376
      // visualChannel doesn't have scale
UNCOV
1377
      return;
×
1378
    }
1379
    const scaleOptions = this.getScaleOptions(channel);
509✔
1380
    // check if current selected scale is
1381
    // supported, if not, change to default
1382
    if (!scaleOptions.includes(this.config[scale])) {
509✔
1383
      this.updateLayerConfig({[scale]: scaleOptions[0]});
41✔
1384
    }
1385
  }
1386

1387
  /**
1388
   * Get scale options based on current field
1389
   * @param {string} channel
1390
   * @returns {string[]}
1391
   */
1392
  getScaleOptions(channel: string): string[] {
1393
    const visualChannel = this.visualChannels[channel];
464✔
1394
    const {field, scale, channelScaleType} = visualChannel;
464✔
1395

1396
    return this.config[field]
464✔
1397
      ? FIELD_OPTS[this.config[field].type].scale[channelScaleType]
1398
      : [this.getDefaultLayerConfig({dataId: ''})[scale]];
1399
  }
1400

1401
  updateLayerVisualChannel(dataset: KeplerTable, channel: string) {
1402
    const visualChannel = this.visualChannels[channel];
33✔
1403
    this.validateVisualChannel(channel);
33✔
1404
    // calculate layer channel domain
1405
    const updatedDomain = this.calculateLayerDomain(dataset, visualChannel);
33✔
1406
    this.updateLayerConfig({[visualChannel.domain]: updatedDomain});
33✔
1407
  }
1408

1409
  getVisualChannelUpdateTriggers(): UpdateTriggers {
1410
    const updateTriggers: UpdateTriggers = {};
43✔
1411
    Object.values(this.visualChannels).forEach(visualChannel => {
43✔
1412
      // field range scale domain
1413
      const {accessor, field, scale, domain, range, defaultValue, fixed} = visualChannel;
153✔
1414

1415
      if (accessor) {
153!
1416
        updateTriggers[accessor] = {
153✔
1417
          [field]: this.config[field],
1418
          [scale]: this.config[scale],
1419
          [domain]: this.config[domain],
1420
          [range]: this.config.visConfig[range],
1421
          defaultValue:
1422
            typeof defaultValue === 'function' ? defaultValue(this.config) : defaultValue,
153✔
1423
          ...(fixed ? {[fixed]: this.config.visConfig[fixed]} : {})
153✔
1424
        };
1425
      }
1426
    });
1427
    return updateTriggers;
43✔
1428
  }
1429

1430
  calculateLayerDomain(dataset, visualChannel) {
1431
    const {scale} = visualChannel;
1,127✔
1432
    const scaleType = this.config[scale];
1,127✔
1433

1434
    const field = this.config[visualChannel.field];
1,127✔
1435
    if (!field) {
1,127✔
1436
      // if colorField or sizeField were set back to null
1437
      return defaultDomain;
996✔
1438
    }
1439

1440
    return dataset.getColumnLayerDomain(field, scaleType) || defaultDomain;
131!
1441
  }
1442

1443
  hasHoveredObject(objectInfo) {
1444
    return this.isLayerHovered(objectInfo) && objectInfo.object ? objectInfo.object : null;
47✔
1445
  }
1446

1447
  isLayerHovered(objectInfo): boolean {
1448
    return objectInfo?.picked && objectInfo?.layer?.props?.id === this.id;
91✔
1449
  }
1450

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

1454
    if (!radiusChannel) {
40!
UNCOV
1455
      return 1;
×
1456
    }
1457

1458
    const field = radiusChannel.field;
40✔
1459
    const fixed = fixedRadius === undefined ? this.config.visConfig.fixedRadius : fixedRadius;
40✔
1460
    const {radius} = this.config.visConfig;
40✔
1461

1462
    return fixed ? 1 : (this.config[field] ? 1 : radius) * this.getZoomFactor(mapState);
40!
1463
  }
1464

1465
  shouldCalculateLayerData(props: string[]) {
1466
    return props.some(p => !this.noneLayerDataAffectingProps.includes(p));
40✔
1467
  }
1468

1469
  getBrushingExtensionProps(interactionConfig, brushingTarget?) {
1470
    const {brush} = interactionConfig;
28✔
1471

1472
    return {
28✔
1473
      // brushing
1474
      autoHighlight: !brush.enabled,
1475
      brushingRadius: brush.config.size * 1000,
1476
      brushingTarget: brushingTarget || 'source',
52✔
1477
      brushingEnabled: brush.enabled
1478
    };
1479
  }
1480

1481
  getDefaultDeckLayerProps({
1482
    idx,
1483
    gpuFilter,
1484
    mapState,
1485
    layerCallbacks,
1486
    visible
1487
  }: {
1488
    idx: number;
1489
    gpuFilter: GpuFilter;
1490
    mapState: MapState;
1491
    layerCallbacks: any;
1492
    visible: boolean;
1493
  }) {
1494
    return {
50✔
1495
      id: this.id,
1496
      idx,
1497
      coordinateSystem: COORDINATE_SYSTEM.LNGLAT,
1498
      pickable: true,
1499
      wrapLongitude: true,
1500
      parameters: {depthTest: Boolean(mapState.dragRotate || this.config.visConfig.enable3d)},
100✔
1501
      hidden: this.config.hidden,
1502
      // visconfig
1503
      opacity: this.config.visConfig.opacity,
1504
      highlightColor: this.config.highlightColor,
1505
      // data filtering
1506
      extensions: [dataFilterExtension],
1507
      filterRange: gpuFilter ? gpuFilter.filterRange : undefined,
50!
1508
      onFilteredItemsChange: gpuFilter ? layerCallbacks?.onFilteredItemsChange : undefined,
50!
1509

1510
      // layer should be visible and if splitMap, shown in to one of panel
1511
      visible: this.config.isVisible && visible
100✔
1512
    };
1513
  }
1514

1515
  getDefaultHoverLayerProps() {
1516
    return {
1✔
1517
      id: `${this.id}-hovered`,
1518
      pickable: false,
1519
      wrapLongitude: true,
1520
      coordinateSystem: COORDINATE_SYSTEM.LNGLAT
1521
    };
1522
  }
1523

1524
  renderTextLabelLayer(
1525
    {
1526
      getPosition,
1527
      getFiltered,
1528
      getPixelOffset,
1529
      backgroundProps,
1530
      updateTriggers,
1531
      sharedProps
1532
    }: {
1533
      getPosition?: ((d: any) => number[]) | arrow.Vector;
1534
      getFiltered?: (data: {index: number}, objectInfo: {index: number}) => number;
1535
      getPixelOffset: (textLabel: any) => number[] | ((d: any) => number[]);
1536
      backgroundProps?: {background: boolean};
1537
      updateTriggers: {
1538
        [key: string]: any;
1539
      };
1540
      sharedProps: any;
1541
    },
1542
    renderOpts
1543
  ) {
1544
    const {data, mapState} = renderOpts;
24✔
1545
    const {textLabel} = this.config;
24✔
1546

1547
    const TextLayerClass = data.data instanceof arrow.Table ? GeoArrowTextLayer : TextLayer;
24!
1548

1549
    return data.textLabels.reduce((accu, d, i) => {
24✔
1550
      if (d.getText) {
26✔
1551
        const background = textLabel[i].background || backgroundProps?.background;
6✔
1552

1553
        accu.push(
6✔
1554
          // @ts-expect-error
1555
          new TextLayerClass({
1556
            ...sharedProps,
1557
            id: `${this.id}-label-${textLabel[i].field?.name}`,
1558
            data: data.data,
1559
            visible: this.config.isVisible,
1560
            getText: d.getText,
1561
            getPosition,
1562
            getFiltered,
1563
            characterSet: d.characterSet,
1564
            getPixelOffset: getPixelOffset(textLabel[i]),
1565
            getSize: PROJECTED_PIXEL_SIZE_MULTIPLIER,
1566
            sizeScale: textLabel[i].size,
1567
            getTextAnchor: textLabel[i].anchor,
1568
            getAlignmentBaseline: textLabel[i].alignment,
1569
            getColor: textLabel[i].color,
1570
            outlineWidth: textLabel[i].outlineWidth * TEXT_OUTLINE_MULTIPLIER,
1571
            outlineColor: textLabel[i].outlineColor,
1572
            background,
1573
            getBackgroundColor: textLabel[i].backgroundColor,
1574
            fontSettings: {
1575
              sdf: textLabel[i].outlineWidth > 0
1576
            },
1577
            parameters: {
1578
              // text will always show on top of all layers
1579
              depthTest: false
1580
            },
1581

1582
            getFilterValue: data.getFilterValue,
1583
            updateTriggers: {
1584
              ...updateTriggers,
1585
              getText: textLabel[i].field?.name,
1586
              getPixelOffset: {
1587
                ...updateTriggers.getRadius,
1588
                mapState,
1589
                anchor: textLabel[i].anchor,
1590
                alignment: textLabel[i].alignment
1591
              },
1592
              getTextAnchor: textLabel[i].anchor,
1593
              getAlignmentBaseline: textLabel[i].alignment,
1594
              getColor: textLabel[i].color
1595
            },
1596
            _subLayerProps: {
1597
              ...(background
6!
1598
                ? {
1599
                    background: {
1600
                      parameters: {
1601
                        cull: false
1602
                      }
1603
                    }
1604
                  }
1605
                : null)
1606
            }
1607
          })
1608
        );
1609
      }
1610
      return accu;
26✔
1611
    }, []);
1612
  }
1613

1614
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1615
  calculateDataAttribute(keplerTable: KeplerTable, getPosition): any {
1616
    // implemented in subclasses
UNCOV
1617
    return [];
×
1618
  }
1619

1620
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1621
  updateLayerMeta(dataset: KeplerTable, getPosition) {
1622
    // implemented in subclasses
1623
  }
1624

1625
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1626
  getPositionAccessor(dataContainer?: DataContainerInterface): (...args: any[]) => any {
1627
    // implemented in subclasses
UNCOV
1628
    return () => null;
×
1629
  }
1630

1631
  getLegendVisualChannels(): {[key: string]: VisualChannel} {
1632
    return this.visualChannels;
18✔
1633
  }
1634
}
1635

1636
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