Skip to content

Commit 290076e

Browse files
authored
feat: tooltips (#1263)
* stand-alone tooltips * listen to location and content changes * bind tooltip to Layer * listen to tooltip changes * doc for tooltip * remove outdated comment * updated doc
1 parent eceb588 commit 290076e

File tree

5 files changed

+217
-1
lines changed

5 files changed

+217
-1
lines changed

docs/layers/tooltip.rst

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
Tooltip
2+
=====
3+
4+
Example
5+
-------
6+
7+
.. jupyter-execute::
8+
9+
from ipyleaflet import Map, Tooltip, Marker, Polygon, Circle
10+
11+
m = Map(center=(51.505,-0.09), zoom=13)
12+
13+
standalone_tooltip = Tooltip(
14+
location=[51.5, -0.09],
15+
content="Hello world!<br />This is a nice tooltip.",
16+
offset=[-30,50], # Offset in pixels
17+
permanent=False, # The default is False, in which case you can remove the tooltip by clicking anywhere on the map.
18+
direction='bottom', # Default is 'auto'
19+
)
20+
21+
marker_tooltip = Tooltip(
22+
content="I'm a marker tooltip! 👋<br>Appears on hover.",
23+
)
24+
25+
marker = Marker(
26+
location=[51.5, -0.09],
27+
draggable=False,
28+
tooltip=marker_tooltip,
29+
)
30+
31+
polygon = Polygon(
32+
locations= [
33+
[51.509, -0.08],
34+
[51.503, -0.06],
35+
[51.51, -0.047]
36+
])
37+
38+
polygon_tooltip = Tooltip(
39+
content = "Polygon's Permanent Tooltip 🗺️",
40+
permanent = True,
41+
direction = 'center', # Centers the tooltip on the polygon
42+
)
43+
44+
polygon.tooltip = polygon_tooltip
45+
46+
circle = Circle(
47+
location = [51.515, -0.1],
48+
radius = 500,
49+
color = 'green',
50+
fillColor = '#0f3',
51+
fillOpacity = 0.5,
52+
tooltip = Tooltip(
53+
content = "Sticky Tooltip here! 📍<br>Stays with the mouse.",
54+
sticky = True,
55+
)
56+
)
57+
58+
m.add(standalone_tooltip)
59+
m.add(marker)
60+
m.add(polygon)
61+
m.add(circle)
62+
63+
m
64+
65+
66+
Attributes and methods
67+
----------------------
68+
69+
.. autoclass:: ipyleaflet.leaflet.Tooltip
70+
:members:

python/ipyleaflet/ipyleaflet/leaflet.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ class Layer(Widget, InteractMixin):
175175
Make Leaflet-Geoman ignore the layer, so it cannot modify it.
176176
snap_ignore: boolean
177177
Make Leaflet-Geoman snapping ignore the layer, so it is not used as a snap target when editing.
178+
tooltip: Tooltip widget
179+
Tooltip widget to bind to the layer.
178180
"""
179181

180182
_view_name = Unicode("LeafletLayerView").tag(sync=True)
@@ -196,6 +198,10 @@ class Layer(Widget, InteractMixin):
196198
popup_max_height = Int(default_value=None, allow_none=True).tag(sync=True)
197199
pane = Unicode("").tag(sync=True)
198200

201+
tooltip = Instance(Widget, allow_none=True, default_value=None).tag(
202+
sync=True, **widget_serialization
203+
)
204+
199205
options = List(trait=Unicode()).tag(sync=True)
200206
subitems = Tuple().tag(trait=Instance(Widget), sync=True, **widget_serialization)
201207

@@ -640,6 +646,43 @@ def close_popup(self):
640646
self.send({"msg": "close"})
641647

642648

649+
class Tooltip(UILayer):
650+
"""Tooltip class.
651+
652+
Used to display small texts on top of map layers.
653+
654+
Attributes
655+
----------
656+
location: tuple, default None
657+
Optional tuple containing the latitude/longitude of the stand-alone tooltip.
658+
content: str, default ""
659+
The text to show inside the tooltip
660+
offset: tuple, default (0, 0)
661+
Optional offset of the tooltip position (in pixels).
662+
direction: str, default 'auto'
663+
Direction where to open the tooltip.
664+
Possible values are: right, left, top, bottom, center, auto.
665+
auto will dynamically switch between right and left according
666+
to the tooltip position on the map.
667+
permanent: bool, default False
668+
Whether to open the tooltip permanently or only on mouseover.
669+
sticky: bool, default False
670+
If true, the tooltip will follow the mouse instead of being fixed at the feature center.
671+
This option only applies when binding the tooltip to a Layer, not as stand-alone.
672+
"""
673+
_view_name = Unicode("LeafletTooltipView").tag(sync=True)
674+
_model_name = Unicode("LeafletTooltipModel").tag(sync=True)
675+
676+
location = List(allow_none=True, default_value=None).tag(sync=True)
677+
678+
# Options
679+
content = Unicode('').tag(sync=True, o=True)
680+
offset = List(def_loc).tag(sync=True, o=True)
681+
direction=Unicode('auto').tag(sync=True, o=True)
682+
permanent = Bool(False).tag(sync=True, o=True)
683+
sticky = Bool(False).tag(sync=True, o=True)
684+
685+
643686
class RasterLayer(Layer):
644687
"""Abstract RasterLayer class.
645688

python/jupyter_leaflet/src/jupyter-leaflet.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export * from './layers/Popup';
2727
export * from './layers/RasterLayer';
2828
export * from './layers/Rectangle';
2929
export * from './layers/TileLayer';
30+
export * from './layers/Tooltip';
3031
export * from './layers/VectorLayer';
3132
export * from './layers/VectorTileLayer';
3233
export * from './layers/Velocity';

python/jupyter_leaflet/src/layers/Layer.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from '@jupyter-widgets/base';
1010
import { IMessageHandler, MessageLoop } from '@lumino/messaging';
1111
import { Widget } from '@lumino/widgets';
12-
import { Control, Layer, LeafletMouseEvent, Popup } from 'leaflet';
12+
import { Control, Layer, LeafletMouseEvent, Popup, Tooltip } from 'leaflet';
1313
import { LeafletControlView, LeafletMapView } from '../jupyter-leaflet';
1414
import L from '../leaflet';
1515
import { LeafletWidgetView } from '../utils';
@@ -29,6 +29,7 @@ export interface ILeafletLayerModel {
2929
popup_max_width: number;
3030
popup_max_height: number | null;
3131
pane: string;
32+
tooltip: WidgetModel | null;
3233
subitems: Layer[];
3334
pm_ignore: boolean;
3435
snap_ignore: boolean;
@@ -55,13 +56,15 @@ export class LeafletLayerModel extends WidgetModel {
5556
subitems: [],
5657
pm_ignore: true,
5758
snap_ignore: false,
59+
tooltip: null,
5860
};
5961
}
6062
}
6163

