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

keplergl / kepler.gl / 12236323982

09 Dec 2024 01:09PM UTC coverage: 69.193% (-0.02%) from 69.212%
12236323982

push

github

web-flow
[fix] Line layer is not displayed for between hex ids (#2820)

- fix Line layer is not displayed for between hex ID's
- rename type ColumnModeConfig since ColumnModeConfig
- export ColumnModeConfig

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

5452 of 9133 branches covered (59.7%)

Branch coverage included in aggregate %.

10 of 14 new or added lines in 2 files covered. (71.43%)

25 existing lines in 1 file now uncovered.

11384 of 15199 relevant lines covered (74.9%)

95.51 hits per line

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

73.0
/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
} from '../base-layer';
13
import {BrushingExtension} from '@deck.gl/extensions';
14
import {GeoArrowArcLayer} from '@kepler.gl/deckgl-arrow-layers';
15
import {FilterArrowExtension} from '@kepler.gl/deckgl-layers';
16
import {ArcLayer as DeckArcLayer} from '@deck.gl/layers';
17

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

27
import {
28
  RGBColor,
29
  Merge,
30
  VisConfigColorRange,
31
  VisConfigColorSelect,
32
  VisConfigNumber,
33
  VisConfigRange,
34
  LayerColumn,
35
  Field,
36
  AnimationConfig
37
} from '@kepler.gl/types';
38
import {KeplerTable} from '@kepler.gl/table';
39

40
export type ArcLayerVisConfigSettings = {
41
  opacity: VisConfigNumber;
42
  thickness: VisConfigNumber;
43
  colorRange: VisConfigColorRange;
44
  sizeRange: VisConfigRange;
45
  targetColor: VisConfigColorSelect;
46
};
47

48
export type ArcLayerColumnsConfig = {
49
  // COLUMN_MODE_POINTS required columns
50
  lat0: LayerColumn;
51
  lat1: LayerColumn;
52
  lng0: LayerColumn;
53
  lng1: LayerColumn;
54

55
  // COLUMN_MODE_NEIGHBORS required columns
56
  lat: LayerColumn;
57
  lng: LayerColumn;
58
  neighbors: LayerColumn;
59

60
  // COLUMN_MODE_GEOARROW
61
  geoarrow0: LayerColumn;
62
  geoarrow1: LayerColumn;
63
};
64

65
export type ArcLayerVisConfig = {
66
  colorRange: ColorRange;
67
  opacity: number;
68
  sizeRange: [number, number];
69
  targetColor: RGBColor;
70
  thickness: number;
71
};
72

73
export type ArcLayerVisualChannelConfig = LayerColorConfig & LayerSizeConfig;
74
export type ArcLayerConfig = Merge<
75
  LayerBaseConfig,
76
  {columns: ArcLayerColumnsConfig; visConfig: ArcLayerVisConfig}
77
> &
78
  ArcLayerVisualChannelConfig;
79

80
export type ArcLayerData = {
81
  index: number;
82
  sourcePosition: [number, number, number];
83
  targetPosition: [number, number, number];
84
};
85

86
export type ArcLayerMeta = {
87
  bounds: LayerBounds;
88
};
89

90
export const arcRequiredColumns = ['lat0', 'lng0', 'lat1', 'lng1'];
91
export const neighborRequiredColumns = ['lat', 'lng', 'neighbors'];
92
export const geoarrowRequiredColumns = ['geoarrow0', 'geoarrow1'];
93

94
export const arcColumnLabels = {
95
  lat0: 'arc.lat0',
11✔
96
  lng0: 'arc.lng0',
11✔
97
  lat1: 'arc.lat1',
11✔
98
  lng1: 'arc.lng1',
99
  neighbors: 'neighbors'
11✔
100
};
101

