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

keplergl / kepler.gl / 25119824328

29 Apr 2026 04:03PM UTC coverage: 59.44%. Remained the same
25119824328

Pull #3393

github

web-flow
Merge 546981007 into 62056d35b
Pull Request #3393: chore: upgrade react-map-gl to 8, maplibre-gl to 4

6898 of 13934 branches covered (49.5%)

Branch coverage included in aggregate %.

1 of 5 new or added lines in 3 files covered. (20.0%)

11 existing lines in 2 files now uncovered.

14243 of 21633 relevant lines covered (65.84%)

80.35 hits per line

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

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

4
import {
5
  ClusterLevel,
6
  clusterLocations,
7
  LocalFlowmapDataProvider,
8
  makeLocationWeightGetter
9
} from '@flowmap.gl/data';
10
import {FlowmapLayer, PickingType} from '@flowmap.gl/layers';
11
import {format as d3Format} from 'd3-format';
12

13
import {TOOLTIP_FORMATS, LAYER_VIS_CONFIGS} from '@kepler.gl/constants';
14
import {DataContainerInterface, maybeHexToGeo, getPositionFromHexValue} from '@kepler.gl/utils';
15
import {Datasets, KeplerTable} from '@kepler.gl/table';
16
import {
17
  ColumnLabels,
18
  ColumnPairs,
19
  SupportedColumnMode,
20
  VisConfigBoolean,
21
  VisConfigNumber,
22
  VisConfigSelection,
23
  MapState
24
} from '@kepler.gl/types';
25

26
import Layer, {LayerBaseConfig, LayerBaseConfigPartial} from '../base-layer';
27
import {getFilterDataFunc} from '../aggregation-layer';
28
import {diffUpdateTriggers} from '../layer-update';
29
import {FindDefaultLayerPropsReturnValue} from '../layer-utils';
30
import FlowLayerIcon from './flow-layer-icon';
31

32
const MAX_CLUSTER_ZOOM_LEVEL = 20;
13✔
33

34
export type LocationDatum = {
35
  id: number;
36
  name: string;
37
  lon: number;
38
  lat: number;
39
};
40

41
export type FlowDatum = {
42
  index: number;
43
  sourceId: number;
44
  targetId: number;
45
  count: number;
46
};
47

48
export type FlowLayerData = {
49
  locations: LocationDatum[];
50
  flows: FlowDatum[];
51
  clusterLevels: ClusterLevel[];
52
};
53

54
const flowmapDataAccessors = {
13✔
55
  getLocationId: (loc: LocationDatum) => loc.id,
366✔
56
  getLocationLon: (loc: LocationDatum) => loc.lon,
246✔
57
  getLocationLat: (loc: LocationDatum) => loc.lat,
246✔
58
  getLocationName: (loc: LocationDatum) => loc.name,
×
59
  getFlowOriginId: (flow: FlowDatum) => flow.sourceId,
×
60
  getFlowDestId: (flow: FlowDatum) => flow.targetId,
×
61
  getFlowMagnitude: (flow: FlowDatum) => flow.count
×
62
};
63

64
export enum FlowLayerColumnMode {
65
  LAT_LNG = 'LAT_LNG',
66
  H3 = 'H3'
67
}
68

69
const flowPosAccessor =
70
  (
13✔
71
    {lat0, lng0, lat1, lng1, sourceH3, targetH3}: Record<string, any>,
72
    columnMode?: FlowLayerColumnMode | null
73
  ) =>
74
  (dataContainer: DataContainerInterface): ((d: {index: number}) => number[]) => {
177✔
75
    if (columnMode === FlowLayerColumnMode.H3) {
177!
76
      return (d: {index: number}): number[] => {
×
77
        const startPos = getPositionFromHexValue(dataContainer.valueAt(d.index, sourceH3.fieldIdx));
×
78
        const endPos = getPositionFromHexValue(dataContainer.valueAt(d.index, targetH3.fieldIdx));
×
79
        return [
×
80
          startPos?.[0] ?? Number.NaN,
×
81
          startPos?.[1] ?? Number.NaN,
×
82
          0,
83
          endPos?.[0] ?? Number.NaN,
×
84
          endPos?.[1] ?? Number.NaN,
×
85
          0
86
        ];
87
      };
88
    }
89
    return (d: {index: number}): number[] => {
177✔
90
      const startPos = maybeHexToGeo(dataContainer, d, lat0, lng0);
183✔
91
      const endPos = maybeHexToGeo(dataContainer, d, lat1, lng1);
183✔
92
      return [
183✔
93
        startPos ? startPos[0] : dataContainer.valueAt(d.index, lng0.fieldIdx),
183!
94
        startPos ? startPos[1] : dataContainer.valueAt(d.index, lat0.fieldIdx),
183!
95
        0,
96
        endPos ? endPos[0] : dataContainer.valueAt(d.index, lng1.fieldIdx),
183!
97
        endPos ? endPos[1] : dataContainer.valueAt(d.index, lat1.fieldIdx),
183!
98
        0
99
      ];
100
    };
101
  };