6264
LeafletLayerModel.serializers = {
6365
...WidgetModel.serializers,
6466
popup: { deserialize: unpack_models },
67+
tooltip: { deserialize: unpack_models },
6568
subitems: { deserialize: unpack_models },
6669
};
6770

@@ -83,13 +86,16 @@ export class LeafletLayerView extends LeafletWidgetView {
8386
obj: Layer;
8487
subitems: WidgetModel[];
8588
pWidget: IMessageHandler;
89+
tooltip_content: LeafletLayerView;
90+
tooltip_content_promise: Promise<void>;
8691

8792
create_obj(): void {}
8893

8994
initialize(parameters: WidgetView.IInitializeParameters<LeafletLayerModel>) {
9095
super.initialize(parameters);
9196
this.map_view = this.options.map_view;
9297
this.popup_content_promise = Promise.resolve();
98+
this.tooltip_content_promise = Promise.resolve();
9399
}
94100

95101
remove_subitem_view(child_view: LeafletLayerView) {
@@ -128,6 +134,7 @@ export class LeafletLayerView extends LeafletWidgetView {
128134
this.listenTo(this.model, 'change:popup', (model, value_2) => {
129135
this.bind_popup(value_2);
130136
});
137+
this.bind_tooltip(this.model.get('tooltip'));
131138
this.update_pane();
132139
this.subitem_views = new ViewList(
133140
this.add_subitem_model,
@@ -200,6 +207,9 @@ export class LeafletLayerView extends LeafletWidgetView {
200207
this.listenTo(this.model, 'change:subitems', () => {
201208
this.subitem_views.update(this.subitems);
202209
});
210+
this.listenTo(this.model, 'change:tooltip', () => {
211+
this.bind_tooltip(this.model.get('tooltip'));
212+
});
203213
}
204214

205215
remove() {
@@ -210,6 +220,11 @@ export class LeafletLayerView extends LeafletWidgetView {
210220
this.popup_content.remove();
211221
}
212222
});
223+
this.tooltip_content_promise.then(() => {
224+
if (this.tooltip_content) {
225+
this.tooltip_content.remove();
226+
}
227+
});
213228
}
214229

215230
bind_popup(value: WidgetModel) {
@@ -252,6 +267,27 @@ export class LeafletLayerView extends LeafletWidgetView {
252267
this.obj.togglePopup();
253268
this.obj.togglePopup();
254269
}
270+
271+
bind_tooltip(value: WidgetModel) {
272+
if (this.tooltip_content) {
273+
this.obj.unbindTooltip();
274+
this.tooltip_content.remove();
275+
}
276+
if (value) {
277+
this.tooltip_content_promise = this.tooltip_content_promise.then(
278+
async () => {
279+
const view = await this.create_child_view<LeafletLayerView>(value, {
280+
map_view: this.map_view,
281+
});
282+
if (view.obj instanceof Tooltip) {
283+
this.obj.bindTooltip(view.obj);
284+
}
285+
this.tooltip_content = view;
286+
}
287+
);
288+
}
289+
return this.tooltip_content_promise;
290+
}
255291
}
256292

257293
export class LeafletUILayerView extends LeafletLayerView {}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
import { WidgetView } from '@jupyter-widgets/base';
5+
import { Tooltip, TooltipOptions } from 'leaflet';
6+
import L from '../leaflet';
7+
import {
8+
ILeafletLayerModel,
9+
LeafletUILayerModel,
10+
LeafletUILayerView,
11+
} from './Layer';
12+
13+
interface ILeafletTooltipModel extends ILeafletLayerModel {
14+
_view_name: string;
15+
_model_name: string;
16+
location: number[] | null;
17+
}
18+
19+
export class LeafletTooltipModel extends LeafletUILayerModel {
20+
defaults(): ILeafletTooltipModel {
21+
return {
22+
...super.defaults(),
23+
_view_name: 'LeafletMarkerView',
24+
_model_name: 'LeafletMarkerModel',
25+
location: null,
26+
};
27+
}
28+
}
29+
30+
export class LeafletTooltipView extends LeafletUILayerView {
31+
obj: Tooltip;
32+
33+
initialize(
34+
parameters: WidgetView.IInitializeParameters<LeafletTooltipModel>
35+
) {
36+
super.initialize(parameters);
37+
}
38+
39+
create_obj() {
40+
if (this.model.get('location')) {
41+
// Stand-alone tooltip
42+
this.obj = (L.tooltip as any)(
43+
this.model.get('location'),
44+
this.get_options() as TooltipOptions
45+
);
46+
} else {
47+
this.obj = L.tooltip(this.get_options() as TooltipOptions);
48+
}
49+
}
50+
51+
model_events() {
52+
super.model_events();
53+
this.listenTo(this.model, 'change:location', () => {
54+
if (this.model.get('location')) {
55+
this.obj.setLatLng(this.model.get('location'));
56+
this.send({
57+
event: 'move',
58+
location: this.model.get('location'),
59+
});
60+
}
61+
});
62+
this.listenTo(this.model, 'change:content', () => {
63+
this.obj.setContent(this.model.get('content'));
64+
});
65+
}
66+
}

0 commit comments

Comments
 (0)