diff --git a/docs/layers/tooltip.rst b/docs/layers/tooltip.rst
new file mode 100644
index 00000000..c0b9450e
--- /dev/null
+++ b/docs/layers/tooltip.rst
@@ -0,0 +1,70 @@
+Tooltip
+=====
+
+Example
+-------
+
+.. jupyter-execute::
+
+ from ipyleaflet import Map, Tooltip, Marker, Polygon, Circle
+
+ m = Map(center=(51.505,-0.09), zoom=13)
+
+ standalone_tooltip = Tooltip(
+ location=[51.5, -0.09],
+ content="Hello world!
This is a nice tooltip.",
+ offset=[-30,50], # Offset in pixels
+ permanent=False, # The default is False, in which case you can remove the tooltip by clicking anywhere on the map.
+ direction='bottom', # Default is 'auto'
+ )
+
+ marker_tooltip = Tooltip(
+ content="I'm a marker tooltip! π
Appears on hover.",
+ )
+
+ marker = Marker(
+ location=[51.5, -0.09],
+ draggable=False,
+ tooltip=marker_tooltip,
+ )
+
+ polygon = Polygon(
+ locations= [
+ [51.509, -0.08],
+ [51.503, -0.06],
+ [51.51, -0.047]
+ ])
+
+ polygon_tooltip = Tooltip(
+ content = "Polygon's Permanent Tooltip πΊοΈ",
+ permanent = True,
+ direction = 'center', # Centers the tooltip on the polygon
+ )
+
+ polygon.tooltip = polygon_tooltip
+
+ circle = Circle(
+ location = [51.515, -0.1],
+ radius = 500,
+ color = 'green',
+ fillColor = '#0f3',
+ fillOpacity = 0.5,
+ tooltip = Tooltip(
+ content = "Sticky Tooltip here! π
Stays with the mouse.",
+ sticky = True,
+ )
+ )
+
+ m.add(standalone_tooltip)
+ m.add(marker)
+ m.add(polygon)
+ m.add(circle)
+
+ m
+
+
+Attributes and methods
+----------------------
+
+.. autoclass:: ipyleaflet.leaflet.Tooltip
+ :members:
diff --git a/python/ipyleaflet/ipyleaflet/leaflet.py b/python/ipyleaflet/ipyleaflet/leaflet.py
index dc1fee5a..1c733503 100644
--- a/python/ipyleaflet/ipyleaflet/leaflet.py
+++ b/python/ipyleaflet/ipyleaflet/leaflet.py
@@ -175,6 +175,8 @@ class Layer(Widget, InteractMixin):
Make Leaflet-Geoman ignore the layer, so it cannot modify it.
snap_ignore: boolean
Make Leaflet-Geoman snapping ignore the layer, so it is not used as a snap target when editing.
+ tooltip: Tooltip widget
+ Tooltip widget to bind to the layer.
"""
_view_name = Unicode("LeafletLayerView").tag(sync=True)
@@ -196,6 +198,10 @@ class Layer(Widget, InteractMixin):
popup_max_height = Int(default_value=None, allow_none=True).tag(sync=True)
pane = Unicode("").tag(sync=True)
+ tooltip = Instance(Widget, allow_none=True, default_value=None).tag(
+ sync=True, **widget_serialization
+ )
+
options = List(trait=Unicode()).tag(sync=True)
subitems = Tuple().tag(trait=Instance(Widget), sync=True, **widget_serialization)
@@ -640,6 +646,43 @@ def close_popup(self):
self.send({"msg": "close"})
+class Tooltip(UILayer):
+ """Tooltip class.
+
+ Used to display small texts on top of map layers.
+
+ Attributes
+ ----------
+ location: tuple, default None
+ Optional tuple containing the latitude/longitude of the stand-alone tooltip.
+ content: str, default ""
+ The text to show inside the tooltip
+ offset: tuple, default (0, 0)
+ Optional offset of the tooltip position (in pixels).
+ direction: str, default 'auto'
+ Direction where to open the tooltip.
+ Possible values are: right, left, top, bottom, center, auto.
+ auto will dynamically switch between right and left according
+ to the tooltip position on the map.
+ permanent: bool, default False
+ Whether to open the tooltip permanently or only on mouseover.
+ sticky: bool, default False
+ If true, the tooltip will follow the mouse instead of being fixed at the feature center.
+ This option only applies when binding the tooltip to a Layer, not as stand-alone.
+ """
+ _view_name = Unicode("LeafletTooltipView").tag(sync=True)
+ _model_name = Unicode("LeafletTooltipModel").tag(sync=True)
+
+ location = List(allow_none=True, default_value=None).tag(sync=True)
+
+ # Options
+ content = Unicode('').tag(sync=True, o=True)
+ offset = List(def_loc).tag(sync=True, o=True)
+ direction=Unicode('auto').tag(sync=True, o=True)
+ permanent = Bool(False).tag(sync=True, o=True)
+ sticky = Bool(False).tag(sync=True, o=True)
+
+
class RasterLayer(Layer):
"""Abstract RasterLayer class.
diff --git a/python/jupyter_leaflet/src/jupyter-leaflet.ts b/python/jupyter_leaflet/src/jupyter-leaflet.ts
index acf10313..b35d939f 100644
--- a/python/jupyter_leaflet/src/jupyter-leaflet.ts
+++ b/python/jupyter_leaflet/src/jupyter-leaflet.ts
@@ -27,6 +27,7 @@ export * from './layers/Popup';
export * from './layers/RasterLayer';
export * from './layers/Rectangle';
export * from './layers/TileLayer';
+export * from './layers/Tooltip';
export * from './layers/VectorLayer';
export * from './layers/VectorTileLayer';
export * from './layers/Velocity';
diff --git a/python/jupyter_leaflet/src/layers/Layer.ts b/python/jupyter_leaflet/src/layers/Layer.ts
index be9b5861..fd3e1b13 100644
--- a/python/jupyter_leaflet/src/layers/Layer.ts
+++ b/python/jupyter_leaflet/src/layers/Layer.ts
@@ -9,7 +9,7 @@ import {
} from '@jupyter-widgets/base';
import { IMessageHandler, MessageLoop } from '@lumino/messaging';
import { Widget } from '@lumino/widgets';
-import { Control, Layer, LeafletMouseEvent, Popup } from 'leaflet';
+import { Control, Layer, LeafletMouseEvent, Popup, Tooltip } from 'leaflet';
import { LeafletControlView, LeafletMapView } from '../jupyter-leaflet';
import L from '../leaflet';
import { LeafletWidgetView } from '../utils';
@@ -29,6 +29,7 @@ export interface ILeafletLayerModel {
popup_max_width: number;
popup_max_height: number | null;
pane: string;
+ tooltip: WidgetModel | null;
subitems: Layer[];
pm_ignore: boolean;
snap_ignore: boolean;
@@ -55,6 +56,7 @@ export class LeafletLayerModel extends WidgetModel {
subitems: [],
pm_ignore: true,
snap_ignore: false,
+ tooltip: null,
};
}
}
@@ -62,6 +64,7 @@ export class LeafletLayerModel extends WidgetModel {
LeafletLayerModel.serializers = {
...WidgetModel.serializers,
popup: { deserialize: unpack_models },
+ tooltip: { deserialize: unpack_models },
subitems: { deserialize: unpack_models },
};
@@ -83,6 +86,8 @@ export class LeafletLayerView extends LeafletWidgetView {
obj: Layer;
subitems: WidgetModel[];
pWidget: IMessageHandler;
+ tooltip_content: LeafletLayerView;
+ tooltip_content_promise: Promise;
create_obj(): void {}
@@ -90,6 +95,7 @@ export class LeafletLayerView extends LeafletWidgetView {
super.initialize(parameters);
this.map_view = this.options.map_view;
this.popup_content_promise = Promise.resolve();
+ this.tooltip_content_promise = Promise.resolve();
}
remove_subitem_view(child_view: LeafletLayerView) {
@@ -128,6 +134,7 @@ export class LeafletLayerView extends LeafletWidgetView {
this.listenTo(this.model, 'change:popup', (model, value_2) => {
this.bind_popup(value_2);
});
+ this.bind_tooltip(this.model.get('tooltip'));
this.update_pane();
this.subitem_views = new ViewList(
this.add_subitem_model,
@@ -200,6 +207,9 @@ export class LeafletLayerView extends LeafletWidgetView {
this.listenTo(this.model, 'change:subitems', () => {
this.subitem_views.update(this.subitems);
});
+ this.listenTo(this.model, 'change:tooltip', () => {
+ this.bind_tooltip(this.model.get('tooltip'));
+ });
}
remove() {
@@ -210,6 +220,11 @@ export class LeafletLayerView extends LeafletWidgetView {
this.popup_content.remove();
}
});
+ this.tooltip_content_promise.then(() => {
+ if (this.tooltip_content) {
+ this.tooltip_content.remove();
+ }
+ });
}
bind_popup(value: WidgetModel) {
@@ -252,6 +267,27 @@ export class LeafletLayerView extends LeafletWidgetView {
this.obj.togglePopup();
this.obj.togglePopup();
}
+
+ bind_tooltip(value: WidgetModel) {
+ if (this.tooltip_content) {
+ this.obj.unbindTooltip();
+ this.tooltip_content.remove();
+ }
+ if (value) {
+ this.tooltip_content_promise = this.tooltip_content_promise.then(
+ async () => {
+ const view = await this.create_child_view(value, {
+ map_view: this.map_view,
+ });
+ if (view.obj instanceof Tooltip) {
+ this.obj.bindTooltip(view.obj);
+ }
+ this.tooltip_content = view;
+ }
+ );
+ }
+ return this.tooltip_content_promise;
+ }
}
export class LeafletUILayerView extends LeafletLayerView {}
diff --git a/python/jupyter_leaflet/src/layers/Tooltip.ts b/python/jupyter_leaflet/src/layers/Tooltip.ts
new file mode 100644
index 00000000..25eb16fa
--- /dev/null
+++ b/python/jupyter_leaflet/src/layers/Tooltip.ts
@@ -0,0 +1,66 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { WidgetView } from '@jupyter-widgets/base';
+import { Tooltip, TooltipOptions } from 'leaflet';
+import L from '../leaflet';
+import {
+ ILeafletLayerModel,
+ LeafletUILayerModel,
+ LeafletUILayerView,
+} from './Layer';
+
+interface ILeafletTooltipModel extends ILeafletLayerModel {
+ _view_name: string;
+ _model_name: string;
+ location: number[] | null;
+}
+
+export class LeafletTooltipModel extends LeafletUILayerModel {
+ defaults(): ILeafletTooltipModel {
+ return {
+ ...super.defaults(),
+ _view_name: 'LeafletMarkerView',
+ _model_name: 'LeafletMarkerModel',
+ location: null,
+ };
+ }
+}
+
+export class LeafletTooltipView extends LeafletUILayerView {
+ obj: Tooltip;
+
+ initialize(
+ parameters: WidgetView.IInitializeParameters
+ ) {
+ super.initialize(parameters);
+ }
+
+ create_obj() {
+ if (this.model.get('location')) {
+ // Stand-alone tooltip
+ this.obj = (L.tooltip as any)(
+ this.model.get('location'),
+ this.get_options() as TooltipOptions
+ );
+ } else {
+ this.obj = L.tooltip(this.get_options() as TooltipOptions);
+ }
+ }
+
+ model_events() {
+ super.model_events();
+ this.listenTo(this.model, 'change:location', () => {
+ if (this.model.get('location')) {
+ this.obj.setLatLng(this.model.get('location'));
+ this.send({
+ event: 'move',
+ location: this.model.get('location'),
+ });
+ }
+ });
+ this.listenTo(this.model, 'change:content', () => {
+ this.obj.setContent(this.model.get('content'));
+ });
+ }
+}