102
export const arcVisConfigs: {
103
  opacity: 'opacity';
104
  thickness: 'thickness';
105
  colorRange: 'colorRange';
106
  sizeRange: 'strokeWidthRange';
107
  targetColor: 'targetColor';
108
} = {
109
  opacity: 'opacity',
110
  thickness: 'thickness',
111
  colorRange: 'colorRange',
112
  sizeRange: 'strokeWidthRange',
113
  targetColor: 'targetColor'
11✔
114
};
115

116
export const COLUMN_MODE_POINTS = 'points';
117
export const COLUMN_MODE_NEIGHBORS = 'neighbors';
118
export const COLUMN_MODE_GEOARROW = 'geoarrow';
119
const SUPPORTED_COLUMN_MODES = [
120
  {
121
    key: COLUMN_MODE_POINTS,
11✔
122
    label: 'Points',
11✔
123
    requiredColumns: arcRequiredColumns
11✔
124
  },
11✔
125
  {
126
    key: COLUMN_MODE_NEIGHBORS,
127
    label: 'Point and Neighbors',
128
    requiredColumns: neighborRequiredColumns
129
  },
130
  {
131
    key: COLUMN_MODE_GEOARROW,
132
    label: 'Geoarrow Points',
133
    requiredColumns: geoarrowRequiredColumns
134
  }
135
];
136
const DEFAULT_COLUMN_MODE = COLUMN_MODE_POINTS;
137

138
const brushingExtension = new BrushingExtension();
139
const arrowCPUFilterExtension = new FilterArrowExtension();
140

141
function isOtherFieldString(columns, allFields, key) {
11✔
142
  const field = allFields[columns[key].fieldIdx];
143
  return field && field.type === 'string';
11✔
144
}
11✔
145
export const arcPosAccessor =
146
  ({lat0, lng0, lat1, lng1, lat, lng, geoarrow0, geoarrow1}: ArcLayerColumnsConfig, columnMode) =>
147
  (dc: DataContainerInterface) => {
8✔
148
    switch (columnMode) {
8✔
149
      case COLUMN_MODE_GEOARROW:
150
        return d => {
151
          const start = dc.valueAt(d.index, geoarrow0.fieldIdx);
11✔
152
          const end = dc.valueAt(d.index, geoarrow1.fieldIdx);
95✔
153
          return [start.get(0), start.get(1), 0, end.get(2), end.get(3), 0];
95!
154
        };
UNCOV
155
      case COLUMN_MODE_NEIGHBORS:
×
UNCOV
156
        return d => {
×
UNCOV
157
          const startPos = maybeHexToGeo(dc, d, lat, lng);
×
UNCOV
158
          // only return source point if columnMode is COLUMN_MODE_NEIGHBORS
×
159

160
          return [
161
            startPos ? startPos[0] : dc.valueAt(d.index, lng.fieldIdx),
8✔
162
            startPos ? startPos[1] : dc.valueAt(d.index, lat.fieldIdx),
312✔
163
            0
164
          ];
165
        };
312✔
166
      default:
312!
167
        // COLUMN_MODE_POINTS
312!
168
        return d => {
169
          // lat or lng column could be hex column
170
          // we assume string value is hex and try to convert it to geo lat lng
171
          const startPos = maybeHexToGeo(dc, d, lat0, lng0);
172
          const endPos = maybeHexToGeo(dc, d, lat1, lng1);
173
          return [
87✔
174
            startPos ? startPos[0] : dc.valueAt(d.index, lng0.fieldIdx),
175
            startPos ? startPos[1] : dc.valueAt(d.index, lat0.fieldIdx),
176
            0,
836✔
177
            endPos ? endPos[0] : dc.valueAt(d.index, lng1.fieldIdx),
836✔
178
            endPos ? endPos[1] : dc.valueAt(d.index, lat1.fieldIdx),
836✔
179
            0
836✔
180
          ];
836✔
181
        };
182
    }
836✔
183
  };
