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

keplergl / kepler.gl / 25884645943

14 May 2026 08:43PM UTC coverage: 57.684% (-1.0%) from 58.684%
25884645943

push

github

web-flow
feat: basic annotations (#3434)

* feat: basic annotations

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

* fixes and improvements

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

* fix annotations lag

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

* tests, lint, fixes

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

* formatting/prettier

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

* update icon from target to letters

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

* fix tests

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

* fixes

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

* fix dragging

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

* fixes

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

* fixes

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

* fixes

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

* follow up

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

* fixes; follow ups

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>

7158 of 14867 branches covered (48.15%)

Branch coverage included in aggregate %.

217 of 737 new or added lines in 25 files covered. (29.44%)

70 existing lines in 2 files now uncovered.

14556 of 22776 relevant lines covered (63.91%)

77.67 hits per line

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

69.01
/src/components/src/annotations/annotation-utils.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import {addMetersToLngLat} from '@math.gl/web-mercator';
5
import {Annotation, AnnotationWithArm} from '@kepler.gl/types';
6
import {AnnotationKind, isAnnotationWithArm} from '@kepler.gl/constants';
7

8
export type MapViewport = {
9
  project: (lngLat: [number, number]) => [number, number];
10
  unproject: (xy: [number, number]) => [number, number];
11
  longitude: number;
12
  latitude: number;
13
  width: number;
14
  height: number;
15
  zoom: number;
16
};
17

18
export type BaseAnnotationMarker = {
19
  kind: AnnotationKind;
20
  x: number;
21
  y: number;
22
  tx: number;
23
  ty: number;
24
};
25

26
export type CircleAnnotationMarker = BaseAnnotationMarker & {
27
  kind: AnnotationKind.CIRCLE;
28
  ax: number;
29
  ay: number;
30
  r: number;
31
};
32

33
export type AnnotationMarker = BaseAnnotationMarker | CircleAnnotationMarker;
34

35
function degreesToRadians(degree: number): number {
36
  return degree * (Math.PI / 180);
3✔
37
}
38

39
function calcRadius(
40
  viewport: MapViewport,
41
  point: [number, number],
42
  radiusInMeters: number
43
): number {
NEW
44
  const [x, y] = viewport.project(point);
×
NEW
45
  const shifted = addMetersToLngLat(point, [radiusInMeters, 0, 0]) as [number, number];
×
NEW
46
  const [x1, y1] = viewport.project(shifted);
×
NEW
47
  const dx = x1 - x;
×
NEW
48
  const dy = y1 - y;
×
NEW
49
  return Math.sqrt(dx * dx + dy * dy);
×
50
}
51

52
export function makeMarker(annotation: Annotation, viewport: MapViewport): AnnotationMarker {
53
  const {kind, anchorPoint} = annotation;
5✔
54
  const angle = isAnnotationWithArm(annotation) ? degreesToRadians(annotation.angle) : 0;
5✔
55
  const [x, y] = viewport.project(anchorPoint);
5✔
56

57
  switch (kind) {
5!
58
    case AnnotationKind.CIRCLE: {
NEW
59
      const {armLength, radiusInMeters} = annotation;
×
NEW
60
      const r = calcRadius(viewport, anchorPoint, radiusInMeters);
×
NEW
61
      const [ax, ay] = [Math.cos(angle) * r, Math.sin(angle) * r];
×
NEW
62
      const [tx, ty] = [Math.cos(angle) * (r + armLength), Math.sin(angle) * (r + armLength)];
×
NEW
63
      return {kind, x, y, ax, ay, tx, ty, r} as CircleAnnotationMarker;
×
64
    }
65
    case AnnotationKind.ARROW:
66
    case AnnotationKind.POINT: {
67
      const {armLength} = annotation;
3✔
68
      const [tx, ty] = [Math.cos(angle) * armLength, Math.sin(angle) * armLength];
3✔
69
      return {kind, x, y, tx, ty};
3✔
70
    }
71
    case AnnotationKind.TEXT:
72
    default:
73
      return {kind, x, y, tx: 0, ty: 0};
2✔
74
  }
75
}
76

77
export function isLeftOriented(angle: number): boolean {
78
  return angle > 90 || angle < -90;
8✔
79
}
80

81
export function movePoint(
82
  annotation: Annotation,
83
  delta: {x: number; y: number},
84
  viewport: MapViewport
85
): Partial<Annotation> {
86
  const {anchorPoint} = annotation;
3✔
87
  const [px, py] = viewport.project(anchorPoint);
3✔
88
  const [lon, lat] = viewport.unproject([px + delta.x, py + delta.y]);
3✔
89
  return {anchorPoint: [lon, lat]};
3✔
90
}
91

92
export function moveText(
93
  annotation: Annotation,
94
  delta: {x: number; y: number},
95
  viewport: MapViewport
96
): Partial<Annotation> {
97
  const marker = makeMarker(annotation, viewport);
2✔
98
  const {kind, tx, ty} = marker;
2✔
99

100
  if (kind === AnnotationKind.TEXT) {
2✔
101
    return movePoint(annotation, delta, viewport);
1✔
102
  }
103
  if (!isAnnotationWithArm(annotation)) {
1!
NEW
104
    return {};
×
105
  }
106

107
  const [tx1, ty1] = [tx + delta.x, ty + delta.y];
1✔
108
  const nextAngle = radiansToDegrees(Math.atan2(ty1, tx1));
1✔
109
  let nextArm: number;
110

111
  switch (kind) {
1!
112
    case AnnotationKind.ARROW:
113
    case AnnotationKind.POINT:
114
      nextArm = Math.sqrt(tx1 * tx1 + ty1 * ty1);
1✔
115
      break;
1✔
116
    case AnnotationKind.CIRCLE:
NEW
117
      nextArm = Math.sqrt(tx1 * tx1 + ty1 * ty1) - (marker as CircleAnnotationMarker).r;
×
NEW
118
      break;
×
119
    default:
NEW
120
      nextArm = (annotation as AnnotationWithArm).armLength;
×
121
  }
122
  return {angle: nextAngle, armLength: nextArm};
1✔
123
}
124

125
export function resizeCircle(
126
  annotation: Annotation,
127
  delta: {x: number; y: number},
128
  viewport: MapViewport
129
): Partial<Annotation> {
130
  if (annotation.kind !== AnnotationKind.CIRCLE) return {};
3✔
131
  const {anchorPoint, radiusInMeters} = annotation;
2✔
132
  const shifted = addMetersToLngLat(anchorPoint, [radiusInMeters, 0, 0]) as [number, number];
2✔
133
  const [x] = viewport.project(shifted);
2✔
134
  const newPoint = viewport.unproject([x + delta.x, viewport.project(anchorPoint)[1]]);
2✔
135
  const dx = newPoint[0] - anchorPoint[0];
2✔
136
  const currentRadius = shifted[0] - anchorPoint[0];
2✔
137
  const ratio = currentRadius !== 0 ? (currentRadius + dx) / currentRadius : 1;
2!
138
  return {radiusInMeters: Math.max(0, radiusInMeters * ratio)};
2✔
139
}
140

141
function radiansToDegrees(radians: number): number {
142
  return radians * (180 / Math.PI);
1✔
143
}
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