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

keplergl / kepler.gl / 25949563245

16 May 2026 01:44AM UTC coverage: 57.474% (-0.2%) from 57.684%
25949563245

Pull #3439

github

web-flow
Merge 712b55237 into 3bca72501
Pull Request #3439: fix: prevent unnecessary UI re-renders

7171 of 14990 branches covered (47.84%)

Branch coverage included in aggregate %.

69 of 153 new or added lines in 7 files covered. (45.1%)

6 existing lines in 1 file now uncovered.

14619 of 22923 relevant lines covered (63.77%)

77.19 hits per line

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

31.82
/src/components/src/map/map-control.tsx
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import React, {memo} from 'react';
5
import styled from 'styled-components';
6
import KeplerGlLogo from '../common/logo';
7

8
// factories
9
import SplitMapButtonFactory from './split-map-button';
10
import Toggle3dButtonFactory from './toggle-3d-button';
11
import LayerSelectorPanelFactory from './layer-selector-panel';
12
import MapLegendPanelFactory from './map-legend-panel';
13
import MapDrawPanelFactory from './map-draw-panel';
14
import LocalePanelFactory from './locale-panel';
15
import {Layer} from '@kepler.gl/layers';
16
import {Editor, LayerVisConfig, MapControls, MapState} from '@kepler.gl/types';
17
import {Datasets} from '@kepler.gl/table';
18
import {MapStateActions, UIStateActions} from '@kepler.gl/actions';
19

20
import AnnotationControlFactory from './annotations/annotation-control';
21

22
interface StyledMapControlProps {
23
  $top?: number;
24
}
25

26
const StyledMapControl = styled.div<StyledMapControlProps>`
7✔
27
  right: 0;
28
  padding: ${props => props.theme.mapControl.padding}px;
28✔
29
  z-index: 10;
30
  margin-top: ${props => props.$top || 0}px;
28✔
31
  position: absolute;
32
  display: grid;
33
  row-gap: 8px;
34
  justify-items: end;
35
  pointer-events: none; /* prevent padding from blocking input */
36
  & > * {
37
    /* all children should allow input */
38
    pointer-events: all;
39
  }
40
`;
41

42
const LegendLogo = <KeplerGlLogo version={false} appName="kepler.gl" />;
7✔
43

44
export type MapControlProps = {
45
  datasets: Datasets;
46
  dragRotate: boolean;
47
  isSplit: boolean;
48
  primary: boolean;
49
  layers: Layer[];
50
  layersToRender: {[key: string]: boolean};
51
  mapIndex: number;
52
  mapControls: MapControls;
53
  onTogglePerspective: () => void;
54
  onToggleSplitMap: typeof MapStateActions.toggleSplitMap;
55
  onToggleSplitMapViewport: ({
56
    isViewportSynced,
57
    isZoomLocked
58
  }: {
59
    isViewportSynced: boolean;
60
    isZoomLocked: boolean;
61
  }) => void;
62
  onMapToggleLayer: (layerId: string) => void;
63
  onToggleMapControl: (control: string) => void;
64
  onSetEditorMode: (mode: string) => void;
65
  onToggleEditorVisibility: () => void;
66
  onLayerVisConfigChange: (oldLayer: Layer, newVisConfig: Partial<LayerVisConfig>) => void;
67
  onToggleLayerVisibility?: (layer: Layer) => void;
68
  top: number;
69
  onSetLocale: typeof UIStateActions.setLocale;
70
  availableLocales: string[];
71
  locale: string;
72
  logoComponent?: React.FC | React.ReactNode;
73
  isExport?: boolean;
74

75
  setMapControlSettings: typeof UIStateActions.setMapControlSettings;
76
  activeSidePanel: string | null;
77

78
  // optional
79
  mapState?: MapState;
80
  readOnly?: boolean;
81
  scale?: number;
82
  mapLayers?: {[key: string]: boolean};
83
  editor: Editor;
84
  actionComponents?: React.ComponentType<any>[];
85
  mapHeight?: number;
86
};
87

88
MapControlFactory.deps = [
7✔
89
  SplitMapButtonFactory,
90
  Toggle3dButtonFactory,
91
  LayerSelectorPanelFactory,
92
  MapLegendPanelFactory,
93
  MapDrawPanelFactory,
94
  LocalePanelFactory,
95
  AnnotationControlFactory
96
];
97