836✔
184
export default class ArcLayer extends Layer {
185
  declare visConfigSettings: ArcLayerVisConfigSettings;
186
  declare config: ArcLayerConfig;
187
  declare meta: ArcLayerMeta;
188

189
  dataContainer: DataContainerInterface | null = null;
190
  geoArrowVector0: arrow.Vector | undefined = undefined;
191
  geoArrowVector1: arrow.Vector | undefined = undefined;
192

193
  /*
194
   * CPU filtering an arrow table by values and assembling a partial copy of the raw table is expensive
135✔
195
   * so we will use filteredIndex to create an attribute e.g. filteredIndex [0|1] for GPU filtering
135✔
196
   * in deck.gl layer, see: FilterArrowExtension in @kepler.gl/deckgl-layers.
135✔
197
   * Note that this approach can create visible lags in case of a lot of discarted geometry.
198
   */
199
  filteredIndex: Uint8ClampedArray | null = null;
200
  filteredIndexTrigger: number[] = [];
201

202
  constructor(props) {
203
    super(props);
204

135✔
205
    this.registerVisConfig(arcVisConfigs);
135✔
206
    this.getPositionAccessor = (dataContainer: DataContainerInterface) =>
207
      arcPosAccessor(this.config.columns, this.config.columnMode)(dataContainer);
208
  }
135✔
209

210
  get type() {
135✔
211
    return 'arc';
135✔
212
  }
95✔
213

214
  get isAggregated() {
215
    return false;
216
  }
162✔
217

218
  get layerIcon() {
219
    return ArcLayerIcon;
220
  }
2✔
221

222
  get columnLabels(): Record<string, string> {
223
    return arcColumnLabels;
224
  }
28✔
225

226
  get columnPairs() {
227
    return this.defaultLinkColumnPairs;
UNCOV
228
  }
×
229

230
  get supportedColumnModes() {
231
    return SUPPORTED_COLUMN_MODES;
232
  }
1✔
233

234
  get visualChannels() {
235
    return {
236
      sourceColor: {
206✔
237
        ...super.visualChannels.color,
238
        property: 'color',
239
        key: 'sourceColor',
240
        accessor: 'getSourceColor',
641✔
241
        defaultValue: config => config.color
242
      },
243
      targetColor: {
244
        ...super.visualChannels.color,
245
        property: 'targetColor',
246
        key: 'targetColor',
89✔
247
        accessor: 'getTargetColor',
248
        defaultValue: config => config.visConfig.targetColor || config.color
249
      },
250
      size: {
251
        ...super.visualChannels.size,
252
        accessor: 'getWidth',
253
        property: 'stroke'
89✔
254
      }
255
    };
256
  }
257

258
  get columnValidators() {
259
    // if one of the lat or lng column is string type, we allow it
260
    // will try to pass it as hex
261
    return {
262
      lat0: (column, columns, allFields) => isOtherFieldString(columns, allFields, 'lng0'),
263
      lng0: (column, columns, allFields) => isOtherFieldString(columns, allFields, 'lat0'),
264
      lat1: (column, columns, allFields) => isOtherFieldString(columns, allFields, 'lng1'),
265
      lng1: (column, columns, allFields) => isOtherFieldString(columns, allFields, 'lat1'),
266
      lat: (column, columns, allFields) => isOtherFieldString(columns, allFields, 'lng'),
158✔
267
      lng: (column, columns, allFields) => isOtherFieldString(columns, allFields, 'lat')
×
268
    };
4✔
UNCOV
269
  }
×
270

4✔
UNCOV
271
  hasAllColumns() {
×
UNCOV
272
    const {columns} = this.config;
×
273
    if (this.config.columnMode === COLUMN_MODE_GEOARROW) {
274
      return this.hasColumnValue(columns.geoarrow0) && this.hasColumnValue(columns.geoarrow1);
275
    }
276
    if (this.config.columnMode === COLUMN_MODE_POINTS) {
277
      // TODO - this does not have access to allFields...
75✔
278
      // So we can't do the same validation as for the field errors
75!
UNCOV
279
      const hasStart = this.hasColumnValue(columns.lat0) || this.hasColumnValue(columns.lng0);
×
280
      const hasEnd = this.hasColumnValue(columns.lat1) || this.hasColumnValue(columns.lng1);
281
      return hasStart && hasEnd;
75✔
282
    }
283
    const hasStart = this.hasColumnValue(columns.lat) || this.hasColumnValue(columns.lng);
284
    const hasNeibors = this.hasColumnValue(columns.neighbors);
72!
285
    return hasStart && hasNeibors;
72!
286
  }
72✔
287

288
  static findDefaultLayerProps({fields, fieldPairs = []}: KeplerTable): {
3!
289
    props: {color?: RGBColor; columns: ArcLayerColumnsConfig; label: string}[];
3✔
290
  } {
3✔
291
    if (fieldPairs.length < 2) {
292
      return {props: []};
293
    }
×
294

295
    const props: {
296
      color: RGBColor;
106✔
297
      columns: ArcLayerColumnsConfig;
83✔
298
      label: string;
299
    } = {
300
      color: hexToRgb(DEFAULT_LAYER_COLOR.tripArc),
301
      // connect the first two point layer with arc
302
      // @ts-expect-error separate types for point / neighbor columns
303
      columns: {
304
        lat0: fieldPairs[0].pair.lat,
23✔
305
        lng0: fieldPairs[0].pair.lng,
306
        lat1: fieldPairs[1].pair.lat,
307
        lng1: fieldPairs[1].pair.lng
308
      },
309
      label: `${fieldPairs[0].defaultName} -> ${fieldPairs[1].defaultName} arc`
310
    };
311

312
    return {props: [props]};
313
  }
314

315
  getDefaultLayerConfig(props: LayerBaseConfigPartial) {
316
    const defaultLayerConfig = super.getDefaultLayerConfig(props);
317

23✔
318
    return {
319
      ...defaultLayerConfig,
320
      columnMode: props?.columnMode ?? DEFAULT_COLUMN_MODE
321
    };
198✔
322
  }
323

198✔
324
  calculateDataAttributeForGeoArrow(
325
    {dataContainer, filteredIndex}: {dataContainer: ArrowDataContainer; filteredIndex: number[]},
388✔
326
    getPosition
327
  ) {
328
    this.filteredIndex = getFilteredIndex(
329
      dataContainer.numRows(),
330
      filteredIndex,
331
      this.filteredIndex
332
    );
333
    this.filteredIndexTrigger = filteredIndex;
×
334

335
    if (this.config.columnMode === COLUMN_MODE_GEOARROW) {
336
      this.geoArrowVector0 = dataContainer.getColumn(this.config.columns.geoarrow0.fieldIdx);
337
      this.geoArrowVector1 = dataContainer.getColumn(this.config.columns.geoarrow1.fieldIdx);
UNCOV
338
    } else {
×
339
      // generate columns compatible with geoarrow point extension
UNCOV
340
      // TODO remove excessive intermediate allocations
×
341
      this.geoArrowVector0 = createGeoArrowPointVector(dataContainer, d => {
×
342
        return getPosition(d).slice(0, 3);
×
343
      });
344
      this.geoArrowVector1 = createGeoArrowPointVector(dataContainer, d => {
345
        return getPosition(d).slice(3, 6);
UNCOV
346
      });
×
UNCOV
347
    }
×
348

349
    return dataContainer.getTable();
×
UNCOV
350
  }
×
351

352
  calculateDataAttributeForPoints(
353
    {filteredIndex}: {dataContainer: DataContainerInterface; filteredIndex: number[]},
UNCOV
354
    getPosition
×
355
  ) {
356
    const data: ArcLayerData[] = [];
357
    for (let i = 0; i < filteredIndex.length; i++) {
358
      const index = filteredIndex[i];
359
      const pos = getPosition({index});
360

361
      // if doesn't have point lat or lng, do not add the point
71✔
362
      // deck.gl can't handle position = null
71✔
363
      if (pos.every(Number.isFinite)) {
391✔
364
        data.push({
391✔
365
          index,
366
          sourcePosition: [pos[0], pos[1], pos[2]],
367
          targetPosition: [pos[3], pos[4], pos[5]]
368
        });
391✔
369
      }
347✔
370
    }
371
    return data;
372
  }
373

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

408
    return data;
409
  }
