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

keplergl / kepler.gl / 12482165950

24 Dec 2024 01:12PM UTC coverage: 67.451% (-0.1%) from 67.586%
12482165950

push

github

web-flow
[chore] 3.1.0-alpha.2 release (#2855)

- bump version for release
- temporarily disable vector tile layer plumbing
- ts fixes required for npm-publish
- prepublish to prepublishOnly script

5810 of 10000 branches covered (58.1%)

Branch coverage included in aggregate %.

4 of 9 new or added lines in 7 files covered. (44.44%)

29 existing lines in 4 files now uncovered.

11954 of 16336 relevant lines covered (73.18%)

86.83 hits per line

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

1.81
/src/layers/src/vector-tile/abstract-tile-layer.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import throttle from 'lodash.throttle';
5

6
import {notNullorUndefined} from '@kepler.gl/common-utils';
7
import {SCALE_TYPES, ALL_FIELD_TYPES, LAYER_VIS_CONFIGS} from '@kepler.gl/constants';
8
import {
9
  KeplerTable as KeplerDataset,
10
  Datasets as KeplerDatasets,
11
  GpuFilter
12
} from '@kepler.gl/table';
13
import {
14
  AnimationConfig,
15
  Field,
16
  BindedLayerCallbacks,
17
  VisConfigBoolean,
18
  VisConfigRange,
19
  VisConfigColorRange,
20
  VisConfigNumber,
21
  VisConfigColorSelect,
22
  LayerColorConfig,
23
  LayerHeightConfig,
24
  Filter,
25
  Field as KeplerField,
26
  MapState,
27
  Merge,
28
  ZoomStopsConfig
29
} from '@kepler.gl/types';
30
import {findDefaultColorField, DataContainerInterface} from '@kepler.gl/utils';
31

32
import {
33
  default as KeplerLayer,
34
  LayerBaseConfig,
35
  LayerBaseConfigPartial,
36
  VisualChannel,
37
  VisualChannelDomain,
38
  VisualChannelField
39
} from '../base-layer';
40
import {isTileDataset} from './utils/vector-tile-utils';
41
import TileDataset from './common-tile/tile-dataset';
42
import {isIndexedField, isDomainQuantiles} from './common-tile/tile-utils';
43

44
const DEFAULT_ELEVATION = 500;
11✔
45
export const DEFAULT_RADIUS = 1;
11✔
46

47
export type AbstractTileLayerVisConfigSettings = {
48
  strokeColor: VisConfigColorSelect;
49
  strokeOpacity: VisConfigNumber;
50
  radius: VisConfigRange;
51
  enable3d: VisConfigBoolean;
52
  stroked: VisConfigBoolean;
53
  transition: VisConfigBoolean;
54
  heightRange: VisConfigRange;
55
  elevationScale: VisConfigNumber;
56
  opacity: VisConfigNumber;
57
  colorRange: VisConfigColorRange;
58
  // TODO: figure out type for radiusByZoom vis config
59
  radiusByZoom: any;
60
  dynamicColor: VisConfigBoolean;
61
};
62

63
export type LayerData = {
64
  minZoom?: number;
65
  maxZoom?: number;
66
  getPointRadius?: () => number;
67
};
68

69
export type LayerOpts = {
70
  idx: number;
71
  mapState: MapState;
72
  data: LayerData;
73
  gpuFilter: GpuFilter;
74
  animationConfig: AnimationConfig;
75
  tilesetDataUrl?: string | null;
76
  layerCallbacks: BindedLayerCallbacks;
77
};
78

79
export const commonTileVisConfigs = {
11✔
80
  strokeColor: 'strokeColor' as const,
81
  strokeOpacity: {
82
    ...LAYER_VIS_CONFIGS.opacity,
83
    property: 'strokeOpacity'
84
  },
85
  radius: {
86
    ...LAYER_VIS_CONFIGS.radius,
87
    isRanged: true,
88
    range: [0, 1000],
89
    step: 0.1,
90
    defaultValue: [50, 50],
91
    allowCustomValue: false
92
  } as VisConfigRange,
93
  enable3d: 'enable3d' as const,
94
  stroked: {
95
    ...LAYER_VIS_CONFIGS.stroked,
96
    defaultValue: false
97
  },
98
  transition: {
99
    type: 'boolean',
100
    defaultValue: false,
101
    label: 'Transition',
102
    group: '',
103
    property: 'transition',
104
    description:
105
      'Smoother transition during animation (enable transition will decrease performance)'
106
  } as VisConfigBoolean,
107
  heightRange: 'elevationRange' as const,
108
  elevationScale: {...LAYER_VIS_CONFIGS.elevationScale, allowCustomValue: false},
109
  opacity: 'opacity' as const,
110
  colorRange: 'colorRange' as const,
111
  // TODO: figure out type for radiusByZoom vis config
112
  radiusByZoom: {
113
    ...LAYER_VIS_CONFIGS.radius,
114
    defaultValue: {
115
      enabled: false,
116
      stops: null
117
    } as ZoomStopsConfig
118
  } as any,
119
  dynamicColor: {
120
    type: 'boolean',
121
    defaultValue: false,
122
    label: 'Dynamic Color',
123
    group: '',
124
    property: 'dynamicColor',
125
    description: 'Use a dynamic color scale based on data visible in the viewport'
126
  } as VisConfigBoolean
127
};
128

129
export type AbstractTileLayerConfig = Merge<
130
  LayerBaseConfig,
131
  {colorField?: VisualChannelField; colorScale?: string; colorDomain?: VisualChannelDomain}
132
>;
133

134
/**
135
 * Abstract tile layer, including common functionality for viewport-based datasets,
136
 * dynamic scales, and tile-based animation
137
 */
138
export default abstract class AbstractTileLayer<
139
  // Type of the tile itself
140
  T,
141
  // Type of the iterable data object
142
  I extends Iterable<any> = T extends Iterable<any> ? T : never
143
> extends KeplerLayer {
144
  declare config: AbstractTileLayerConfig;
145
  declare visConfigSettings: AbstractTileLayerVisConfigSettings;
146

147
  constructor(props: ConstructorParameters<typeof KeplerLayer>[0]) {
UNCOV
148
    super(props);
×
UNCOV
149
    this.registerVisConfig(commonTileVisConfigs);
×
UNCOV
150
    this.tileDataset = this.initTileDataset();
×
151
  }
152

153
  protected tileDataset: TileDataset<T, I>;
UNCOV
154
  protected setLayerDomain: BindedLayerCallbacks['onSetLayerDomain'] = undefined;
×
155

156
  protected abstract initTileDataset(): TileDataset<T, I>;
157

158
  static findDefaultLayerProps(dataset: KeplerDataset): {
159
    props: {dataId: string; label?: string; isVisible: boolean}[];
160
  } {
161
    if (!isTileDataset(dataset)) {
×
162
      return {props: []};
×
163
    }
164

165
    const newLayerProp = {
×
166
      dataId: dataset.id,
167
      label: dataset.label,
168
      isVisible: true
169
    };
170

171
    return {props: [newLayerProp]};
×
172
  }
173

174
  get requireData(): boolean {
UNCOV
175
    return true;
×
176
  }
177

178
  get requiredLayerColumns(): string[] {
UNCOV
179
    return [];
×
180
  }
181

182
  get visualChannels(): Record<string, VisualChannel> {
183
    return {
×
184
      color: {
185
        ...super.visualChannels.color,
186
        accessor: 'getFillColor',
187
        defaultValue: config => config.color
×
188
      },
189
      height: {
190
        property: 'height',
191
        field: 'heightField',
192
        scale: 'heightScale',
193
        domain: 'heightDomain',
194
        range: 'heightRange',
195
        key: 'height',
196
        channelScaleType: 'size',
197
        accessor: 'getElevation',
198
        condition: config => config.visConfig.enable3d,
×
199
        nullValue: 0,
200
        defaultValue: DEFAULT_ELEVATION
201
      }
202
    };
203
  }
204

205
  /**
206
   * Callback to invoke when the viewport changes
207
   */
UNCOV
208
  onViewportLoad = (tiles: T[]): void => {
×
209
    this.tileDataset.updateTiles(tiles);
×
210
    // Update dynamic color domain if required
211
    if (this.config.visConfig.dynamicColor) {
×
212
      this.setDynamicColorDomain();
×
213
    }
214
  };
215

216
  abstract accessRowValue(
217
    field?: KeplerField,
218
    indexKey?: number | null
219
  ): (field: KeplerField, datum: T extends Iterable<infer V> ? V : never) => string | number | null;
220

221
  accessVSFieldValue(inputField?: Field, indexKey?: number | null): any {
222
    return this.accessRowValue(inputField, indexKey);
×
223
  }
224

225
  getScaleOptions(channelKey: string): string[] {
226
    if (channelKey === 'color') {
×
227
      const channel = this.visualChannels.color;
×
228
      const field = this.config[channel.field];
×
229

230
      if (
×
231
        isDomainQuantiles(field?.filterProps?.domainQuantiles) ||
×
232
        this.config.visConfig.dynamicColor ||
233
        // If we've set the scale to quantile, we need to include it - there's a loading
234
        // period in which the visConfig isn't set yet, but if we don't return the right
235
        // scale type we lose it
236
        this.config.colorScale === SCALE_TYPES.quantile
237
      ) {
238
        return [SCALE_TYPES.quantize, SCALE_TYPES.quantile, SCALE_TYPES.custom];
×
239
      }
240
      return [SCALE_TYPES.quantize, SCALE_TYPES.custom];
×
241
    }
242

243
    return [SCALE_TYPES.linear];
×
244
  }
245

UNCOV
246
  setDynamicColorDomain = throttle((): void => {
×
247
    const {config, tileDataset, setLayerDomain} = this;
×
248
    const field = config.colorField;
×
249
    const {colorDomain, colorScale} = config;
×
250
    if (!tileDataset || !setLayerDomain || !field) return;
×
251

252
    if (colorScale === SCALE_TYPES.quantize) {
×
253
      const [min, max] = tileDataset.getExtent(field);
×
254
      if (!Array.isArray(colorDomain) || min !== colorDomain[0] || max !== colorDomain[1]) {
×
255
        setLayerDomain({domain: [min, max]});
×
256
      }
257
    } else if (colorScale === SCALE_TYPES.quantile) {
×
258
      const domain = tileDataset.getQuantileSample(field);
×
259
      setLayerDomain({domain});
×
260
    } else if (colorScale === SCALE_TYPES.ordinal) {
×
261
      const domain = tileDataset.getUniqueValues(field);
×
262
      setLayerDomain({domain});
×
263
    }
264
  }, 500);
265

266
  resetColorDomain(): void {
267
    const {datasetId, datasets} = this.meta;
×
268
    this.updateLayerVisualChannel(datasets?.[datasetId || ''], 'color');
×
269
    this.setLayerDomain?.({domain: this.config.colorDomain});
×
270
  }
271

272
  updateLayerConfig(
273
    newConfig: Partial<LayerBaseConfig> & Partial<LayerColorConfig>
274
  ): AbstractTileLayer<T, I> {
275
    // When the dynamic color setting changes, we need to recalculate the layer domain
276
    const old = this.config.visConfig.dynamicColor ?? false;
×
277
    const next = newConfig.visConfig?.dynamicColor ?? old;
×
278
    const scaleTypeChanged =
279
      newConfig.colorScale && this.config.colorScale !== newConfig.colorScale;
×
280

281
    super.updateLayerConfig(newConfig);
×
282
    const {colorField} = this.config;
×
283

284
    if (colorField) {
×
285
      // When we switch from dynamic to non-dynamic or vice versa, we need to update
286
      // the color domain. This is downstream from a dispatch call, so we use
287
      // setTimeout to avoid "reducers may not dispatch actions" errors
288
      if (next && (!old || scaleTypeChanged)) {
×
289
        setTimeout(() => this.setDynamicColorDomain(), 0);
×
290
      } else if (old && !next) {
×
291
        setTimeout(() => this.resetColorDomain(), 0);
×
292
      }
293
    }
294

295
    return this;
×
296
  }
297

298
  get animationDomain(): [number, number] | null | undefined {
299
    return this.config.animation.domain;
×
300
  }
301

302
  setInitialLayerConfig(dataset: KeplerDataset): AbstractTileLayer<T, I> {
303
    const defaultColorField = findDefaultColorField(dataset);
×
304

305
    if (defaultColorField) {
×
306
      this.updateLayerConfig({
×
307
        colorField: defaultColorField
308
      });
309
      this.updateLayerVisualChannel(dataset, 'color');
×
310
    }
311

312
    return this;
×
313
  }
314

315
  getDefaultLayerConfig(
316
    props: LayerBaseConfigPartial
317
  ): LayerBaseConfig & Partial<LayerColorConfig & LayerHeightConfig> {
UNCOV
318
    return {
×
319
      ...super.getDefaultLayerConfig(props),
320
      colorScale: SCALE_TYPES.quantize,
321

322
      // add height visual channel
323
      heightField: null,
324
      heightDomain: [0, 1],
325
      heightScale: 'linear'
326
    };
327
  }
328

329
  // We can render without columns, so we redefine this method
330
  shouldRenderLayer(): boolean {
331
    return Boolean(this.type && this.config.isVisible);
×
332
  }
333

334
  updateAnimationDomainByField(channel: string): void {
335
    const field = this.config[this.visualChannels[channel].field];
×
336

337
    if (isIndexedField(field) && field.indexBy.type === ALL_FIELD_TYPES.timestamp) {
×
338
      const {timeDomain} = field.indexBy;
×
339

340
      this.updateLayerConfig({
×
341
        animation: {
342
          ...timeDomain,
343
          enabled: true,
344
          startTime: timeDomain.domain[0]
345
        }
346
      });
347
    }
348
  }
349

350
  updateAnimationDomain(domain: [number, number] | null | undefined): void {
351
    this.updateLayerConfig({
×
352
      animation: {
353
        ...this.config.animation,
354
        domain
355
      }
356
    });
357
  }
358

359
  updateLayerDomain(datasets: KeplerDatasets, newFilter?: Filter): AbstractTileLayer<T, I> {
360
    super.updateLayerDomain(datasets, newFilter);
×
361
    if (newFilter) {
×
362
      // invalidate cachedVisibleDataset when e.g. changing filters
363
      this.tileDataset.invalidateCache();
×
364
    }
365
    Object.keys(this.visualChannels).forEach(channel => {
×
366
      this.updateAnimationDomainByField(channel);
×
367
    });
368
    return this;
×
369
  }
370

371
  updateLayerVisualChannel(dataset: KeplerDataset, channel: string): void {
372
    super.updateLayerVisualChannel(dataset, channel);
×
373

374
    // create animation if field is indexed by time
375
    this.updateAnimationDomainByField(channel);
×
376
  }
377

378
  formatLayerData(
379
    datasets: KeplerDatasets,
380
    oldLayerData: unknown,
381
    animationConfig: AnimationConfig
382
  ): LayerData {
383
    const {dataId} = this.config;
×
384
    if (!notNullorUndefined(dataId)) {
×
385
      return {};
×
386
    }
387
    const dataset = datasets[dataId];
×
388

389
    const dataUpdateTriggers = this.getDataUpdateTriggers(dataset);
×
390
    const triggerChanged = this.getChangedTriggers(dataUpdateTriggers);
×
391

392
    if (triggerChanged && triggerChanged.getMeta) {
×
393
      this.updateLayerMeta(dataset, datasets);
×
394
    }
395

396
    const indexKey = this.config.animation.enabled ? animationConfig.currentTime : null;
×
397
    const accessors = this.getAttributeAccessors({
×
398
      dataAccessor: () => d => d,
×
399
      dataContainer: dataset.dataContainer,
400
      indexKey
401
    });
402

403
    const getPointRadius = () => DEFAULT_RADIUS;
×
404

405
    const metadata = dataset?.metadata as LayerData | undefined;
×
406

407
    return {
×
408
      ...(metadata ? {minZoom: metadata.minZoom} : {}),
×
409
      ...(metadata ? {maxZoom: metadata.maxZoom} : {}),
×
410
      ...accessors,
411
      getPointRadius
412
    };
413
  }
414

415
  getGpuFilterValueAccessor({gpuFilter, animationConfig}: LayerOpts): any {
416
    const indexKey = this.config.animation.enabled ? animationConfig.currentTime : null;
×
417
    const valueAccessor = (dataContainer: DataContainerInterface, d) => field =>
×
418
      this.accessVSFieldValue(field, indexKey)(field, d);
×
419
    return gpuFilter.filterValueAccessor(null as any)(undefined, valueAccessor);
×
420
  }
421
}
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