102

103
const COLUMN_LABELS: ColumnLabels = {
13✔
104
  lat0: 'flow.source.lat',
105
  lng0: 'flow.source.lng',
106
  lat1: 'flow.target.lat',
107
  lng1: 'flow.target.lng',
108
  sourceName: 'flow.source.name',
109
  targetName: 'flow.target.name',
110
  count: 'flow.count',
111
  sourceH3: 'flow.source.h3',
112
  targetH3: 'flow.target.h3'
113
};
114

115
const SUPPORTED_COLUMN_MODES: SupportedColumnMode[] = [
13✔
116
  {
117
    key: FlowLayerColumnMode.LAT_LNG,
118
    label: 'Lat/Lng',
119
    requiredColumns: ['lat0', 'lng0', 'lat1', 'lng1'],
120
    optionalColumns: ['count', 'sourceName', 'targetName']
121
  },
122
  {
123
    key: FlowLayerColumnMode.H3,
124
    label: 'H3 hexagons',
125
    requiredColumns: ['sourceH3', 'targetH3'],
126
    optionalColumns: ['count', 'sourceName', 'targetName']
127
  }
128
];
129

130
const DEFAULT_COLUMN_MODE = FlowLayerColumnMode.LAT_LNG;
13✔
131

132
export const FLOW_LINES_RENDERING_MODES = ['straight', 'curved', 'animated-straight'] as const;
13✔
133
export type FlowLinesRenderingMode = (typeof FLOW_LINES_RENDERING_MODES)[number];
134

135
export const flowVisConfigs = {
13✔
136
  colorRange: 'colorRange' as const,
137
  opacity: {
138
    ...LAYER_VIS_CONFIGS.opacity,
139
    defaultValue: 1.0
140
  },
141
  flowLinesRenderingMode: {
142
    defaultValue: 'straight',
143
    type: 'select',
144
    label: 'layerVisConfigs.flow.renderingMode',
145
    property: 'flowLinesRenderingMode',
146
    options: FLOW_LINES_RENDERING_MODES as unknown as string[]
147
  } as VisConfigSelection,
148
  flowAdaptiveScalesEnabled: {
149
    defaultValue: true,
150
    type: 'boolean',
151
    label: 'layerVisConfigs.flow.adaptiveScalesEnabled',
152
    property: 'flowAdaptiveScalesEnabled'
153
  } as VisConfigBoolean,
154
  flowFadeEnabled: {
155
    defaultValue: true,
156
    type: 'boolean',
157
    label: 'layerVisConfigs.flow.fadeEnabled',
158
    property: 'flowFadeEnabled'
159
  } as VisConfigBoolean,
160
  flowFadeAmount: {
161
    defaultValue: 50,
162
    type: 'number',
163
    label: 'layerVisConfigs.flow.fadeAmount',
164
    property: 'flowFadeAmount',
165
    isRanged: false,
166
    range: [0, 100],
167
    step: 1.0
168
  } as VisConfigNumber,
169
  maxTopFlowsDisplayNum: {
170
    defaultValue: 5000,
171
    type: 'number',
172
    label: 'layerVisConfigs.flow.maxTopFlowsDisplayNum',
173
    property: 'maxTopFlowsDisplayNum',
174
    isRanged: false,
175
    range: [0, 10000],
176
    step: 1
177
  } as VisConfigNumber,
178
  flowLocationTotalsEnabled: {
179
    defaultValue: true,
180
    type: 'boolean',
181
    label: 'layerVisConfigs.flow.locationTotalsEnabled',
182
    property: 'flowLocationTotalsEnabled'
183
  } as VisConfigBoolean,
184
  flowClusteringEnabled: {
185
    defaultValue: true,
186
    type: 'boolean',
187
    label: 'layerVisConfigs.flow.clusteringEnabled',
188
    property: 'flowClusteringEnabled'
189
  } as VisConfigBoolean,
190
  flowLineThicknessScale: {
191
    defaultValue: 1,
192
    type: 'number',
193
    label: 'layerVisConfigs.flow.lineThicknessScale',
194
    property: 'flowLineThicknessScale',
195
    isRanged: false,
196
    range: [0.1, 5],
197
    step: 0.1
198
  } as VisConfigNumber,
199
  flowLineCurviness: {
200
    defaultValue: 1,
201
    type: 'number',
202
    label: 'layerVisConfigs.flow.lineCurviness',
203
    property: 'flowLineCurviness',
204
    isRanged: false,
205
    range: [0, 2],
206
    step: 0.1
207
  } as VisConfigNumber,
208
  darkBaseMapEnabled: 'darkBaseMapEnabled' as const
209
};
210