410

411
  calculateDataAttribute({dataContainer, filteredIndex}: KeplerTable, getPosition) {
412
    const {columnMode} = this.config;
413

4✔
414
    // 1) COLUMN_MODE_GEOARROW - when we have a geoarrow point column
415
    // 2) COLUMN_MODE_POINTS + ArrowDataContainer > create geoarrow point column on the fly
416
    if (
417
      dataContainer instanceof ArrowDataContainer &&
75✔
418
      (columnMode === COLUMN_MODE_GEOARROW || columnMode === COLUMN_MODE_POINTS)
419
    ) {
420
      return this.calculateDataAttributeForGeoArrow({dataContainer, filteredIndex}, getPosition);
421
    }
75!
422

75!
423
    // we don't need these in non-Arrow modes atm.
424
    this.geoArrowVector0 = undefined;
UNCOV
425
    this.geoArrowVector1 = undefined;
×
426
    this.filteredIndex = null;
427

428
    if (this.config.columnMode === COLUMN_MODE_POINTS) {
429
      return this.calculateDataAttributeForPoints({dataContainer, filteredIndex}, getPosition);
75✔
430
    }
75✔
431
    return this.calculateDataAttributeForPointNNeighbors(
75✔
432
      {dataContainer, filteredIndex},
433
      getPosition
75✔
434
    );
71✔
435
  }