98
function MapControlFactory(
99
  SplitMapButton: ReturnType<typeof SplitMapButtonFactory>,
100
  Toggle3dButton: ReturnType<typeof Toggle3dButtonFactory>,
101
  LayerSelectorPanel: ReturnType<typeof LayerSelectorPanelFactory>,
102
  MapLegendPanel: ReturnType<typeof MapLegendPanelFactory>,
103
  MapDrawPanel: ReturnType<typeof MapDrawPanelFactory>,
104
  LocalePanel: ReturnType<typeof LocalePanelFactory>,
105
  AnnotationControl: ReturnType<typeof AnnotationControlFactory>
106
) {
107
  const DEFAULT_ACTIONS = [
14✔
108
    SplitMapButton,
109
    LayerSelectorPanel,
110
    Toggle3dButton,
111
    MapDrawPanel,
112
    AnnotationControl,
113
    LocalePanel,
114
    MapLegendPanel
115
  ];
116

117
  const MapControl: React.FC<MapControlProps> & {
118
    defaultActionComponents: MapControlProps['actionComponents'];
119
  } = ({
14✔
120
    actionComponents = DEFAULT_ACTIONS,
29✔
121
    isSplit = false,
1✔
122
    top = 0,
1✔
123
    mapIndex = 0,
×
124
    logoComponent = LegendLogo,
29✔
125
    ...restProps
126
  }) => {
127
    const actionComponentProps = {
29✔
128
      isSplit,
129
      mapIndex,
130
      logoComponent,
131
      ...restProps
132
    };
133
    return (
29✔
134
      <StyledMapControl className="map-control" $top={top}>
135
        {actionComponents.map((ActionComponent, index) => (
136
          <ActionComponent key={index} className="map-control-action" {...actionComponentProps} />
203✔
137
        ))}
138
      </StyledMapControl>
139
    );
140
  };
141

142
  MapControl.defaultActionComponents = DEFAULT_ACTIONS;
14✔
143

144
  MapControl.displayName = 'MapControl';
14✔
145

146
  const areMapControlPropsEqual = (prev: MapControlProps, next: MapControlProps): boolean => {
14✔
147
    const keys = Object.keys(next) as (keyof MapControlProps)[];
4✔
148
    for (const key of keys) {
4✔
149
      if (prev[key] === next[key]) continue;
53✔
150

151
      if (key === 'layers') {
3!
NEW
152
        const pl = prev.layers;
×
NEW
153
        const nl = next.layers;
×
NEW
154
        if (!pl || !nl || pl.length !== nl.length) return false;
×
NEW
155
        for (let i = 0; i < nl.length; i++) {
×
NEW
156
          if (pl[i] === nl[i]) continue;
×
NEW
157
          if (pl[i].id !== nl[i].id) return false;
×
NEW
158
          if (pl[i].config.isVisible !== nl[i].config.isVisible) return false;
×
NEW
159
          if (pl[i].config.label !== nl[i].config.label) return false;
×
NEW
160
          if (pl[i].config.isConfigActive !== nl[i].config.isConfigActive) return false;
×
161
        }
NEW
162
        continue;
×
163
      }
164

165
      if (key === 'datasets') {
3!
NEW
166
        const pd = prev.datasets;
×
NEW
167
        const nd = next.datasets;
×
NEW
168
        if (!pd || !nd) return false;
×
NEW
169
        const pKeys = Object.keys(pd);
×
NEW
170
        const nKeys = Object.keys(nd);
×
NEW
171
        if (pKeys.length !== nKeys.length) return false;
×
NEW
172
        for (const dk of nKeys) {
×
NEW
173
          if (!pd[dk]) return false;
×
NEW
174
          if (pd[dk] === nd[dk]) continue;
×
NEW
175
          if (pd[dk].id !== nd[dk].id) return false;
×
NEW
176
          if (pd[dk].label !== nd[dk].label) return false;
×
NEW
177
          if (pd[dk].color !== nd[dk].color) return false;
×
178
        }
NEW
179
        continue;
×
180
      }
181

182
      if (key === 'layersToRender') {
3!
NEW
183
        const pl = prev.layersToRender;
×
NEW
184
        const nl = next.layersToRender;
×
NEW
185
        if (!pl || !nl) return false;
×
NEW
186
        const pKeys = Object.keys(pl);
×
NEW
187
        const nKeys = Object.keys(nl);
×
NEW
188
        if (pKeys.length !== nKeys.length) return false;
×
NEW
189
        for (const lk of nKeys) {
×
NEW
190
          if (pl[lk] !== nl[lk]) return false;
×
191
        }
NEW
192
        continue;
×
193
      }
194

195
      return false;
3✔
196
    }
197
    return true;
1✔
198
  };
199

200
  const MemoizedMapControl = memo(MapControl, areMapControlPropsEqual) as React.NamedExoticComponent<MapControlProps> & {
14✔
201
    defaultActionComponents: MapControlProps['actionComponents'];
202
  };
203
  (MemoizedMapControl as any).defaultActionComponents = DEFAULT_ACTIONS;
204

205
  return MemoizedMapControl;
206
}
14✔
207

208
export default MapControlFactory;
14✔
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