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

keplergl / kepler.gl / 25830234009

13 May 2026 10:31PM UTC coverage: 57.668% (-1.0%) from 58.644%
25830234009

Pull #3434

github

web-flow
Merge b10a56eba into b4790f0f5
Pull Request #3434: feat: basic annotations

7143 of 14848 branches covered (48.11%)

Branch coverage included in aggregate %.

216 of 732 new or added lines in 25 files covered. (29.51%)

74 existing lines in 3 files now uncovered.

14551 of 22771 relevant lines covered (63.9%)

77.31 hits per line

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

25.68
/src/components/src/annotations/annotation-overlay.tsx
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import React, {FC, useCallback, useMemo} from 'react';
5
import styled from 'styled-components';
6
import {
7
  DndContext,
8
  DragMoveEvent,
9
  DragStartEvent,
10
  DragEndEvent,
11
  PointerSensor,
12
  useSensor,
13
  useSensors
14
} from '@dnd-kit/core';
15
import {Annotation} from '@kepler.gl/types';
16
import {ActionHandler, updateAnnotation, setSelectedAnnotation} from '@kepler.gl/actions';
17
import AnnotationNode from './annotation-node';
18
import AnnotationText from './annotation-text';
19
import {MapViewport, movePoint, moveText, resizeCircle} from './annotation-utils';
20

21
const AnnotationOverlayContainer = styled.div`
7✔
22
  position: absolute;
23
  top: 0;
24
  left: 0;
25
  width: 100%;
26
  height: 100%;
27
  pointer-events: none;
28
  overflow: hidden;
29
  z-index: 0;
30
`;
31

32
const AnnotationSvgContainer = styled.svg`
7✔
33
  position: absolute;
34
  top: 0;
35
  left: 0;
36
  pointer-events: none;
37
`;
38

39
const AnnotationTextContainer = styled.div`
7✔
40
  position: absolute;
41
  top: 0;
42
  left: 0;
43
  pointer-events: none;
44
`;
45

46
export type AnnotationOverlayProps = {
47
  annotations: Annotation[];
48
  selectedAnnotationId: string | null;
49
  isEditingAnnotationText: boolean;
50
  isAnnotationMode: boolean;
51
  mapIndex: number;
52
  viewport: MapViewport;
53
  updateAnnotation: ActionHandler<typeof updateAnnotation>;
54
  setSelectedAnnotation: ActionHandler<typeof setSelectedAnnotation>;
55
};
56