436

4✔
437
  formatLayerData(datasets, oldLayerData) {
438
    if (this.config.dataId === null) {
439
      return {};
440
    }
441
    const {gpuFilter, dataContainer} = datasets[this.config.dataId];
442
    const {data} = this.updateData(datasets, oldLayerData);
443
    const accessors = this.getAttributeAccessors({dataContainer});
87!
UNCOV
444
    const isFilteredAccessor = (data: {index: number}) => {
×
445
      // for GeoArrow data is a buffer, so use objectInfo
446
      return this.filteredIndex ? this.filteredIndex[data.index] : 1;
87✔
447
    };
87✔
448

87✔
449
    return {
87✔
450
      data,
UNCOV
451
      getFilterValue: gpuFilter.filterValueAccessor(dataContainer)(),
×
452
      getFiltered: isFilteredAccessor,
453
      ...accessors
454
    };
87✔
455
  }
456
  /* eslint-enable complexity */
457

458
  updateLayerMeta(dataContainer) {
459
    this.dataContainer = dataContainer;
460

461
    // get bounds from arcs
462
    const getPosition = this.getPositionAccessor(dataContainer);
463

464
    const sBounds = this.getPointsBounds(dataContainer, d => {
75✔
465
      const pos = getPosition(d);
466
      return [pos[0], pos[1]];
467
    });
75✔
468

469
    let tBounds: number[] | null = [];
75✔
470
    if (this.config.columnMode === COLUMN_MODE_POINTS) {
551✔
471
      tBounds = this.getPointsBounds(dataContainer, d => {
551✔
472
        const pos = getPosition(d);
473
        return [pos[3], pos[4]];
474
      });
75✔
475
    } else {
75✔
476
      // when columnMode is neighbors, it reference the same collection of points
71✔
477
      tBounds = sBounds;
471✔
478
    }
471✔
479

480
    const bounds =
481
      tBounds && sBounds
482
        ? [
4✔
483
            Math.min(sBounds[0], tBounds[0]),
484
            Math.min(sBounds[1], tBounds[1]),
485
            Math.max(sBounds[2], tBounds[2]),
486
            Math.max(sBounds[3], tBounds[3])
75!
487
          ]
488
        : sBounds || tBounds;
489

490
    this.updateMeta({bounds});
491
  }
