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

visgl / deck.gl / 10512776969

22 Aug 2024 05:25PM UTC coverage: 89.141% (-0.08%) from 89.221%
10512776969

push

github

web-flow
GPU Aggregation (5/8): GridLayer (#9096)

6656 of 7313 branches covered (91.02%)

Branch coverage included in aggregate %.

814 of 823 new or added lines in 10 files covered. (98.91%)

155 existing lines in 3 files now uncovered.

55618 of 62547 relevant lines covered (88.92%)

6487.64 hits per line

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

98.3
/modules/aggregation-layers/src/grid-layer/grid-layer.ts
1
// deck.gl
1✔
2
// SPDX-License-Identifier: MIT
1✔
3
// Copyright (c) vis.gl contributors
1✔
4

1✔
5
import {
1✔
6
  Accessor,
1✔
7
  Color,
1✔
8
  GetPickingInfoParams,
1✔
9
  CompositeLayerProps,
1✔
10
  createIterable,
1✔
11
  Layer,
1✔
12
  Material,
1✔
13
  project32,
1✔
14
  LayersList,
1✔
15
  PickingInfo,
1✔
16
  Position,
1✔
17
  Viewport,
1✔
18
  UpdateParameters,
1✔
19
  DefaultProps
1✔
20
} from '@deck.gl/core';
1✔
21
import {getDistanceScales} from '@math.gl/web-mercator';
1✔
22
import {WebGLAggregator} from '../aggregation-layer-v9/gpu-aggregator/webgl-aggregator';
1✔
23
import {CPUAggregator} from '../aggregation-layer-v9/cpu-aggregator/cpu-aggregator';
1✔
24
import AggregationLayer from '../aggregation-layer-v9/aggregation-layer';
1✔
25
import {AggregationOperation} from '../aggregation-layer-v9/aggregator';
1✔
26
import {AggregateAccessor} from '../types';
1✔
27
import {defaultColorRange} from '../utils/color-utils';
1✔
28

1✔
29
import {GridCellLayer} from './grid-cell-layer';
1✔
30

1✔
31
// eslint-disable-next-line @typescript-eslint/no-empty-function
1✔
32
function noop() {}
54✔
33

1✔
34
const defaultProps: DefaultProps<GridLayerProps> = {
1✔
35
  gpuAggregation: false,
1✔
36

1✔
37
  // color
1✔
38
  colorDomain: null,
1✔
39
  colorRange: defaultColorRange,
1✔
40
  getColorValue: {type: 'accessor', value: null}, // default value is calculated from `getColorWeight` and `colorAggregation`
1✔
41
  getColorWeight: {type: 'accessor', value: 1},
1✔
42
  colorAggregation: 'SUM',
1✔
43
  // lowerPercentile: {type: 'number', min: 0, max: 100, value: 0},
1✔
44
  // upperPercentile: {type: 'number', min: 0, max: 100, value: 100},
1✔
45
  // colorScaleType: 'quantize',
1✔
46
  onSetColorDomain: noop,
1✔
47

1✔
48
  // elevation
1✔
49
  elevationDomain: null,
1✔
50
  elevationRange: [0, 1000],
1✔
51
  getElevationValue: {type: 'accessor', value: null}, // default value is calculated from `getElevationWeight` and `elevationAggregation`
1✔
52
  getElevationWeight: {type: 'accessor', value: 1},
1✔
53
  elevationAggregation: 'SUM',
1✔
54
  elevationScale: {type: 'number', min: 0, value: 1},
1✔
55
  // elevationLowerPercentile: {type: 'number', min: 0, max: 100, value: 0},
1✔
56
  // elevationUpperPercentile: {type: 'number', min: 0, max: 100, value: 100},
1✔
57
  // elevationScaleType: 'linear',
1✔
58
  onSetElevationDomain: noop,
1✔
59

1✔
60
  // grid
1✔
61
  cellSize: {type: 'number', min: 0, value: 1000},
1✔
62
  coverage: {type: 'number', min: 0, max: 1, value: 1},
1✔
63
  getPosition: {type: 'accessor', value: (x: any) => x.position},
1✔
64
  gridAggregator: {type: 'function', optional: true, value: null},
1✔
65
  extruded: false,
1✔
66

1✔
67
  // Optional material for 'lighting' shader module
1✔
68
  material: true
1✔
69
};
1✔
70

1✔
71
/** All properties supported by GridLayer. */
1✔
72
export type GridLayerProps<DataT = unknown> = _GridLayerProps<DataT> & CompositeLayerProps;
1✔
73

1✔
74
/** Properties added by GridLayer. */
1✔
75
type _GridLayerProps<DataT> = {
1✔
76
  /**
1✔
77
   * Accessor to retrieve a grid bin index from each data object.
1✔
78
   */
1✔
79
  gridAggregator?: ((position: number[], cellSize: number) => [number, number]) | null;
1✔
80

1✔
81
  /**
1✔
82
   * Size of each cell in meters.
1✔
83
   * @default 1000
1✔
84
   */
1✔
85
  cellSize?: number;
1✔
86

1✔
87
  /**
1✔
88
   * Color scale domain, default is set to the extent of aggregated weights in each cell.
1✔
89
   * @default [min(colorWeight), max(colorWeight)]
1✔
90
   */
1✔
91
  colorDomain?: [number, number] | null;
1✔
92

1✔
93
  /**
1✔
94
   * Default: [colorbrewer](http://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=6) `6-class YlOrRd`
1✔
95
   */
1✔
96
  colorRange?: Color[];
1✔
97

1✔
98
  /**
1✔
99
   * Cell size multiplier, clamped between 0 - 1.
1✔
100
   * @default 1
1✔
101
   */
1✔
102
  coverage?: number;
1✔
103

1✔
104
  /**
1✔
105
   * Elevation scale input domain, default is set to between 0 and the max of aggregated weights in each cell.
1✔
106
   * @default [0, max(elevationWeight)]
1✔
107
   */
1✔
108
  elevationDomain?: [number, number] | null;
1✔
109

1✔
110
  /**
1✔
111
   * Elevation scale output range.
1✔
112
   * @default [0, 1000]
1✔
113
   */
1✔
114
  elevationRange?: [number, number];
1✔
115

1✔
116
  /**
1✔
117
   * Cell elevation multiplier.
1✔
118
   * @default 1
1✔
119
   */
1✔
120
  elevationScale?: number;
1✔
121

1✔
122
  /**
1✔
123
   * Whether to enable cell elevation. If set to false, all cell will be flat.
1✔
124
   * @default true
1✔
125
   */
1✔
126
  extruded?: boolean;
1✔
127

1✔
128
  // TODO - v9
1✔
129
  /**
1✔
130
   * Filter cells and re-calculate color by `upperPercentile`.
1✔
131
   * Cells with value larger than the upperPercentile will be hidden.
1✔
132
   * @default 100
1✔
133
   */
1✔
134
  // upperPercentile?: number;
1✔
135

1✔
136
  // TODO - v9
1✔
137
  /**
1✔
138
   * Filter cells and re-calculate color by `lowerPercentile`.
1✔
139
   * Cells with value smaller than the lowerPercentile will be hidden.
1✔
140
   * @default 0
1✔
141
   */
1✔
142
  // lowerPercentile?: number;
1✔
143

1✔
144
  /**
1✔
145
   * Filter cells and re-calculate elevation by `elevationUpperPercentile`.
1✔
146
   * Cells with elevation value larger than the `elevationUpperPercentile` will be hidden.
1✔
147
   * @default 100
1✔
148
   */
1✔
149
  elevationUpperPercentile?: number;
1✔
150

1✔
151
  /**
1✔
152
   * Filter cells and re-calculate elevation by `elevationLowerPercentile`.
1✔
153
   * Cells with elevation value larger than the `elevationLowerPercentile` will be hidden.
1✔
154
   * @default 0
1✔
155
   */
1✔
156
  elevationLowerPercentile?: number;
1✔
157

1✔
158
  // TODO - v9
1✔
159
  /**
1✔
160
   * Scaling function used to determine the color of the grid cell, default value is 'quantize'.
1✔
161
   * Supported Values are 'quantize', 'linear', 'quantile' and 'ordinal'.
1✔
162
   * @default 'quantize'
1✔
163
   */
1✔
164
  // colorScaleType?: 'quantize' | 'linear' | 'quantile' | 'ordinal';
1✔
165

1✔
166
  // TODO - v9
1✔
167
  /**
1✔
168
   * Scaling function used to determine the elevation of the grid cell, only supports 'linear'.
1✔
169
   */
1✔
170
  // elevationScaleType?: 'linear';
1✔
171

1✔
172
  /**
1✔
173
   * Material settings for lighting effect. Applies if `extruded: true`.
1✔
174
   *
1✔
175
   * @default true
1✔
176
   * @see https://deck.gl/docs/developer-guide/using-lighting
1✔
177
   */
1✔
178
  material?: Material;
1✔
179

1✔
180
  /**
1✔
181
   * Defines the operation used to aggregate all data object weights to calculate a cell's color value.
1✔
182
   * Valid values are 'SUM', 'MEAN', 'MIN', 'MAX', 'COUNT'.
1✔
183
   *
1✔
184
   * @default 'SUM'
1✔
185
   */
1✔
186
  colorAggregation?: AggregationOperation;
1✔
187

1✔
188
  /**
1✔
189
   * Defines the operation used to aggregate all data object weights to calculate a cell's elevation value.
1✔
190
   * Valid values are 'SUM', 'MEAN', 'MIN', 'MAX', 'COUNT'.
1✔
191
   *
1✔
192
   * @default 'SUM'
1✔
193
   */
1✔
194
  elevationAggregation?: AggregationOperation;
1✔
195

1✔
196
  /**
1✔
197
   * Method called to retrieve the position of each object.
1✔
198
   * @default object => object.position
1✔
199
   */
1✔
200
  getPosition?: Accessor<DataT, Position>;
1✔
201

1✔
202
  /**
1✔
203
   * The weight of a data object used to calculate the color value for a cell.
1✔
204
   * @default 1
1✔
205
   */
1✔
206
  getColorWeight?: Accessor<DataT, number>;
1✔
207

1✔
208
  /**
1✔
209
   * After data objects are aggregated into cells, this accessor is called on each cell to get the value that its color is based on.
1✔
210
   * Not supported by GPU aggregation.
1✔
211
   * @default null
1✔
212
   */
1✔
213
  getColorValue?: AggregateAccessor<DataT> | null;
1✔
214

1✔
215
  /**
1✔
216
   * The weight of a data object used to calculate the elevation value for a cell.
1✔
217
   * @default 1
1✔
218
   */
1✔
219
  getElevationWeight?: Accessor<DataT, number>;
1✔
220

1✔
221
  /**
1✔
222
   * After data objects are aggregated into cells, this accessor is called on each cell to get the value that its elevation is based on.
1✔
223
   * Not supported by GPU aggregation.
1✔
224
   * @default null
1✔
225
   */
1✔
226
  getElevationValue?: AggregateAccessor<DataT> | null;
1✔
227

1✔
228
  /**
1✔
229
   * This callback will be called when bin color domain has been calculated.
1✔
230
   * @default () => {}
1✔
231
   */
1✔
232
  onSetColorDomain?: (minMax: [number, number]) => void;
1✔
233

1✔
234
  /**
1✔
235
   * This callback will be called when bin elevation domain has been calculated.
1✔
236
   * @default () => {}
1✔
237
   */
1✔
238
  onSetElevationDomain?: (minMax: [number, number]) => void;
1✔
239

1✔
240
  /**
1✔
241
   * When set to true, aggregation is performed on GPU, provided other conditions are met.
1✔
242
   * @default false
1✔
243
   */
1✔
244
  gpuAggregation?: boolean;
1✔
245
};
1✔
246

1✔
247
export type GridLayerPickingInfo<DataT> = PickingInfo<{
1✔
248
  /** Column index of the picked cell, starting from 0 at the left of the viewport */
1✔
249
  col: number;
1✔
250
  /** Row index of the picked cell, starting from 0 at the top of the viewport */
1✔
251
  row: number;
1✔
252
  /** Aggregated color value */
1✔
253
  colorValue: number;
1✔
254
  /** Aggregated elevation value */
1✔
255
  elevationValue: number;
1✔
256
  /** Number of data points in the picked cell */
1✔
257
  count: number;
1✔
258
  /** Indices of the data objects in the picked cell. Only available if using CPU aggregation. */
1✔
259
  pointIndices?: number[];
1✔
260
  /** The data objects in the picked cell. Only available if using CPU aggregation and layer data is an array. */
1✔
261
  points?: DataT[];
1✔
262
}>;
1✔
263

1✔
264
/** Aggregate data into a grid-based heatmap. The color and height of a cell are determined based on the objects it contains. */
1✔
265
export default class GridLayer<DataT = any, ExtraPropsT extends {} = {}> extends AggregationLayer<
1✔
266
  DataT,
1✔
267
  ExtraPropsT & Required<_GridLayerProps<DataT>>
1✔
268
> {
1✔
269
  static layerName = 'GridLayer';
1✔
270
  static defaultProps = defaultProps;
1✔
271

1✔
272
  state!: AggregationLayer<DataT>['state'] & {
1✔
273
    // Needed if getColorValue, getElevationValue are used
1✔
274
    dataAsArray?: DataT[];
1✔
275
    cellSizeCommon: [number, number];
1✔
276
    cellOriginCommon: [number, number];
1✔
277
    binIdRange: [number, number][];
1✔
278
    aggregatorViewport: Viewport;
1✔
279
  };
1✔
280

1✔
281
  getAggregatorType(): string {
1✔
282
    const {
33✔
283
      gpuAggregation,
33✔
284
      gridAggregator,
33✔
285
      // lowerPercentile,
33✔
286
      // upperPercentile,
33✔
287
      getColorValue,
33✔
288
      getElevationValue
33✔
289
      // colorScaleType
33✔
290
    } = this.props;
33✔
291
    if (
33✔
292
      // GPU aggregation is requested
33✔
293
      gpuAggregation &&
33✔
294
      // GPU aggregation is supported by the device
7✔
295
      WebGLAggregator.isSupported(this.context.device) &&
7✔
296
      // Default grid
7✔
297
      !gridAggregator &&
7✔
298
      // Does not need custom aggregation operation
7✔
299
      !getColorValue &&
7✔
300
      !getElevationValue
5✔
301
      // Does not need CPU-only scale
33✔
302
      // && lowerPercentile === 0 &&
33✔
303
      // && upperPercentile === 100 &&
33✔
304
      // && colorScaleType !== 'quantile'
33✔
305
      // && colorScaleType !== 'ordinal'
33✔
306
    ) {
33✔
307
      return 'gpu';
5✔
308
    }
5✔
309
    return 'cpu';
28✔
310
  }
33✔
311

1✔
312
  createAggregator(type: string): WebGLAggregator | CPUAggregator {
1✔
313
    if (type === 'cpu') {
14✔
314
      const {gridAggregator, cellSize} = this.props;
9✔
315
      return new CPUAggregator({
9✔
316
        dimensions: 2,
9✔
317
        getBin: {
9✔
318
          sources: ['positions'],
9✔
319
          getValue: (
9✔
320
            {positions}: {positions: number[]},
10,110✔
321
            index: number,
10,110✔
322
            opts: {
10,110✔
323
              cellSizeCommon: [number, number];
10,110✔
324
              cellOriginCommon: [number, number];
10,110✔
325
            }
10,110✔
326
          ) => {
10,110✔
327
            if (gridAggregator) {
10,110!
NEW
328
              return gridAggregator(positions, cellSize);
×
NEW
329
            }
×
330
            const viewport = this.state.aggregatorViewport;
10,110✔
331
            // project to common space
10,110✔
332
            const p = viewport.projectPosition(positions);
10,110✔
333
            const {cellSizeCommon, cellOriginCommon} = opts;
10,110✔
334
            return [
10,110✔
335
              Math.floor((p[0] - cellOriginCommon[0]) / cellSizeCommon[0]),
10,110✔
336
              Math.floor((p[1] - cellOriginCommon[1]) / cellSizeCommon[1])
10,110✔
337
            ];
10,110✔
338
          }
10,110✔
339
        },
9✔
340
        getValue: [
9✔
341
          {sources: ['colorWeights'], getValue: ({colorWeights}) => colorWeights},
9✔
342
          {sources: ['elevationWeights'], getValue: ({elevationWeights}) => elevationWeights}
9✔
343
        ]
9✔
344
      });
9✔
345
    }
9✔
346
    return new WebGLAggregator(this.context.device, {
5✔
347
      dimensions: 2,
5✔
348
      channelCount: 2,
5✔
349
      bufferLayout: this.getAttributeManager()!.getBufferLayouts({isInstanced: false}),
5✔
350
      ...super.getShaders({
5✔
351
        modules: [project32],
5✔
352
        vs: /* glsl */ `
5✔
353
  uniform vec2 cellOriginCommon;
5✔
354
  uniform vec2 cellSizeCommon;
5✔
355
  in vec3 positions;
5✔
356
  in vec3 positions64Low;
5✔
357
  in float colorWeights;
5✔
358
  in float elevationWeights;
5✔
359

5✔
360
  void getBin(out ivec2 binId) {
5✔
361
    vec3 positionCommon = project_position(positions, positions64Low);
5✔
362
    vec2 gridCoords = floor(positionCommon.xy / cellSizeCommon);
5✔
363
    binId = ivec2(gridCoords);
5✔
364
  }
5✔
365
  void getValue(out vec2 value) {
5✔
366
    value = vec2(colorWeights, elevationWeights);
5✔
367
  }
5✔
368
  `
5✔
369
      })
5✔
370
    });
5✔
371
  }
14✔
372

1✔
373
  initializeState() {
1✔
374
    super.initializeState();
9✔
375

9✔
376
    const attributeManager = this.getAttributeManager()!;
9✔
377
    attributeManager.add({
9✔
378
      positions: {
9✔
379
        size: 3,
9✔
380
        accessor: 'getPosition',
9✔
381
        type: 'float64',
9✔
382
        fp64: this.use64bitPositions()
9✔
383
      },
9✔
384
      colorWeights: {size: 1, accessor: 'getColorWeight'},
9✔
385
      elevationWeights: {size: 1, accessor: 'getElevationWeight'}
9✔
386
    });
9✔
387
  }
9✔
388

1✔
389
  updateState(params: UpdateParameters<this>) {
1✔
390
    const aggregatorChanged = super.updateState(params);
33✔
391

33✔
392
    const {props, oldProps, changeFlags} = params;
33✔
393
    const {aggregator} = this.state;
33✔
394
    if (
33✔
395
      (changeFlags.dataChanged || !this.state.dataAsArray) &&
33✔
396
      (props.getColorValue || props.getElevationValue)
20✔
397
    ) {
33✔
398
      // Convert data to array
4✔
399
      this.state.dataAsArray = Array.from(createIterable(props.data).iterable);
4✔
400
    }
4✔
401
    if (
33✔
402
      aggregatorChanged ||
33✔
403
      changeFlags.dataChanged ||
19✔
404
      props.cellSize !== oldProps.cellSize ||
15✔
405
      props.getColorValue !== oldProps.getColorValue ||
13✔
406
      props.getElevationValue !== oldProps.getElevationValue ||
11✔
407
      props.colorAggregation !== oldProps.colorAggregation ||
8✔
408
      props.elevationAggregation !== oldProps.elevationAggregation
8✔
409
    ) {
33✔
410
      this._updateBinOptions();
25✔
411
      const {cellSizeCommon, cellOriginCommon, binIdRange, dataAsArray} = this.state;
25✔
412

25✔
413
      aggregator.setProps({
25✔
414
        // @ts-expect-error only used by GPUAggregator
25✔
415
        binIdRange,
25✔
416
        pointCount: this.getNumInstances(),
25✔
417
        operations: [props.colorAggregation, props.elevationAggregation],
25✔
418
        binOptions: {
25✔
419
          cellSizeCommon,
25✔
420
          cellOriginCommon
25✔
421
        },
25✔
422
        onUpdate: this._onAggregationUpdate.bind(this)
25✔
423
      });
25✔
424

25✔
425
      if (dataAsArray) {
25✔
426
        const {getColorValue, getElevationValue} = this.props;
10✔
427
        aggregator.setProps({
10✔
428
          // @ts-expect-error only used by CPUAggregator
10✔
429
          customOperations: [
10✔
430
            getColorValue &&
10✔
431
              ((indices: number[]) =>
5✔
432
                getColorValue(
756✔
433
                  indices.map(i => dataAsArray[i]),
756✔
434
                  {indices, data: props.data}
756✔
435
                )),
756✔
436
            getElevationValue &&
10✔
437
              ((indices: number[]) =>
4✔
438
                getElevationValue(
756✔
439
                  indices.map(i => dataAsArray[i]),
756✔
440
                  {indices, data: props.data}
756✔
441
                ))
756✔
442
          ]
10✔
443
        });
10✔
444
      }
10✔
445
    }
25✔
446
    if (changeFlags.updateTriggersChanged && changeFlags.updateTriggersChanged.getColorValue) {
33✔
447
      aggregator.setNeedsUpdate(0);
3✔
448
    }
3✔
449
    if (changeFlags.updateTriggersChanged && changeFlags.updateTriggersChanged.getElevationValue) {
33✔
450
      aggregator.setNeedsUpdate(1);
3✔
451
    }
3✔
452

33✔
453
    return aggregatorChanged;
33✔
454
  }
33✔
455

1✔
456
  private _updateBinOptions() {
1✔
457
    const bounds = this.getBounds();
39✔
458
    const cellSizeCommon: [number, number] = [1, 1];
39✔
459
    let cellOriginCommon: [number, number] = [0, 0];
39✔
460
    const binIdRange: [number, number][] = [
39✔
461
      [0, 1],
39✔
462
      [0, 1]
39✔
463
    ];
39✔
464
    let viewport = this.context.viewport;
39✔
465

39✔
466
    if (bounds && Number.isFinite(bounds[0][0])) {
39✔
467
      let centroid = [(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2];
28✔
468
      const {cellSize} = this.props;
28✔
469
      const {unitsPerMeter} = getDistanceScales({longitude: centroid[0], latitude: centroid[1]});
28✔
470
      cellSizeCommon[0] = unitsPerMeter[0] * cellSize;
28✔
471
      cellSizeCommon[1] = unitsPerMeter[1] * cellSize;
28✔
472

28✔
473
      // Offset common space to center at the origin of the grid cell where the data center is in
28✔
474
      // This improves precision without affecting the cell positions
28✔
475
      const centroidCommon = viewport.projectFlat(centroid);
28✔
476
      cellOriginCommon = [
28✔
477
        Math.floor(centroidCommon[0] / cellSizeCommon[0]) * cellSizeCommon[0],
28✔
478
        Math.floor(centroidCommon[1] / cellSizeCommon[1]) * cellSizeCommon[1]
28✔
479
      ];
28✔
480
      centroid = viewport.unprojectFlat(cellOriginCommon);
28✔
481

28✔
482
      const ViewportType = viewport.constructor as any;
28✔
483
      // We construct a viewport for the GPU aggregator's project module
28✔
484
      // This viewport is determined by data
28✔
485
      // removes arbitrary precision variance that depends on initial view state
28✔
486
      viewport = viewport.isGeospatial
28✔
487
        ? new ViewportType({longitude: centroid[0], latitude: centroid[1], zoom: 12})
28!
NEW
488
        : new Viewport({position: [centroid[0], centroid[1], 0], zoom: 12});
×
489

28✔
490
      // Round to the nearest 32-bit float to match CPU and GPU results
28✔
491
      cellOriginCommon = [Math.fround(viewport.center[0]), Math.fround(viewport.center[1])];
28✔
492

28✔
493
      const corners = [
28✔
494
        bounds[0],
28✔
495
        bounds[1],
28✔
496
        [bounds[0][0], bounds[1][1]],
28✔
497
        [bounds[1][0], bounds[0][1]]
28✔
498
      ].map(p => viewport.projectFlat(p));
28✔
499

28✔
500
      const minX = Math.min(...corners.map(p => p[0]));
28✔
501
      const minY = Math.min(...corners.map(p => p[1]));
28✔
502
      const maxX = Math.max(...corners.map(p => p[0]));
28✔
503
      const maxY = Math.max(...corners.map(p => p[1]));
28✔
504
      binIdRange[0][0] = Math.floor((minX - cellOriginCommon[0]) / cellSizeCommon[0]);
28✔
505
      binIdRange[0][1] = Math.floor((maxX - cellOriginCommon[0]) / cellSizeCommon[0]) + 1;
28✔
506
      binIdRange[1][0] = Math.floor((minY - cellOriginCommon[1]) / cellSizeCommon[1]);
28✔
507
      binIdRange[1][1] = Math.floor((maxY - cellOriginCommon[1]) / cellSizeCommon[1]) + 1;
28✔
508
    }
28✔
509

39✔
510
    this.setState({cellSizeCommon, cellOriginCommon, binIdRange, aggregatorViewport: viewport});
39✔
511
  }
39✔
512

1✔
513
  override draw(opts) {
1✔
514
    // Replaces render time viewport with our own
34✔
515
    if (opts.moduleParameters.viewport) {
34✔
516
      opts.moduleParameters.viewport = this.state.aggregatorViewport;
34✔
517
    }
34✔
518
    super.draw(opts);
34✔
519
  }
34✔
520

1✔
521
  private _onAggregationUpdate({channel}: {channel: number}) {
1✔
522
    const props = this.getCurrentLayer()!.props;
54✔
523
    const {aggregator} = this.state;
54✔
524
    if (channel === 0) {
54✔
525
      props.onSetColorDomain(aggregator.getResultDomain(0));
26✔
526
    } else if (channel === 1) {
54✔
527
      props.onSetElevationDomain(aggregator.getResultDomain(1));
28✔
528
    }
28✔
529
  }
54✔
530

1✔
531
  onAttributeChange(id: string) {
1✔
532
    const {aggregator} = this.state;
50✔
533
    switch (id) {
50✔
534
      case 'positions':
50✔
535
        aggregator.setNeedsUpdate();
14✔
536

14✔
537
        this._updateBinOptions();
14✔
538
        const {cellSizeCommon, cellOriginCommon, binIdRange} = this.state;
14✔
539
        aggregator.setProps({
14✔
540
          // @ts-expect-error only used by GPUAggregator
14✔
541
          binIdRange,
14✔
542
          binOptions: {
14✔
543
            cellSizeCommon,
14✔
544
            cellOriginCommon
14✔
545
          }
14✔
546
        });
14✔
547
        break;
14✔
548

50✔
549
      case 'colorWeights':
50✔
550
        aggregator.setNeedsUpdate(0);
18✔
551
        break;
18✔
552

50✔
553
      case 'elevationWeights':
50✔
554
        aggregator.setNeedsUpdate(1);
18✔
555
        break;
18✔
556

50✔
557
      default:
50!
558
      // This should not happen
50✔
559
    }
50✔
560
  }
50✔
561

1✔
562
  renderLayers(): LayersList | Layer | null {
1✔
563
    const {aggregator, cellOriginCommon, cellSizeCommon} = this.state;
33✔
564
    const {elevationScale, colorRange, elevationRange, extruded, coverage, material, transitions} =
33✔
565
      this.props;
33✔
566
    const CellLayerClass = this.getSubLayerClass('cells', GridCellLayer);
33✔
567
    const binAttribute = aggregator.getBins();
33✔
568
    const colorsAttribute = aggregator.getResult(0);
33✔
569
    const elevationsAttribute = aggregator.getResult(1);
33✔
570

33✔
571
    return new CellLayerClass(
33✔
572
      this.getSubLayerProps({
33✔
573
        id: 'cells'
33✔
574
      }),
33✔
575
      {
33✔
576
        data: {
33✔
577
          length: aggregator.binCount,
33✔
578
          attributes: {
33✔
579
            getBin: binAttribute,
33✔
580
            getColorValue: colorsAttribute,
33✔
581
            getElevationValue: elevationsAttribute
33✔
582
          }
33✔
583
        },
33✔
584
        // Data has changed shallowly, but we likely don't need to update the attributes
33✔
585
        dataComparator: (data, oldData) => data.length === oldData.length,
33✔
586
        updateTriggers: {
33✔
587
          getBin: [binAttribute],
33✔
588
          getColorValue: [colorsAttribute],
33✔
589
          getElevationValue: [elevationsAttribute]
33✔
590
        },
33✔
591
        cellOriginCommon,
33✔
592
        cellSizeCommon,
33✔
593
        elevationScale,
33✔
594
        colorRange,
33✔
595
        elevationRange,
33✔
596
        extruded,
33✔
597
        coverage,
33✔
598
        material,
33✔
599
        // Evaluate domain at draw() time
33✔
600
        colorDomain: () => this.props.colorDomain || aggregator.getResultDomain(0),
33✔
601
        elevationDomain: () => this.props.elevationDomain || aggregator.getResultDomain(1),
33✔
602
        transitions: transitions && {
33!
NEW
603
          getFillColor: transitions.getColorValue || transitions.getColorWeight,
×
NEW
604
          getElevation: transitions.getElevationValue || transitions.getElevationWeight
×
NEW
605
        },
×
606
        // Extensions are already handled by the GPUAggregator, do not pass it down
33✔
607
        extensions: []
33✔
608
      }
33✔
609
    );
33✔
610
  }
33✔
611

1✔
612
  getPickingInfo(params: GetPickingInfoParams): GridLayerPickingInfo<DataT> {
1✔
613
    const info: GridLayerPickingInfo<DataT> = params.info;
22✔
614
    const {index} = info;
22✔
615
    if (index >= 0) {
22✔
616
      const bin = this.state.aggregator.getBin(index);
22✔
617
      let object: GridLayerPickingInfo<DataT>['object'];
22✔
618
      if (bin) {
22✔
619
        object = {
22✔
620
          col: bin.id[0],
22✔
621
          row: bin.id[1],
22✔
622
          colorValue: bin.value[0],
22✔
623
          elevationValue: bin.value[1],
22✔
624
          count: bin.count
22✔
625
        };
22✔
626
        if (bin.pointIndices) {
22✔
627
          object.pointIndices = bin.pointIndices;
11✔
628
          object.points = Array.isArray(this.props.data)
11✔
629
            ? bin.pointIndices.map(i => (this.props.data as DataT[])[i])
11!
NEW
630
            : [];
×
631
        }
11✔
632
      }
22✔
633
      info.object = object;
22✔
634
    }
22✔
635

22✔
636
    return info;
22✔
637
  }
22✔
638
}
1✔
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