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

keplergl / kepler.gl / 27918898543

21 Jun 2026 10:03PM UTC coverage: 57.137% (-0.04%) from 57.181%
27918898543

push

github

web-flow
feat: layer groups (#3488)

* feat: layer groups

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

* fixes; regressions; improve

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fixes

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* improve reordering

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fixes for groups

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* application config; group color

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* map with groups import fix

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* follow up

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* follow up

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* follow up fixes

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fixes

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fix

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

---------

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>
Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>
Co-authored-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

7706 of 16138 branches covered (47.75%)

Branch coverage included in aggregate %.

322 of 498 new or added lines in 16 files covered. (64.66%)

179 existing lines in 9 files now uncovered.

15495 of 24468 relevant lines covered (63.33%)

75.32 hits per line

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

0.96
/src/layers/src/mapbox-utils.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import Layer, {OVERLAY_TYPE_CONST} from './base-layer';
5
import {Feature} from 'geojson';
6

7
import {isPlainObject} from '@kepler.gl/utils';
8
import {LayerOrderGroup, LayerOrder} from '@kepler.gl/types';
9

10
/**
11
 * This function will convert layers to mapbox layers
12
 * @param layers the layers to be converted
13
 * @param layerData extra layer information
14
 * @param layerOrder the order by which we should convert layers
15
 * @param layersToRender {[id]: true | false} object whether each layer should be rendered
16
 * @returns
17
 */
18
export function generateMapboxLayers(
19
  layers: Layer[] = [],
×
20
  layerData: any[] = [],
×
21
  layerOrder: LayerOrder = [],
×
22
  layersToRender: {[key: string]: boolean} = {}
×
23
): {[key: string]: Layer} {
24
  if (layerData.length > 0) {
×
25
    return layerOrder
×
26
      .slice()
27
      .reverse()
28
      .reduce((acc, element) => {
NEW
29
        if (isPlainObject(element)) {
×
NEW
30
          const layerGroup = element as LayerOrderGroup;
×
NEW
31
          if (!layerGroup.isVisible) {
×
NEW
32
            return acc;
×
33
          }
NEW
34
          return {
×
35
            ...acc,
36
            ...generateMapboxLayers(layers, layerData, layerGroup.layerOrder, layersToRender)
37
          };
38
        }
39

NEW
40
        const layerId = element as string;
×
41
        const layerIndex = layers.findIndex(l => l.id === layerId);
×
42
        if (layerIndex === -1) {
×
43
          return acc;
×
44
        }
45

46
        const layer = layers[layerIndex];
×
47

48
        if (!(layer.overlayType === OVERLAY_TYPE_CONST.mapboxgl && layersToRender[layerId])) {
×
49
          return acc;
×
50
        }
51

52
        return {
×
53
          ...acc,
54
          [layer.id]: {
55
            id: layer.id,
56
            data: layerData[layerIndex].data,
57
            isVisible: layer.config.isVisible,
58
            config: layerData[layerIndex].config,
59
            hidden: layer.config.hidden,
60
            sourceId: layerData[layerIndex].config.source
61
          }
62
        };
63
      }, {});
64
  }
65

66
  return {};
×
67
}
68

69
type newLayersType = {
70
  [key: string]: Layer & Partial<{data: any; sourceId: any; isVisible: boolean}>;
71
};
72
type oldLayersType = {[key: string]: Layer & {data?: any}};
73
/**
74
 * Update mapbox layers on the given map
75
 * @param map
76
 * @param newLayers Map of new mapbox layers to be displayed
77
 * @param oldLayers Map of the old layers to be compare with the current ones to detect deleted layers
78
 *                  {layerId: sourceId}
79
 */
80
export function updateMapboxLayers(
81
  map,
82
  newLayers: newLayersType = {},
×
83
  oldLayers: oldLayersType | null = null
×
84
) {
85
  // delete no longer existed old layers
86
  if (oldLayers) {
×
87
    checkAndRemoveOldLayers(map, oldLayers, newLayers);
×
88
  }
89

90
  // insert or update new layer
91
  Object.values(newLayers).forEach(overlay => {
×
92
    const {id: layerId, config, data, sourceId, isVisible} = overlay;
×
93
    if (!data && !config) {
×
94
      return;
×
95
    }
96

97
    const {data: oldData, config: oldConfig} = (oldLayers && oldLayers[layerId]) || {};
×
98

99
    if (data && data !== oldData) {
×
100
      updateSourceData(map, sourceId, data);
×
101
    }
102

103
    // compare with previous configs
104
    if (oldConfig !== config) {
×
105
      updateLayerConfig(map, layerId, config, isVisible);
×
106
    }
107
  });
108
}
109

110
function checkAndRemoveOldLayers(map, oldLayers: oldLayersType, newLayers: newLayersType) {
111
  Object.keys(oldLayers).forEach(layerId => {
×
112
    if (!newLayers[layerId]) {
×
113
      map.removeLayer(layerId);
×
114
    }
115
  });
116
}
117

118
function updateLayerConfig(map, layerId, config, isVisible) {
119
  const mapboxLayer = map.getLayer(layerId);
×
120

121
  if (mapboxLayer) {
×
122
    // check if layer already is set
123
    // remove it if exists
124
    map.removeLayer(layerId);
×
125
  }
126

127
  map.addLayer(config);
×
128
  map.setLayoutProperty(layerId, 'visibility', isVisible ? 'visible' : 'none');
×
129
}
130

131
function updateSourceData(map, sourceId, data) {
132
  const source = map.getSource(sourceId);
×
133

134
  if (!source) {
×
135
    map.addSource(sourceId, {
×
136
      type: 'geojson',
137
      data
138
    });
139
  } else {
140
    source.setData(data);
×
141
  }
142
}
143

144
/**
145
 *
146
 * @param filteredIndex
147
 * @param getGeometry {({index: number}) => any}
148
 * @param getProperties {({index: number}) => any}
149
 * @returns FeatureCollection
150
 */
151
export function geoJsonFromData(
152
  filteredIndex: number[] = [],
×
153
  getGeometry: (arg: {index: number}) => any,
154
  getProperties: (arg: {index: number}) => object
155
) {
156
  const geojson: {type: string; features: Feature[]} = {
×
157
    type: 'FeatureCollection',
158
    features: []
159
  };
160

161
  for (let i = 0; i < filteredIndex.length; i++) {
×
162
    const index = filteredIndex[i];
×
163
    const rowIndex = {index};
×
164
    const geometry = getGeometry(rowIndex);
×
165

166
    if (geometry) {
×
167
      geojson.features.push({
×
168
        type: 'Feature',
169
        properties: {
170
          index,
171
          ...getProperties(rowIndex)
172
        },
173
        geometry
174
      });
175
    }
176
  }
177

178
  return geojson;
×
179
}
180

181
export const prefixGpuField = name => `gpu:${name}`;
13✔
182

183
export function gpuFilterToMapboxFilter(gpuFilter) {
184
  const {filterRange, filterValueUpdateTriggers} = gpuFilter;
×
185

186
  const hasFilter = Object.values(filterValueUpdateTriggers).filter(d => d);
×
187

188
  if (!hasFilter.length) {
×
189
    return null;
×
190
  }
191

192
  const condition = ['all'];
×
193

194
  // [">=", key, value]
195
  // ["<=", key, value]
196
  const expressions = Object.values(filterValueUpdateTriggers as ({name: string} | null)[]).reduce(
×
197
    (accu: any[], gpu, i) =>
198
      gpu?.name
×
199
        ? [
200
            ...accu,
201
            ['>=', prefixGpuField(gpu.name), filterRange[i][0]],
202
            ['<=', prefixGpuField(gpu.name), filterRange[i][1]]
203
          ]
204
        : accu,
205
    condition
206
  );
207

208
  return expressions;
×
209
}
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