492

493
  renderLayer(opts) {
×
494
    const {data, gpuFilter, objectHovered, interactionConfig, dataset} = opts;
495
    const updateTriggers = {
75✔
496
      getPosition: this.config.columns,
497
      getFilterValue: gpuFilter.filterValueUpdateTriggers,
498
      getFiltered: this.filteredIndexTrigger,
499
      ...this.getVisualChannelUpdateTriggers()
2✔
500
    };
2✔
501
    const widthScale = this.config.visConfig.thickness * PROJECTED_PIXEL_SIZE_MULTIPLIER;
502
    const defaultLayerProps = this.getDefaultDeckLayerProps(opts);
503
    const hoveredObject = this.hasHoveredObject(objectHovered);
504

505
    const useArrowLayer = Boolean(this.geoArrowVector0);
506

2✔
507
    let ArcLayerClass: typeof DeckArcLayer | typeof GeoArrowArcLayer = DeckArcLayer;
2✔
508
    let experimentalPropOverrides: {
2✔
509
      data?: arrow.Table;
510
      getSourcePosition?: arrow.Vector;
2✔
511
      getTargetPosition?: arrow.Vector;
512
    } = {};
2✔
513

514
    if (useArrowLayer) {
515
      ArcLayerClass = GeoArrowArcLayer;
516
      experimentalPropOverrides = {
517
        data: dataset.dataContainer.getTable(),
2✔
518
        getSourcePosition: this.geoArrowVector0,
519
        getTargetPosition: this.geoArrowVector1
2!
UNCOV
520
      };
×
UNCOV
521
    }
×
522

523
    return [
524
      // @ts-expect-error
525
      new ArcLayerClass({
526
        ...defaultLayerProps,
527
        ...this.getBrushingExtensionProps(interactionConfig, 'source_target'),
528
        ...data,
2✔
529
        ...experimentalPropOverrides,
530
        widthScale,
531
        updateTriggers,
532
        extensions: [
533
          ...defaultLayerProps.extensions,
534
          brushingExtension,
535
          ...(useArrowLayer ? [arrowCPUFilterExtension] : [])
536
        ]
537
      }),
538
      // hover layer
539
      ...(hoveredObject
540
        ? [
2!
541
            new DeckArcLayer({
542
              ...this.getDefaultHoverLayerProps(),
543
              visible: defaultLayerProps.visible,
544
              data: [hoveredObject],
2!
545
              widthScale,
546
              getSourceColor: this.config.highlightColor,
547
              getTargetColor: this.config.highlightColor,
548
              getWidth: data.getWidth
549
            })
550
          ]
551
        : [])
552
    ];
553
  }
554

555
  hasHoveredObject(objectInfo: {index: number}) {
556
    if (
557
      isLayerHoveredFromArrow(objectInfo, this.id) &&
558
      objectInfo.index >= 0 &&
559
      this.dataContainer
560
    ) {
561
      return {
4!
562
        index: objectInfo.index,
4!
563
        position: this.getPositionAccessor(this.dataContainer)(objectInfo)
564
      };
565
    }
UNCOV
566

×
567
    return super.hasHoveredObject(objectInfo);
568
  }
569

570
  getHoverData(
571
    object: {index: number} | arrow.StructRow | undefined,
572
    dataContainer: DataContainerInterface,
4✔
573
    fields: Field[],
574
    animationConfig: AnimationConfig,
575
    hoverInfo: {index: number}
576
  ) {
577
    // for arrow format, `object` is the Arrow row object Proxy,
578
    // and index is passed in `hoverInfo`.
579
    const index = Boolean(this.geoArrowVector0)
580
      ? hoverInfo?.index
581
      : (object as {index: number}).index;
582
    if (index >= 0) {
583
      return dataContainer.row(index);
UNCOV
584
    }
×
UNCOV
585
    return null;
×
UNCOV
586
  }
×
587
}
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