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

keplergl / kepler.gl / 12509145710

26 Dec 2024 10:43PM UTC coverage: 67.491%. Remained the same
12509145710

push

github

web-flow
[chore] ts refactoring (#2861)

- move several base layer types to layer.d.ts
- other ts changes

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

5841 of 10041 branches covered (58.17%)

Branch coverage included in aggregate %.

8 of 12 new or added lines in 9 files covered. (66.67%)

31 existing lines in 6 files now uncovered.

11978 of 16361 relevant lines covered (73.21%)

87.57 hits per line

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

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

4
import * as arrow from 'apache-arrow';
5

6
import Layer, {
7
  LayerBaseConfig,
8
  LayerColorConfig,
9
  LayerSizeConfig,
10
  LayerBounds,
11
  LayerBaseConfigPartial,
12
  VisualChannel
13
} from '../base-layer';
14
import {BrushingExtension} from '@deck.gl/extensions';
15
import {GeoArrowArcLayer} from '@kepler.gl/deckgl-arrow-layers';
16
import {FilterArrowExtension} from '@kepler.gl/deckgl-layers';
17
import {ArcLayer as DeckArcLayer} from '@deck.gl/layers';
18

19
import {
20
  hexToRgb,
21
  DataContainerInterface,
22
  maybeHexToGeo,
23
  ArrowDataContainer
24
} from '@kepler.gl/utils';
25
import ArcLayerIcon from './arc-layer-icon';
26
import {isLayerHoveredFromArrow, createGeoArrowPointVector, getFilteredIndex} from '../layer-utils';
27
import {
28
  DEFAULT_LAYER_COLOR,
29
  PROJECTED_PIXEL_SIZE_MULTIPLIER,
30
  ALL_FIELD_TYPES
31
} from '@kepler.gl/constants';
32

33
import {
34
  ColorRange,
35
  RGBColor,
36
  Merge,
37
  VisConfigColorRange,
38
  VisConfigColorSelect,
39
  VisConfigNumber,
40
  VisConfigRange,
41
  LayerColumn,
42
  Field,
43
  AnimationConfig
44
} from '@kepler.gl/types';
45
import {KeplerTable} from '@kepler.gl/table';
46

47
export type ArcLayerVisConfigSettings = {
48
  opacity: VisConfigNumber;
49
  thickness: VisConfigNumber;
50
  colorRange: VisConfigColorRange;
51
  sizeRange: VisConfigRange;
52
  targetColor: VisConfigColorSelect;
53
};
54

55
export type ArcLayerColumnsConfig = {
56
  // COLUMN_MODE_POINTS required columns
57
  lat0: LayerColumn;
58
  lat1: LayerColumn;
59
  lng0: LayerColumn;
60
  lng1: LayerColumn;
61

62
  // COLUMN_MODE_NEIGHBORS required columns
63
  lat: LayerColumn;
64
  lng: LayerColumn;
65
  neighbors: LayerColumn;
66

67
  // COLUMN_MODE_GEOARROW
68
  geoarrow0: LayerColumn;
69
  geoarrow1: LayerColumn;
70
};
71

72
export type ArcLayerVisConfig = {
73
  colorRange: ColorRange;
74
  opacity: number;
75
  sizeRange: [number, number];
76
  targetColor: RGBColor;
77
  thickness: number;
78
};
79

80
export type ArcLayerVisualChannelConfig = LayerColorConfig & LayerSizeConfig;
81
export type ArcLayerConfig = Merge<
82
  LayerBaseConfig,
83
  {columns: ArcLayerColumnsConfig; visConfig: ArcLayerVisConfig}
84
> &
85
  ArcLayerVisualChannelConfig;
86

87
export type ArcLayerData = {
88
  index: number;
89
  sourcePosition: [number, number, number];
90
  targetPosition: [number, number, number];
91
};
92

93
export type ArcLayerMeta = {
94
  bounds: LayerBounds;
95
};
96

97
export const arcRequiredColumns = ['lat0', 'lng0', 'lat1', 'lng1'];
11✔
98
export const neighborRequiredColumns = ['lat', 'lng', 'neighbors'];
11✔
99
export const geoarrowRequiredColumns = ['geoarrow0', 'geoarrow1'];
11✔
100

101
export const arcColumnLabels = {
11✔
102
  lat0: 'arc.lat0',
103
  lng0: 'arc.lng0',
104
  lat1: 'arc.lat1',
105
  lng1: 'arc.lng1',
106
  neighbors: 'neighbors'
107
};
108

109
export const arcVisConfigs: {
110
  opacity: 'opacity';
111
  thickness: 'thickness';
112
  colorRange: 'colorRange';
113
  sizeRange: 'strokeWidthRange';
114
  targetColor: 'targetColor';
115
} = {
11✔
116
  opacity: 'opacity',
117
  thickness: 'thickness',
118
  colorRange: 'colorRange',
119
  sizeRange: 'strokeWidthRange',
120
  targetColor: 'targetColor'
121
};
122

123
export const COLUMN_MODE_POINTS = 'points';
11✔
124
export const COLUMN_MODE_NEIGHBORS = 'neighbors';
11✔
125
export const COLUMN_MODE_GEOARROW = 'geoarrow';
11✔
126
const SUPPORTED_COLUMN_MODES = [
11✔
127
  {
128
    key: COLUMN_MODE_POINTS,
129
    label: 'Points',
130
    requiredColumns: arcRequiredColumns
131
  },
132
  {
133
    key: COLUMN_MODE_NEIGHBORS,
134
    label: 'Point and Neighbors',
135
    requiredColumns: neighborRequiredColumns
136
  },
137
  {
138
    key: COLUMN_MODE_GEOARROW,
139
    label: 'Geoarrow Points',
140
    requiredColumns: geoarrowRequiredColumns
141
  }
142
];
143
const DEFAULT_COLUMN_MODE = COLUMN_MODE_POINTS;
11✔
144

145
const brushingExtension = new BrushingExtension();
11✔
146
const arrowCPUFilterExtension = new FilterArrowExtension();
11✔
147

148
function isH3Field(columns, allFields, key) {
149
  const field = allFields[columns[key].fieldIdx];
6✔
150
  return field?.type === ALL_FIELD_TYPES.h3;
6✔
151
}
152

153
export const arcPosAccessor =
154
  ({lat0, lng0, lat1, lng1, lat, lng, geoarrow0, geoarrow1}: ArcLayerColumnsConfig, columnMode) =>
11✔
155
  (dc: DataContainerInterface) => {
89✔
156
    switch (columnMode) {
89!
157
      case COLUMN_MODE_GEOARROW:
158
        return d => {
×
159
          const start = dc.valueAt(d.index, geoarrow0.fieldIdx);
×
160
          const end = dc.valueAt(d.index, geoarrow1.fieldIdx);
×
161
          return [start.get(0), start.get(1), 0, end.get(2), end.get(3), 0];
×
162
        };
163
      case COLUMN_MODE_NEIGHBORS:
164
        return d => {
6✔
165
          const startPos = maybeHexToGeo(dc, d, lat, lng);
234✔
166
          // only return source point if columnMode is COLUMN_MODE_NEIGHBORS
167

168
          return [
234✔
169
            startPos ? startPos[0] : dc.valueAt(d.index, lng.fieldIdx),
234!
170
            startPos ? startPos[1] : dc.valueAt(d.index, lat.fieldIdx),
234!
171
            0
172
          ];
173
        };
174
      default:
175
        // COLUMN_MODE_POINTS
176
        return d => {
83✔
177
          // lat or lng column could be hex column
178
          // we assume string value is hex and try to convert it to geo lat lng
179
          const startPos = maybeHexToGeo(dc, d, lat0, lng0);
758✔
180
          const endPos = maybeHexToGeo(dc, d, lat1, lng1);
758✔
181
          return [
758✔
182
            startPos ? startPos[0] : dc.valueAt(d.index, lng0.fieldIdx),
758✔
183
            startPos ? startPos[1] : dc.valueAt(d.index, lat0.fieldIdx),
758✔
184
            0,
185
            endPos ? endPos[0] : dc.valueAt(d.index, lng1.fieldIdx),
758✔
186
            endPos ? endPos[1] : dc.valueAt(d.index, lat1.fieldIdx),
758✔
187
            0
188
          ];
189
        };
190
    }
191
  };
192
export default class ArcLayer extends Layer {
193
  declare visConfigSettings: ArcLayerVisConfigSettings;
194
  declare config: ArcLayerConfig;
195
  declare meta: ArcLayerMeta;
196

197
  dataContainer: DataContainerInterface | null = null;
132✔
198
  geoArrowVector0: arrow.Vector | undefined = undefined;
132✔
199
  geoArrowVector1: arrow.Vector | undefined = undefined;
132✔
200

201
  /*
202
   * CPU filtering an arrow table by values and assembling a partial copy of the raw table is expensive
203
   * so we will use filteredIndex to create an attribute e.g. filteredIndex [0|1] for GPU filtering
204
   * in deck.gl layer, see: FilterArrowExtension in @kepler.gl/deckgl-layers.
205
   * Note that this approach can create visible lags in case of a lot of discarted geometry.
206
   */
207
  filteredIndex: Uint8ClampedArray | null = null;
132✔
208
  filteredIndexTrigger: number[] = [];
132✔
209

210
  constructor(props) {
211
    super(props);
132✔
212

213
    this.registerVisConfig(arcVisConfigs);
132✔
214
    this.getPositionAccessor = (dataContainer: DataContainerInterface) =>
132✔
215
      arcPosAccessor(this.config.columns, this.config.columnMode)(dataContainer);
89✔
216
  }
217

218
  get type() {
219
    return 'arc';
159✔
220
  }
221

222
  get isAggregated() {
223
    return false;
2✔
224
  }
225

226
  get layerIcon() {
227
    return ArcLayerIcon;
28✔
228
  }
229

230
  get columnLabels(): Record<string, string> {
231
    return arcColumnLabels;
×
232
  }
233

234
  get columnPairs() {
235
    return this.defaultLinkColumnPairs;
1✔
236
  }
237

238
  get supportedColumnModes() {
239
    return SUPPORTED_COLUMN_MODES;
191✔
240
  }
241

242
  get visualChannels() {
243
    return {
596✔
244
      sourceColor: {
245
        ...super.visualChannels.color,
246
        property: 'color',
247
        key: 'sourceColor',
248
        accessor: 'getSourceColor',
249
        defaultValue: config => config.color
86✔
250
      },
251
      targetColor: {
252
        ...super.visualChannels.color,
253
        property: 'targetColor',
254
        key: 'targetColor',
255
        accessor: 'getTargetColor',
256
        defaultValue: config => config.visConfig.targetColor || config.color
86✔
257
      },
258
      size: {
259
        ...super.visualChannels.size,
260
        accessor: 'getWidth',
261
        property: 'stroke'
262
      }
263
    };
264
  }
265

266
  get columnValidators() {
267
    // if one of the lat or lng column is string type, we allow it
268
    // will try to pass it as hex
269
    return {
152✔
270
      lat0: (column, columns, allFields) => isH3Field(columns, allFields, 'lng0'),
×
271
      lng0: (column, columns, allFields) => isH3Field(columns, allFields, 'lat0'),
3✔
272
      lat1: (column, columns, allFields) => isH3Field(columns, allFields, 'lng1'),
×
273
      lng1: (column, columns, allFields) => isH3Field(columns, allFields, 'lat1'),
3✔
274
      lat: (column, columns, allFields) => isH3Field(columns, allFields, 'lng'),
×
275
      lng: (column, columns, allFields) => isH3Field(columns, allFields, 'lat')
×
276
    };
277
  }
278

279
  hasAllColumns() {
280
    const {columns} = this.config;
72✔
281
    if (this.config.columnMode === COLUMN_MODE_GEOARROW) {
72!
282
      return this.hasColumnValue(columns.geoarrow0) && this.hasColumnValue(columns.geoarrow1);
×
283
    }
284
    if (this.config.columnMode === COLUMN_MODE_POINTS) {
72✔
285
      // TODO - this does not have access to allFields...
286
      // So we can't do the same validation as for the field errors
287
      const hasStart = this.hasColumnValue(columns.lat0) || this.hasColumnValue(columns.lng0);
70!
288
      const hasEnd = this.hasColumnValue(columns.lat1) || this.hasColumnValue(columns.lng1);
70!
289
      return hasStart && hasEnd;
70✔
290
    }
291
    const hasStart = this.hasColumnValue(columns.lat) || this.hasColumnValue(columns.lng);
2!
292
    const hasNeibors = this.hasColumnValue(columns.neighbors);
2✔
293
    return hasStart && hasNeibors;
2✔
294
  }
295

296
  static findDefaultLayerProps({fields, fieldPairs = []}: KeplerTable): {
×
297
    props: {color?: RGBColor; columns: ArcLayerColumnsConfig; label: string}[];
298
  } {
299
    if (fieldPairs.length < 2) {
90✔
300
      return {props: []};
67✔
301
    }
302

303
    const props: {
304
      color: RGBColor;
305
      columns: ArcLayerColumnsConfig;
306
      label: string;
307
    } = {
23✔
308
      color: hexToRgb(DEFAULT_LAYER_COLOR.tripArc),
309
      // connect the first two point layer with arc
310
      // @ts-expect-error separate types for point / neighbor columns
311
      columns: {
312
        lat0: fieldPairs[0].pair.lat,
313
        lng0: fieldPairs[0].pair.lng,
314
        lat1: fieldPairs[1].pair.lat,
315
        lng1: fieldPairs[1].pair.lng
316
      },
317
      label: `${fieldPairs[0].defaultName} -> ${fieldPairs[1].defaultName} arc`
318
    };
319

320
    return {props: [props]};
23✔
321
  }
322

323
  getDefaultLayerConfig(props: LayerBaseConfigPartial) {
324
    const defaultLayerConfig = super.getDefaultLayerConfig(props);
186✔
325

326
    return {
186✔
327
      ...defaultLayerConfig,
328
      columnMode: props?.columnMode ?? DEFAULT_COLUMN_MODE
366✔
329
    };
330
  }
331

332
  calculateDataAttributeForGeoArrow(
333
    {dataContainer, filteredIndex}: {dataContainer: ArrowDataContainer; filteredIndex: number[]},
334
    getPosition
335
  ) {
336
    this.filteredIndex = getFilteredIndex(
×
337
      dataContainer.numRows(),
338
      filteredIndex,
339
      this.filteredIndex
340
    );
341
    this.filteredIndexTrigger = filteredIndex;
×
342

343
    if (this.config.columnMode === COLUMN_MODE_GEOARROW) {
×
344
      this.geoArrowVector0 = dataContainer.getColumn(this.config.columns.geoarrow0.fieldIdx);
×
345
      this.geoArrowVector1 = dataContainer.getColumn(this.config.columns.geoarrow1.fieldIdx);
×
346
    } else {
347
      // generate columns compatible with geoarrow point extension
348
      // TODO remove excessive intermediate allocations
349
      this.geoArrowVector0 = createGeoArrowPointVector(dataContainer, d => {
×
350
        return getPosition(d).slice(0, 3);
×
351
      });
352
      this.geoArrowVector1 = createGeoArrowPointVector(dataContainer, d => {
×
353
        return getPosition(d).slice(3, 6);
×
354
      });
355
    }
356

357
    return dataContainer.getTable();
×
358
  }
359

360
  calculateDataAttributeForPoints(
361
    {filteredIndex}: {dataContainer: DataContainerInterface; filteredIndex: number[]},
362
    getPosition
363
  ) {
364
    const data: ArcLayerData[] = [];
69✔
365
    for (let i = 0; i < filteredIndex.length; i++) {
69✔
366
      const index = filteredIndex[i];
365✔
367
      const pos = getPosition({index});
365✔
368

369
      // if doesn't have point lat or lng, do not add the point
370
      // deck.gl can't handle position = null
371
      if (pos.every(Number.isFinite)) {
365✔
372
        data.push({
321✔
373
          index,
374
          sourcePosition: [pos[0], pos[1], pos[2]],
375
          targetPosition: [pos[3], pos[4], pos[5]]
376
        });
377
      }
378
    }
379
    return data;
69✔
380
  }
381

382
  calculateDataAttributeForPointNNeighbors(
383
    {
384
      dataContainer,
385
      filteredIndex
386
    }: {dataContainer: DataContainerInterface; filteredIndex: number[]},
387
    getPosition
388
  ) {
389
    const data: {index: number; sourcePosition: number[]; targetPosition: number[]}[] = [];
3✔
390
    for (let i = 0; i < filteredIndex.length; i++) {
3✔
391
      const index = filteredIndex[i];
60✔
392
      const pos = getPosition({index});
60✔
393
      // if doesn't have point lat or lng, do not add the point
394
      // deck.gl can't handle position = null
395
      if (pos.every(Number.isFinite)) {
60!
396
        // push all neibors
397
        const neighborIdx = this.config.columns.neighbors.value
60!
398
          ? dataContainer.valueAt(index, this.config.columns.neighbors.fieldIdx)
399
          : [];
400
        if (Array.isArray(neighborIdx)) {
60!
401
          neighborIdx.forEach(idx => {
60✔
402
            // TODO prevent row materialization here
403
            const tPos = dataContainer.rowAsArray(idx) ? getPosition({index: idx}) : null;
123✔
404
            if (tPos && tPos.every(Number.isFinite)) {
123✔
405
              data.push({
114✔
406
                index,
407
                sourcePosition: [pos[0], pos[1], pos[2]],
408
                targetPosition: [tPos[0], tPos[1], tPos[2]]
409
              });
410
            }
411
          });
412
        }
413
      }
414
    }
415

416
    return data;
3✔
417
  }
418

419
  calculateDataAttribute({dataContainer, filteredIndex}: KeplerTable, getPosition) {
420
    const {columnMode} = this.config;
72✔
421

422
    // 1) COLUMN_MODE_GEOARROW - when we have a geoarrow point column
423
    // 2) COLUMN_MODE_POINTS + ArrowDataContainer > create geoarrow point column on the fly
424
    if (
72!
425
      dataContainer instanceof ArrowDataContainer &&
72!
426
      (columnMode === COLUMN_MODE_GEOARROW || columnMode === COLUMN_MODE_POINTS)
427
    ) {
428
      return this.calculateDataAttributeForGeoArrow({dataContainer, filteredIndex}, getPosition);
×
429
    }
430

431
    // we don't need these in non-Arrow modes atm.
432
    this.geoArrowVector0 = undefined;
72✔
433
    this.geoArrowVector1 = undefined;
72✔
434
    this.filteredIndex = null;
72✔
435

436
    if (this.config.columnMode === COLUMN_MODE_POINTS) {
72✔
437
      return this.calculateDataAttributeForPoints({dataContainer, filteredIndex}, getPosition);
69✔
438
    }
439
    return this.calculateDataAttributeForPointNNeighbors(
3✔
440
      {dataContainer, filteredIndex},
441
      getPosition
442
    );
443
  }
444

445
  formatLayerData(datasets, oldLayerData) {
446
    if (this.config.dataId === null) {
84!
447
      return {};
×
448
    }
449
    const {gpuFilter, dataContainer} = datasets[this.config.dataId];
84✔
450
    const {data} = this.updateData(datasets, oldLayerData);
84✔
451
    const accessors = this.getAttributeAccessors({dataContainer});
84✔
452
    const isFilteredAccessor = (data: {index: number}) => {
84✔
453
      // for GeoArrow data is a buffer, so use objectInfo
454
      return this.filteredIndex ? this.filteredIndex[data.index] : 1;
×
455
    };
456

457
    return {
84✔
458
      data,
459
      getFilterValue: gpuFilter.filterValueAccessor(dataContainer)(),
460
      getFiltered: isFilteredAccessor,
461
      ...accessors
462
    };
463
  }
464
  /* eslint-enable complexity */
465

466
  updateLayerMeta(dataset: KeplerTable) {
467
    const {dataContainer} = dataset;
72✔
468

469
    this.dataContainer = dataContainer;
72✔
470

471
    // get bounds from arcs
472
    const getPosition = this.getPositionAccessor(dataContainer);
72✔
473

474
    const sBounds = this.getPointsBounds(dataContainer, d => {
72✔
475
      const pos = getPosition(d);
505✔
476
      return [pos[0], pos[1]];
505✔
477
    });
478

479
    let tBounds: number[] | null = [];
72✔
480
    if (this.config.columnMode === COLUMN_MODE_POINTS) {
72✔
481
      tBounds = this.getPointsBounds(dataContainer, d => {
69✔
482
        const pos = getPosition(d);
445✔
483
        return [pos[3], pos[4]];
445✔
484
      });
485
    } else {
486
      // when columnMode is neighbors, it reference the same collection of points
487
      tBounds = sBounds;
3✔
488
    }
489

490
    const bounds =
491
      tBounds && sBounds
72!
492
        ? [
493
            Math.min(sBounds[0], tBounds[0]),
494
            Math.min(sBounds[1], tBounds[1]),
495
            Math.max(sBounds[2], tBounds[2]),
496
            Math.max(sBounds[3], tBounds[3])
497
          ]
498
        : sBounds || tBounds;
×
499

500
    this.updateMeta({bounds});
72✔
501
  }
502

503
  renderLayer(opts) {
504
    const {data, gpuFilter, objectHovered, interactionConfig, dataset} = opts;
2✔
505
    const updateTriggers = {
2✔
506
      getPosition: this.config.columns,
507
      getFilterValue: gpuFilter.filterValueUpdateTriggers,
508
      getFiltered: this.filteredIndexTrigger,
509
      ...this.getVisualChannelUpdateTriggers()
510
    };
511
    const widthScale = this.config.visConfig.thickness * PROJECTED_PIXEL_SIZE_MULTIPLIER;
2✔
512
    const defaultLayerProps = this.getDefaultDeckLayerProps(opts);
2✔
513
    const hoveredObject = this.hasHoveredObject(objectHovered);
2✔
514

515
    const useArrowLayer = Boolean(this.geoArrowVector0);
2✔
516

517
    let ArcLayerClass: typeof DeckArcLayer | typeof GeoArrowArcLayer = DeckArcLayer;
2✔
518
    let experimentalPropOverrides: {
519
      data?: arrow.Table;
520
      getSourcePosition?: arrow.Vector;
521
      getTargetPosition?: arrow.Vector;
522
    } = {};
2✔
523

524
    if (useArrowLayer) {
2!
525
      ArcLayerClass = GeoArrowArcLayer;
×
526
      experimentalPropOverrides = {
×
527
        data: dataset.dataContainer.getTable(),
528
        getSourcePosition: this.geoArrowVector0,
529
        getTargetPosition: this.geoArrowVector1
530
      };
531
    }
532

533
    return [
2✔
534
      // @ts-expect-error
535
      new ArcLayerClass({
536
        ...defaultLayerProps,
537
        ...this.getBrushingExtensionProps(interactionConfig, 'source_target'),
538
        ...data,
539
        ...experimentalPropOverrides,
540
        widthScale,
541
        updateTriggers,
542
        extensions: [
543
          ...defaultLayerProps.extensions,
544
          brushingExtension,
545
          ...(useArrowLayer ? [arrowCPUFilterExtension] : [])
2!
546
        ]
547
      }),
548
      // hover layer
549
      ...(hoveredObject
2!
550
        ? [
551
            new DeckArcLayer({
552
              ...this.getDefaultHoverLayerProps(),
553
              visible: defaultLayerProps.visible,
554
              data: [hoveredObject],
555
              widthScale,
556
              getSourceColor: this.config.highlightColor,
557
              getTargetColor: this.config.highlightColor,
558
              getWidth: data.getWidth
559
            })
560
          ]
561
        : [])
562
    ];
563
  }
564

565
  hasHoveredObject(objectInfo: {index: number}) {
566
    if (
4!
567
      isLayerHoveredFromArrow(objectInfo, this.id) &&
4!
568
      objectInfo.index >= 0 &&
569
      this.dataContainer
570
    ) {
571
      return {
×
572
        index: objectInfo.index,
573
        position: this.getPositionAccessor(this.dataContainer)(objectInfo)
574
      };
575
    }
576

577
    return super.hasHoveredObject(objectInfo);
4✔
578
  }
579

580
  getHoverData(
581
    object: {index: number} | arrow.StructRow | undefined,
582
    dataContainer: DataContainerInterface,
583
    fields: Field[],
584
    animationConfig: AnimationConfig,
585
    hoverInfo: {index: number}
586
  ) {
587
    // for arrow format, `object` is the Arrow row object Proxy,
588
    // and index is passed in `hoverInfo`.
NEW
589
    const index = this.geoArrowVector0 ? hoverInfo?.index : (object as {index: number}).index;
×
590
    if (index >= 0) {
×
591
      return dataContainer.row(index);
×
592
    }
UNCOV
593
    return null;
×
594
  }
595

596
  getLegendVisualChannels() {
UNCOV
597
    let channels: {[key: string]: VisualChannel} = this.visualChannels;
×
598
    if (channels.sourceColor?.field && this.config[channels.sourceColor.field]) {
×
599
      // Remove targetColor to avoid duplicate legend
UNCOV
600
      channels = {...channels};
×
601
      delete channels.targetColor;
×
602
    }
UNCOV
603
    return channels;
×
604
  }
605
}
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