211
type Props = ConstructorParameters<typeof Layer>[0];
212

213
export default class FlowLayer extends Layer {
214
  _locationsByLatLon: Record<string, LocationDatum> | null = null;
35✔
215
  _dataProvider: LocalFlowmapDataProvider<LocationDatum, FlowDatum>;
216
  _lastLayerData: FlowLayerData | null = null;
35✔
217
  _dataVersion = 0;
35✔
218

219
  constructor(props: Props) {
220
    super(props);
35✔
221
    this.registerVisConfig(flowVisConfigs);
35✔
222
    this._dataProvider = new LocalFlowmapDataProvider<LocationDatum, FlowDatum>(
35✔
223
      flowmapDataAccessors
224
    );
225
  }
226

227
  get type(): string {
228
    return 'flow';
30✔
229
  }
230

231
  get isAggregated(): boolean {
232
    return true;
1✔
233
  }
234

235
  get layerIcon() {
236
    return FlowLayerIcon;
28✔
237
  }
238

239
  get columnLabels(): ColumnLabels {
240
    return COLUMN_LABELS;
×
241
  }
242

243
  get columnPairs(): ColumnPairs {
244
    return this.defaultLinkColumnPairs;
1✔
245
  }
246

247
  get supportedColumnModes(): SupportedColumnMode[] {
248
    return SUPPORTED_COLUMN_MODES;
71✔
249
  }
250

251
  static findDefaultLayerProps(): FindDefaultLayerPropsReturnValue {
252
    return {props: []};
94✔
253
  }
254

255
  getDefaultLayerConfig(config: {[key: string]: any}): LayerBaseConfig {
256
    const defaultLayerConfig = super.getDefaultLayerConfig(config as LayerBaseConfigPartial);
35✔
257

258
    return {
35✔
259
      ...defaultLayerConfig,
260
      columnMode: config?.columnMode ?? DEFAULT_COLUMN_MODE
70✔
261
    };
262
  }
263

264
  /**
265
   * Migrate legacy flowAnimationEnabled/flowCurvedLinesEnabled booleans
266
   * to the new flowLinesRenderingMode select.
267
   */
268
  updateLayerConfig(newConfig: any): FlowLayer {
269
    if (newConfig?.visConfig) {
4!
270
      const {flowAnimationEnabled, flowCurvedLinesEnabled, flowLinesRenderingMode, ...rest} =
271
        newConfig.visConfig;
×
NEW
272
      if (
×
273
        flowLinesRenderingMode === undefined &&
×
274
        (flowAnimationEnabled || flowCurvedLinesEnabled)
275
      ) {
UNCOV
276
        newConfig = {
×
277
          ...newConfig,
278
          visConfig: {
279
            ...rest,
280
            flowLinesRenderingMode: flowCurvedLinesEnabled
×
281
              ? 'curved'
282
              : flowAnimationEnabled
×
283
              ? 'animated-straight'
284
              : 'straight'
285
          }
286
        };
287
      }
288
    }
289
    super.updateLayerConfig(newConfig);
4✔
290
    return this;
4✔
291
  }
292

293
  getDataProvider(): LocalFlowmapDataProvider<LocationDatum, FlowDatum> {
UNCOV
294
    return this._dataProvider;
×
295
  }
296

297
  getPositionAccessor = (dataContainer: DataContainerInterface): ((d: any) => number[]) =>
35✔
298
    flowPosAccessor(
177✔
299
      this.config.columns,
300
      this.config.columnMode
177!
301
        ? FlowLayerColumnMode[this.config.columnMode as keyof typeof FlowLayerColumnMode]
302
        : null
303
    )(dataContainer);
304

305
  private getSourcePosition = (dataContainer: DataContainerInterface) => (d: any) =>
35✔
306
    this.getPositionAccessor(dataContainer)(d).slice(0, 2);
88✔
307

308
  private getTargetPosition = (dataContainer: DataContainerInterface) => (d: any) =>
35✔
309
    this.getPositionAccessor(dataContainer)(d).slice(3, 5);
88✔
310

311
  private static getLatLonKey = ([lon, lat]: number[]) => `${lat}:${lon}`;
68✔
312

313
  private getLocationFromPosition = (pos: number[]) =>
35✔
314
    this._locationsByLatLon?.[FlowLayer.getLatLonKey(pos)];
68✔
315

316
  private getMagnitude = (dataContainer: DataContainerInterface) => (rowIndex: number) => {
35✔
317
    const fieldIdx = this.config.columns.count?.fieldIdx;
34✔
318
    return fieldIdx != null && fieldIdx >= 0 ? dataContainer.valueAt(rowIndex, fieldIdx) : 1;
34!
319
  };
320

321
  private getSourceName = (dataContainer: DataContainerInterface, d: {index: number}) => {
35✔
322
    const {fieldIdx} = this.config.columns.sourceName ?? {};
27✔
323
    return fieldIdx != null && fieldIdx >= 0 ? dataContainer.valueAt(d.index, fieldIdx) : null;
27!
324
  };
325

326
  private getTargetName = (dataContainer: DataContainerInterface, d: {index: number}) => {
35✔
327
    const {fieldIdx} = this.config.columns.targetName ?? {};
27✔
328
    return fieldIdx != null && fieldIdx >= 0 ? dataContainer.valueAt(d.index, fieldIdx) : null;
27!
329
  };
330

331
  updateLayerMeta(dataset: KeplerTable): void {
332
    const {dataContainer} = dataset;
3✔
333

334
    const sourceBounds = this.getPointsBounds(dataContainer, this.getSourcePosition(dataContainer));
3✔
335
    const targetBounds = this.getPointsBounds(dataContainer, this.getTargetPosition(dataContainer));
3✔
336

337
    const bounds =
338
      targetBounds && sourceBounds
3!
339
        ? [
340
            Math.min(sourceBounds[0], targetBounds[0]),
341
            Math.min(sourceBounds[1], targetBounds[1]),
342
            Math.max(sourceBounds[2], targetBounds[2]),
343
            Math.max(sourceBounds[3], targetBounds[3])
344
          ]
345
        : sourceBounds || targetBounds;
×
346

347
    this._locationsByLatLon = {};
3✔
348
    let nextId = 1;
3✔
349
    const maybeAddLocation = ([lon, lat]: number[], name: string | null) => {
3✔
350
      if (!this._locationsByLatLon) return;
54!
351
      if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;
54✔
352
      const latLon = `${lat}:${lon}`;
42✔
353
      const loc = this._locationsByLatLon[latLon];
42✔
354
      if (!loc) {
42!
355
        const id = nextId++;
42✔
356
        this._locationsByLatLon[latLon] = {
42✔
357
          id,
358
          name: name ?? `Location#${id}`,
84✔
359
          lat,
360
          lon
361
        };
362
      }
363
    };
364

365
    const getSource = this.getSourcePosition(dataContainer);
3✔
366
    const getTarget = this.getTargetPosition(dataContainer);
3✔
367

368
    const numRows = dataContainer.numRows();
3✔
369
    for (let i = 0; i < numRows; ++i) {
3✔
370
      const datum = {index: i};
27✔
371
      const sourcePos = getSource(datum);
27✔
372
      const targetPos = getTarget(datum);
27✔
373
      maybeAddLocation(sourcePos, this.getSourceName(dataContainer, datum));
27✔
374
      maybeAddLocation(targetPos, this.getTargetName(dataContainer, datum));
27✔
375
    }
376

377
    const locations = Object.values(this._locationsByLatLon);
3✔
378

379
    const getFlowOriginId = (index: number) =>
3✔
380
      this.getLocationFromPosition(getSource({index}))?.id || 0;
27✔
381
    const getFlowDestId = (index: number) =>
3✔
382
      this.getLocationFromPosition(getTarget({index}))?.id || 0;
27✔
383

384
    const flowIndices = dataContainer.getPlainIndex();
3✔
385
    const getLocationWeight = makeLocationWeightGetter(flowIndices, {
3✔
386
      getFlowOriginId,
387
      getFlowDestId,
388
      getFlowMagnitude: this.getMagnitude(dataContainer)
389
    });
390

391
    const clusterLevels = clusterLocations(locations, flowmapDataAccessors, getLocationWeight, {
3✔
392
      maxZoom: MAX_CLUSTER_ZOOM_LEVEL
393
    });
394

395
    this.updateMeta({bounds, locations, clusterLevels});
3✔
396
  }
397

398
  calculateDataAttribute(
399
    {dataContainer, filteredIndex}: KeplerTable,
400
    getPosition: (d: any) => number[]
401
  ): FlowDatum[] {
402
    const data: FlowDatum[] = [];
1✔
403
    const datum = {index: 0};
1✔
404
    const getSource = this.getSourcePosition(dataContainer);
1✔
405
    const getTarget = this.getTargetPosition(dataContainer);
1✔
406
    const getMag = this.getMagnitude(dataContainer);
1✔
407
    for (let i = 0; i < filteredIndex.length; i++) {
1✔
408
      const index = filteredIndex[i];
7✔
409
      datum.index = index;
7✔
410
      const pos = getPosition(datum);
7✔
411

412
      if (pos.every(Number.isFinite)) {
7!
413
        const source = this.getLocationFromPosition(getSource(datum));
7✔
414
        const target = this.getLocationFromPosition(getTarget(datum));
7✔
415
        if (source && target) {
7!
416
          data.push({
7✔
417
            index,
418
            sourceId: source.id,
419
            targetId: target.id,
420
            count: getMag(datum.index)
421
          });
422
        }
423
      }
424
    }
425

426
    return data;
1✔
427
  }
428

429
  private _oldFilterUpdateTriggers: Record<string, any> | null = null;
35✔
430

431
  formatLayerData(datasets: Datasets, oldLayerData: Record<string, any>): Record<string, any> {
432
    const {dataId} = this.config;
1✔
433
    if (!dataId) {
1!
UNCOV
434
      return {};
×
435
    }
436
    const dataset = datasets[dataId];
1✔
437
    const {gpuFilter, dataContainer} = dataset;
1✔
438

439
    const {data, triggerChanged} = this.updateData(datasets, oldLayerData) as {
1✔
440
      data?: FlowDatum[];
441
      triggerChanged?: boolean | Record<string, any>;
442
    };
443
    const accessors = this.getAttributeAccessors({dataContainer});
1✔
444
    const {filterRange} = gpuFilter;
1✔
445
    const hasFilter = Object.values(filterRange).some(arr => (arr as number[]).some(v => v !== 0));
1✔
446
    let resultingFlows = data || [];
1!
447
    if (hasFilter) {
1!
448
      const filterUpdateTriggers = {
1✔
449
        filterRange: gpuFilter.filterRange,
450
        ...gpuFilter.filterValueUpdateTriggers
451
      };
452
      const dataChanged =
453
        triggerChanged && typeof triggerChanged === 'object' && triggerChanged.getData;
1✔
454
      const filterChanged =
455
        this._oldFilterUpdateTriggers === null ||
1!
456
        diffUpdateTriggers(filterUpdateTriggers, this._oldFilterUpdateTriggers);
457
      if (filterChanged || dataChanged) {
1!
458
        const indexAccessor = (d: FlowDatum) => d.index;
7✔
459
        const valueAccessor = (
1✔
460
          dc: DataContainerInterface,
461
          d: {index: number},
462
          fieldIndex: number
463
        ) => dc.valueAt(d.index, fieldIndex);
7✔
464
        const getFilterValue = gpuFilter.filterValueAccessor(dataContainer)(
1✔
465
          indexAccessor,
466
          valueAccessor
467
        );
468
        resultingFlows = resultingFlows.filter(getFilterDataFunc(filterRange, getFilterValue));
1✔
469
      }
470
      this._oldFilterUpdateTriggers = filterUpdateTriggers;
1✔
471
    }
472

473
    const layerData: FlowLayerData = {
1✔
474
      locations: this.meta.locations,
475
      clusterLevels: this.meta.clusterLevels,
476
      flows: resultingFlows
477
    };
478

479
    return {
1✔
480
      data,
481
      layerData,
482
      ...accessors
483
    };
484
  }
485

486
  renderLayer(opts: {
487
    data: any;
488
    gpuFilter: any;
489
    objectHovered: any;
490
    mapState: MapState;
491
    layerCallbacks: any;
492
    idx: number;
493
    visible: boolean;
494
  }): any[] {
495
    const {
496
      layerCallbacks,
497
      data: {layerData}
498
    } = opts;
1✔
499

500
    if (!layerData) {
1!
501
      return [];
1✔
502
    }
503

504
    const dataContentChanged =
UNCOV
505
      !this._lastLayerData ||
×
506
      layerData.locations !== this._lastLayerData.locations ||
507
      layerData.flows !== this._lastLayerData.flows ||
508
      layerData.clusterLevels !== this._lastLayerData.clusterLevels;
UNCOV
509
    if (dataContentChanged) {
×
UNCOV
510
      this._dataProvider.setFlowmapData(layerData);
×
UNCOV
511
      this._lastLayerData = layerData;
×
512
      this._dataVersion++;
×
513
    }
514
    const defaultLayerProps = this.getDefaultDeckLayerProps(opts);
×
515

516
    // FlowmapLayer doesn't support GPU filtering
517
    const {
518
      extensions: _extensions,
519
      filterRange: _filterRange,
520
      onFilteredItemsChange: _onFilteredItemsChange,
521
      ...cleanProps
NEW
522
    } = defaultLayerProps as any;
×
523

UNCOV
524
    const {visConfig} = this.config;
×
UNCOV
525
    const flowLinesRenderingMode = visConfig.flowLinesRenderingMode || 'straight';
×
UNCOV
526
    return [
×
527
      new FlowmapLayer<LocationDatum, FlowDatum>({
528
        ...cleanProps,
529
        dataProvider: this._dataProvider,
530
        data: this._dataVersion as any,
531
        ...flowmapDataAccessors,
532
        pickable: true,
533
        darkMode: visConfig.darkBaseMapEnabled,
534
        colorScheme: visConfig.colorRange?.colors,
535
        fadeAmount: visConfig.flowFadeAmount,
536
        fadeEnabled: visConfig.flowFadeEnabled,
537
        fadeOpacityEnabled: false,
538
        opacity: visConfig.opacity,
539
        locationTotalsEnabled: visConfig.flowLocationTotalsEnabled,
540
        flowLinesRenderingMode,
541
        flowLineThicknessScale: visConfig.flowLineThicknessScale,
542
        flowLineCurviness: visConfig.flowLineCurviness,
543
        clusteringEnabled: visConfig.flowClusteringEnabled,
544
        maxTopFlowsDisplayNum: visConfig.maxTopFlowsDisplayNum,
545
        clusteringAuto: true,
546
        adaptiveScalesEnabled: visConfig.flowAdaptiveScalesEnabled,
547
        onHover: layerCallbacks.onLayerHover,
548
        parameters: {
549
          ...(cleanProps.parameters || {}),
×
550
          cull: false
551
        }
552
      })
553
    ];
554
  }
555

556
  getHoverData(object: Record<string, any>): {
557
    object: Record<string, any>;
558
    fieldValues: Array<{labelMessage: string; value: any}>;
559
  } | null {
560
    const fmt = d3Format(TOOLTIP_FORMATS.DECIMAL_COMMA.format);
4✔
561
    switch (object?.type) {
4✔
562
      case PickingType.LOCATION:
563
        return {
1✔
564
          object,
565
          fieldValues: [
566
            {
567
              labelMessage: 'flow.tooltip.location.name',
568
              value: object.location.name
569
            },
570
            {
571
              labelMessage: 'flow.tooltip.location.incomingCount',
572
              value: fmt(object.totals.incomingCount)
573
            },
574
            {
575
              labelMessage: 'flow.tooltip.location.outgoingCount',
576
              value: fmt(object.totals.outgoingCount)
577
            },
578
            {
579
              labelMessage: 'flow.tooltip.location.internalCount',
580
              value: fmt(object.totals.internalCount)
581
            }
582
          ]
583
        };
584
      case PickingType.FLOW:
585
        return {
1✔
586
          object,
587
          fieldValues: [
588
            {
589
              labelMessage: 'flow.tooltip.flow.sourceName',
590
              value: object.origin.name
591
            },
592
            {
593
              labelMessage: 'flow.tooltip.flow.targetName',
594
              value: object.dest.name
595
            },
596
            {
597
              labelMessage: 'flow.tooltip.flow.count',
598
              value: fmt(object.count)
599
            }
600
          ]
601
        };
602
      default:
603
        return null;
2✔
604
    }
605
  }
606
}
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