57
const AnnotationOverlay: FC<AnnotationOverlayProps> = ({
7✔
58
  annotations,
59
  selectedAnnotationId,
60
  isEditingAnnotationText,
61
  isAnnotationMode,
62
  mapIndex,
63
  viewport,
64
  updateAnnotation: onUpdateAnnotation,
65
  setSelectedAnnotation: onSetSelectedAnnotation
66
}) => {
67
  const annotationsToRender = useMemo(
29✔
68
    () => annotations.filter(ann => (ann.mapIndex ?? 0) === mapIndex && ann.isVisible).reverse(),
26!
69
    [annotations, mapIndex]
70
  );
71

72
  const draggedAnnotationRef = React.useRef<Annotation | null>(null);
29✔
73

74
  const sensors = useSensors(
29✔
75
    useSensor(PointerSensor, {
76
      activationConstraint: {distance: 3}
77
    })
78
  );
79

80
  const accessibility = useMemo(
29✔
81
    () => ({
25✔
82
      announcements: {
83
        onDragStart() {
NEW
84
          return '';
×
85
        },
86
        onDragOver() {
NEW
87
          return '';
×
88
        },
89
        onDragEnd() {
NEW
90
          return '';
×
91
        },
92
        onDragCancel() {
NEW
93
          return '';
×
94
        }
95
      },
96
      screenReaderInstructions: {draggable: ''},
97
      ...(typeof document !== 'undefined' ? {container: document.body} : {})
25!
98
    }),
99
    []
100
  );
101

102
  const handleDragStart = useCallback(
29✔
103
    (ev: DragStartEvent) => {
NEW
104
      const annId = `${ev.active.id}`.split(':')[0];
×
NEW
105
      const ann = annotations.find(a => a.id === annId) ?? null;
×
NEW
106
      draggedAnnotationRef.current = ann;
×
NEW
107
      if (ann) {
×
NEW
108
        onSetSelectedAnnotation(ann.id, false);
×
109
      }
110
    },
111
    [annotations, onSetSelectedAnnotation]
112
  );
113

114
  const handleDragMove = useCallback(
29✔
115
    (ev: DragMoveEvent) => {
NEW
116
      const [annId, handleKind] = `${ev.active.id}`.split(':');
×
NEW
117
      const draggedAnn = draggedAnnotationRef.current;
×
NEW
118
      if (!draggedAnn || draggedAnn.id !== annId) return;
×
119

NEW
120
      let changes: Partial<Annotation> = {};
×
NEW
121
      switch (handleKind) {
×
122
        case 'MOVE_POINT':
NEW
123
          changes = movePoint(draggedAnn, ev.delta, viewport);
×
NEW
124
          break;
×
125
        case 'MOVE_TEXT':
NEW
126
          changes = moveText(draggedAnn, ev.delta, viewport);
×
NEW
127
          break;
×
128
        case 'RESIZE':
NEW
129
          changes = resizeCircle(draggedAnn, ev.delta, viewport);
×
NEW
130
          break;
×
131
      }
NEW
132
      if (Object.keys(changes).length > 0) {
×
NEW
133
        onUpdateAnnotation(annId, changes);
×
134
      }
135
    },
136
    [viewport, onUpdateAnnotation]
137
  );
138

139
  const handleDragEnd = useCallback((_ev: DragEndEvent) => {
29✔
NEW
140
    draggedAnnotationRef.current = null;
×
141
  }, []);
142

143
  const handleSelectAnnotation = useCallback(
29✔
144
    (id: string, isEditingText: boolean) => {
NEW
145
      onSetSelectedAnnotation(id, isEditingText);
×
146
    },
147
    [onSetSelectedAnnotation]
148
  );
149

150
  const handleChangeText = useCallback(
29✔
151
    (id: string, text: string, editorState?: Record<string, any>) => {
NEW
152
      onUpdateAnnotation(id, {label: text, ...(editorState ? {editorState} : {})});
×
153
    },
154
    [onUpdateAnnotation]
155
  );
156

157
  if (!annotationsToRender.length) {
29!
158
    return null;
29✔
159
  }
160

NEW
161
  const {width, height} = viewport;
×
162

NEW
163
  return (
×
164
    <AnnotationOverlayContainer>
165
      <DndContext
166
        sensors={sensors}
167
        accessibility={accessibility}
168
        onDragStart={handleDragStart}
169
        onDragMove={handleDragMove}
170
        onDragEnd={handleDragEnd}
171
      >
172
        <AnnotationSvgContainer width={width} height={height}>
173
          {annotationsToRender.map(annotation => (
NEW
174
            <AnnotationNode
×
175
              key={annotation.id}
176
              annotation={annotation}
177
              viewport={viewport}
178
              isEditing={isAnnotationMode}
179
              isSelected={annotation.id === selectedAnnotationId}
180
            />
181
          ))}
182
        </AnnotationSvgContainer>
183
        <AnnotationTextContainer style={{width, height}}>
184
          {annotationsToRender.map(annotation => (
NEW
185
            <AnnotationText
×
186
              key={annotation.id}
187
              annotation={annotation}
188
              viewport={viewport}
189
              isEditing={isAnnotationMode}
190
              isSelected={annotation.id === selectedAnnotationId}
191
              isEditingText={annotation.id === selectedAnnotationId && isEditingAnnotationText}
×
NEW
192
              onSelect={isEditingText => handleSelectAnnotation(annotation.id, isEditingText)}
×
193
              onChangeText={(text, editorState) =>
NEW
194
                handleChangeText(annotation.id, text, editorState)
×
195
              }
NEW
196
              onUpdateAnnotation={config => onUpdateAnnotation(annotation.id, config)}
×
197
            />
198
          ))}
199
        </AnnotationTextContainer>
200
      </DndContext>
201
    </AnnotationOverlayContainer>
202
  );
203
};
204

205
export default AnnotationOverlay;
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