leaflet module¶
Leaflet map widget implementation.
LeafletMap (MapWidget)
¶
Interactive map widget using Leaflet.
This class provides a Python interface to Leaflet maps with full bidirectional communication through anywidget.
Note
Leaflet uses [lat, lng] order internally, but this class accepts [lng, lat] for consistency with other map libraries.
Examples:
>>> from anymap_ts import LeafletMap
>>> m = LeafletMap(center=[-122.4, 37.8], zoom=10)
>>> m.add_basemap("OpenStreetMap")
>>> m
Source code in anymap_ts/leaflet.py
class LeafletMap(MapWidget):
"""Interactive map widget using Leaflet.
This class provides a Python interface to Leaflet maps with
full bidirectional communication through anywidget.
Note:
Leaflet uses [lat, lng] order internally, but this class
accepts [lng, lat] for consistency with other map libraries.
Example:
>>> from anymap_ts import LeafletMap
>>> m = LeafletMap(center=[-122.4, 37.8], zoom=10)
>>> m.add_basemap("OpenStreetMap")
>>> m
"""
# ESM module for frontend
_esm = STATIC_DIR / "leaflet.js"
_css = STATIC_DIR / "leaflet.css"
# Layer tracking
_layer_dict = traitlets.Dict({}).tag(sync=True)
def __init__(
self,
center: Tuple[float, float] = (0.0, 0.0),
zoom: float = 2.0,
width: str = "100%",
height: str = "600px",
controls: Optional[Dict[str, Any]] = None,
**kwargs,
):
"""Initialize a Leaflet map.
Args:
center: Map center as (longitude, latitude).
zoom: Initial zoom level.
width: Map width as CSS string.
height: Map height as CSS string.
controls: Dict of controls to add (e.g., {"zoom": True}).
**kwargs: Additional widget arguments.
"""
super().__init__(
center=list(center),
zoom=zoom,
width=width,
height=height,
style="", # Leaflet doesn't use style URLs
**kwargs,
)
# Initialize layer dictionary
self._layer_dict = {"Background": []}
# Add default controls
if controls is None:
controls = {"zoom": True, "scale": True}
for control_name, config in controls.items():
if config:
self.add_control(
control_name, **(config if isinstance(config, dict) else {})
)
# -------------------------------------------------------------------------
# Basemap Methods
# -------------------------------------------------------------------------
def add_basemap(
self,
basemap: str = "OpenStreetMap",
attribution: Optional[str] = None,
**kwargs,
) -> None:
"""Add a basemap layer.
Args:
basemap: Name of basemap provider (e.g., "OpenStreetMap", "CartoDB.Positron").
attribution: Custom attribution text.
**kwargs: Additional options.
"""
try:
url, default_attribution = get_basemap_url(basemap)
except (ValueError, KeyError):
url = basemap
default_attribution = ""
self.call_js_method(
"addBasemap",
url,
attribution=attribution or default_attribution,
name=basemap,
**kwargs,
)
# Track in layer dict
basemaps = self._layer_dict.get("Basemaps", [])
if basemap not in basemaps:
self._layer_dict = {
**self._layer_dict,
"Basemaps": basemaps + [basemap],
}
# -------------------------------------------------------------------------
# Vector Data Methods
# -------------------------------------------------------------------------
def add_vector(
self,
data: Any,
style: Optional[Dict] = None,
name: Optional[str] = None,
fit_bounds: bool = True,
**kwargs,
) -> None:
"""Add vector data to the map.
Supports GeoJSON, GeoDataFrame, or file paths to vector formats.
Args:
data: GeoJSON dict, GeoDataFrame, or path to vector file.
style: Leaflet style properties.
name: Layer name.
fit_bounds: Whether to fit map to data bounds.
**kwargs: Additional layer options.
"""
geojson = to_geojson(data)
# Handle URL data
if geojson.get("type") == "url":
self.add_geojson(
geojson["url"],
style=style,
name=name,
fit_bounds=fit_bounds,
**kwargs,
)
return
layer_id = name or f"vector-{len(self._layers)}"
# Get default style if not provided
if style is None:
layer_type = _infer_leaflet_type(geojson)
style = _get_default_style(layer_type)
# Get bounds
bounds = get_bounds(data) if fit_bounds else None
# Call JavaScript
self.call_js_method(
"addGeoJSON",
data=geojson,
name=layer_id,
style=style,
fitBounds=fit_bounds,
bounds=bounds,
**kwargs,
)
# Track layer
self._layers = {
**self._layers,
layer_id: {
"id": layer_id,
"type": "geojson",
"style": style,
},
}
def add_geojson(
self,
data: Union[str, Dict],
style: Optional[Dict] = None,
name: Optional[str] = None,
fit_bounds: bool = True,
**kwargs,
) -> None:
"""Add GeoJSON data to the map.
Args:
data: GeoJSON dict or URL to GeoJSON file.
style: Leaflet style properties.
name: Layer name.
fit_bounds: Whether to fit map to data bounds.
**kwargs: Additional layer options.
"""
self.add_vector(
data,
style=style,
name=name,
fit_bounds=fit_bounds,
**kwargs,
)
# -------------------------------------------------------------------------
# Raster Data Methods
# -------------------------------------------------------------------------
def add_tile_layer(
self,
url: str,
name: Optional[str] = None,
attribution: str = "",
min_zoom: int = 0,
max_zoom: int = 22,
opacity: float = 1.0,
**kwargs,
) -> None:
"""Add an XYZ tile layer.
Args:
url: Tile URL template with {x}, {y}, {z} placeholders.
name: Layer name.
attribution: Attribution text.
min_zoom: Minimum zoom level.
max_zoom: Maximum zoom level.
opacity: Layer opacity.
**kwargs: Additional options.
"""
layer_id = name or f"tiles-{len(self._layers)}"
self.call_js_method(
"addTileLayer",
url,
name=layer_id,
attribution=attribution,
minZoom=min_zoom,
maxZoom=max_zoom,
opacity=opacity,
**kwargs,
)
# Track layer
self._layers = {
**self._layers,
layer_id: {
"id": layer_id,
"type": "tile",
},
}
# -------------------------------------------------------------------------
# Layer Management
# -------------------------------------------------------------------------
def remove_layer(self, layer_id: str) -> None:
"""Remove a layer from the map.
Args:
layer_id: Layer identifier to remove.
"""
if layer_id in self._layers:
layers = dict(self._layers)
del layers[layer_id]
self._layers = layers
self.call_js_method("removeLayer", layer_id)
def set_visibility(self, layer_id: str, visible: bool) -> None:
"""Set layer visibility.
Args:
layer_id: Layer identifier.
visible: Whether layer should be visible.
"""
self.call_js_method("setVisibility", layer_id, visible)
def set_opacity(self, layer_id: str, opacity: float) -> None:
"""Set layer opacity.
Args:
layer_id: Layer identifier.
opacity: Opacity value between 0 and 1.
"""
self.call_js_method("setOpacity", layer_id, opacity)
# -------------------------------------------------------------------------
# Controls
# -------------------------------------------------------------------------
def add_control(
self,
control_type: str,
position: str = "topright",
**kwargs,
) -> None:
"""Add a map control.
Args:
control_type: Type of control ('zoom', 'scale', 'attribution', 'layers').
position: Control position ('topleft', 'topright', 'bottomleft', 'bottomright').
**kwargs: Control-specific options.
"""
# Convert position format
position_map = {
"top-left": "topleft",
"top-right": "topright",
"bottom-left": "bottomleft",
"bottom-right": "bottomright",
}
pos = position_map.get(position, position)
self.call_js_method("addControl", control_type, position=pos, **kwargs)
self._controls = {
**self._controls,
control_type: {"type": control_type, "position": pos, **kwargs},
}
def remove_control(self, control_type: str) -> None:
"""Remove a map control.
Args:
control_type: Type of control to remove.
"""
self.call_js_method("removeControl", control_type)
if control_type in self._controls:
controls = dict(self._controls)
del controls[control_type]
self._controls = controls
def add_layer_control(
self,
position: str = "topright",
collapsed: bool = True,
) -> None:
"""Add a layer control for toggling layer visibility.
Args:
position: Control position.
collapsed: Whether control starts collapsed.
"""
self.add_control("layers", position=position, collapsed=collapsed)
# -------------------------------------------------------------------------
# Markers
# -------------------------------------------------------------------------
def add_marker(
self,
lng: float,
lat: float,
popup: Optional[str] = None,
marker_id: Optional[str] = None,
) -> None:
"""Add a marker to the map.
Args:
lng: Longitude.
lat: Latitude.
popup: HTML content for popup.
marker_id: Unique marker ID.
"""
self.call_js_method("addMarker", lng, lat, popup=popup, id=marker_id)
def remove_marker(self, marker_id: str) -> None:
"""Remove a marker from the map.
Args:
marker_id: Marker ID to remove.
"""
self.call_js_method("removeMarker", marker_id)
# -------------------------------------------------------------------------
# HTML Export
# -------------------------------------------------------------------------
def _generate_html_template(self) -> str:
"""Generate standalone HTML for the map."""
template_path = Path(__file__).parent / "templates" / "leaflet.html"
if template_path.exists():
template = template_path.read_text(encoding="utf-8")
else:
template = self._get_default_template()
# Serialize state
state = {
"center": self.center,
"zoom": self.zoom,
"width": self.width,
"height": self.height,
"layers": self._layers,
"controls": self._controls,
"js_calls": self._js_calls,
}
template = template.replace("{{state}}", json.dumps(state, indent=2))
return template
def _get_default_template(self) -> str:
"""Get default HTML template."""
return """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
const state = {{state}};
// Note: Leaflet uses [lat, lng], but we store [lng, lat]
const map = L.map('map').setView([state.center[1], state.center[0]], state.zoom);
// Replay JS calls
for (const call of state.js_calls || []) {
try {
executeMethod(call.method, call.args, call.kwargs);
} catch (e) {
console.error('Error executing', call.method, e);
}
}
function executeMethod(method, args, kwargs) {
switch (method) {
case 'addBasemap':
case 'addTileLayer':
const url = args[0];
L.tileLayer(url, {
attribution: kwargs.attribution || '',
maxZoom: kwargs.maxZoom || 22,
minZoom: kwargs.minZoom || 0,
opacity: kwargs.opacity || 1
}).addTo(map);
break;
case 'addGeoJSON':
const geojson = kwargs.data;
const style = kwargs.style || {
color: '#3388ff',
weight: 2,
opacity: 0.8,
fillOpacity: 0.5
};
const layer = L.geoJSON(geojson, {
style: style,
pointToLayer: (feature, latlng) => L.circleMarker(latlng, style)
}).addTo(map);
if (kwargs.fitBounds) {
map.fitBounds(layer.getBounds(), { padding: [50, 50] });
}
break;
case 'addControl':
const controlType = args[0];
const position = kwargs.position || 'topright';
if (controlType === 'zoom' || controlType === 'navigation') {
L.control.zoom({ position }).addTo(map);
} else if (controlType === 'scale') {
L.control.scale({ position, imperial: false }).addTo(map);
}
break;
case 'addMarker':
const [lng, lat] = args;
const marker = L.marker([lat, lng]).addTo(map);
if (kwargs.popup) {
marker.bindPopup(kwargs.popup);
}
break;
case 'flyTo':
map.flyTo([args[1], args[0]], kwargs.zoom || map.getZoom(), {
duration: (kwargs.duration || 2000) / 1000
});
break;
case 'fitBounds':
const bounds = args[0];
map.fitBounds([
[bounds[1], bounds[0]],
[bounds[3], bounds[2]]
], { padding: [kwargs.padding || 50, kwargs.padding || 50] });
break;
default:
console.log('Unknown method:', method);
}
}
</script>
</body>
</html>"""
__init__(self, center=(0.0, 0.0), zoom=2.0, width='100%', height='600px', controls=None, **kwargs)
special
¶
Initialize a Leaflet map.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
center |
Tuple[float, float] |
Map center as (longitude, latitude). |
(0.0, 0.0) |
zoom |
float |
Initial zoom level. |
2.0 |
width |
str |
Map width as CSS string. |
'100%' |
height |
str |
Map height as CSS string. |
'600px' |
controls |
Optional[Dict[str, Any]] |
Dict of controls to add (e.g., {"zoom": True}). |
None |
**kwargs |
Additional widget arguments. |
{} |
Source code in anymap_ts/leaflet.py
def __init__(
self,
center: Tuple[float, float] = (0.0, 0.0),
zoom: float = 2.0,
width: str = "100%",
height: str = "600px",
controls: Optional[Dict[str, Any]] = None,
**kwargs,
):
"""Initialize a Leaflet map.
Args:
center: Map center as (longitude, latitude).
zoom: Initial zoom level.
width: Map width as CSS string.
height: Map height as CSS string.
controls: Dict of controls to add (e.g., {"zoom": True}).
**kwargs: Additional widget arguments.
"""
super().__init__(
center=list(center),
zoom=zoom,
width=width,
height=height,
style="", # Leaflet doesn't use style URLs
**kwargs,
)
# Initialize layer dictionary
self._layer_dict = {"Background": []}
# Add default controls
if controls is None:
controls = {"zoom": True, "scale": True}
for control_name, config in controls.items():
if config:
self.add_control(
control_name, **(config if isinstance(config, dict) else {})
)
add_basemap(self, basemap='OpenStreetMap', attribution=None, **kwargs)
¶
Add a basemap layer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
basemap |
str |
Name of basemap provider (e.g., "OpenStreetMap", "CartoDB.Positron"). |
'OpenStreetMap' |
attribution |
Optional[str] |
Custom attribution text. |
None |
**kwargs |
Additional options. |
{} |
Source code in anymap_ts/leaflet.py
def add_basemap(
self,
basemap: str = "OpenStreetMap",
attribution: Optional[str] = None,
**kwargs,
) -> None:
"""Add a basemap layer.
Args:
basemap: Name of basemap provider (e.g., "OpenStreetMap", "CartoDB.Positron").
attribution: Custom attribution text.
**kwargs: Additional options.
"""
try:
url, default_attribution = get_basemap_url(basemap)
except (ValueError, KeyError):
url = basemap
default_attribution = ""
self.call_js_method(
"addBasemap",
url,
attribution=attribution or default_attribution,
name=basemap,
**kwargs,
)
# Track in layer dict
basemaps = self._layer_dict.get("Basemaps", [])
if basemap not in basemaps:
self._layer_dict = {
**self._layer_dict,
"Basemaps": basemaps + [basemap],
}
add_control(self, control_type, position='topright', **kwargs)
¶
Add a map control.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
control_type |
str |
Type of control ('zoom', 'scale', 'attribution', 'layers'). |
required |
position |
str |
Control position ('topleft', 'topright', 'bottomleft', 'bottomright'). |
'topright' |
**kwargs |
Control-specific options. |
{} |
Source code in anymap_ts/leaflet.py
def add_control(
self,
control_type: str,
position: str = "topright",
**kwargs,
) -> None:
"""Add a map control.
Args:
control_type: Type of control ('zoom', 'scale', 'attribution', 'layers').
position: Control position ('topleft', 'topright', 'bottomleft', 'bottomright').
**kwargs: Control-specific options.
"""
# Convert position format
position_map = {
"top-left": "topleft",
"top-right": "topright",
"bottom-left": "bottomleft",
"bottom-right": "bottomright",
}
pos = position_map.get(position, position)
self.call_js_method("addControl", control_type, position=pos, **kwargs)
self._controls = {
**self._controls,
control_type: {"type": control_type, "position": pos, **kwargs},
}
add_geojson(self, data, style=None, name=None, fit_bounds=True, **kwargs)
¶
Add GeoJSON data to the map.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data |
Union[str, Dict] |
GeoJSON dict or URL to GeoJSON file. |
required |
style |
Optional[Dict] |
Leaflet style properties. |
None |
name |
Optional[str] |
Layer name. |
None |
fit_bounds |
bool |
Whether to fit map to data bounds. |
True |
**kwargs |
Additional layer options. |
{} |
Source code in anymap_ts/leaflet.py
def add_geojson(
self,
data: Union[str, Dict],
style: Optional[Dict] = None,
name: Optional[str] = None,
fit_bounds: bool = True,
**kwargs,
) -> None:
"""Add GeoJSON data to the map.
Args:
data: GeoJSON dict or URL to GeoJSON file.
style: Leaflet style properties.
name: Layer name.
fit_bounds: Whether to fit map to data bounds.
**kwargs: Additional layer options.
"""
self.add_vector(
data,
style=style,
name=name,
fit_bounds=fit_bounds,
**kwargs,
)
add_layer_control(self, position='topright', collapsed=True)
¶
Add a layer control for toggling layer visibility.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
position |
str |
Control position. |
'topright' |
collapsed |
bool |
Whether control starts collapsed. |
True |
Source code in anymap_ts/leaflet.py
def add_layer_control(
self,
position: str = "topright",
collapsed: bool = True,
) -> None:
"""Add a layer control for toggling layer visibility.
Args:
position: Control position.
collapsed: Whether control starts collapsed.
"""
self.add_control("layers", position=position, collapsed=collapsed)
add_marker(self, lng, lat, popup=None, marker_id=None)
¶
Add a marker to the map.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lng |
float |
Longitude. |
required |
lat |
float |
Latitude. |
required |
popup |
Optional[str] |
HTML content for popup. |
None |
marker_id |
Optional[str] |
Unique marker ID. |
None |
Source code in anymap_ts/leaflet.py
def add_marker(
self,
lng: float,
lat: float,
popup: Optional[str] = None,
marker_id: Optional[str] = None,
) -> None:
"""Add a marker to the map.
Args:
lng: Longitude.
lat: Latitude.
popup: HTML content for popup.
marker_id: Unique marker ID.
"""
self.call_js_method("addMarker", lng, lat, popup=popup, id=marker_id)
add_tile_layer(self, url, name=None, attribution='', min_zoom=0, max_zoom=22, opacity=1.0, **kwargs)
¶
Add an XYZ tile layer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url |
str |
Tile URL template with {x}, {y}, {z} placeholders. |
required |
name |
Optional[str] |
Layer name. |
None |
attribution |
str |
Attribution text. |
'' |
min_zoom |
int |
Minimum zoom level. |
0 |
max_zoom |
int |
Maximum zoom level. |
22 |
opacity |
float |
Layer opacity. |
1.0 |
**kwargs |
Additional options. |
{} |
Source code in anymap_ts/leaflet.py
def add_tile_layer(
self,
url: str,
name: Optional[str] = None,
attribution: str = "",
min_zoom: int = 0,
max_zoom: int = 22,
opacity: float = 1.0,
**kwargs,
) -> None:
"""Add an XYZ tile layer.
Args:
url: Tile URL template with {x}, {y}, {z} placeholders.
name: Layer name.
attribution: Attribution text.
min_zoom: Minimum zoom level.
max_zoom: Maximum zoom level.
opacity: Layer opacity.
**kwargs: Additional options.
"""
layer_id = name or f"tiles-{len(self._layers)}"
self.call_js_method(
"addTileLayer",
url,
name=layer_id,
attribution=attribution,
minZoom=min_zoom,
maxZoom=max_zoom,
opacity=opacity,
**kwargs,
)
# Track layer
self._layers = {
**self._layers,
layer_id: {
"id": layer_id,
"type": "tile",
},
}
add_vector(self, data, style=None, name=None, fit_bounds=True, **kwargs)
¶
Add vector data to the map.
Supports GeoJSON, GeoDataFrame, or file paths to vector formats.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data |
Any |
GeoJSON dict, GeoDataFrame, or path to vector file. |
required |
style |
Optional[Dict] |
Leaflet style properties. |
None |
name |
Optional[str] |
Layer name. |
None |
fit_bounds |
bool |
Whether to fit map to data bounds. |
True |
**kwargs |
Additional layer options. |
{} |
Source code in anymap_ts/leaflet.py
def add_vector(
self,
data: Any,
style: Optional[Dict] = None,
name: Optional[str] = None,
fit_bounds: bool = True,
**kwargs,
) -> None:
"""Add vector data to the map.
Supports GeoJSON, GeoDataFrame, or file paths to vector formats.
Args:
data: GeoJSON dict, GeoDataFrame, or path to vector file.
style: Leaflet style properties.
name: Layer name.
fit_bounds: Whether to fit map to data bounds.
**kwargs: Additional layer options.
"""
geojson = to_geojson(data)
# Handle URL data
if geojson.get("type") == "url":
self.add_geojson(
geojson["url"],
style=style,
name=name,
fit_bounds=fit_bounds,
**kwargs,
)
return
layer_id = name or f"vector-{len(self._layers)}"
# Get default style if not provided
if style is None:
layer_type = _infer_leaflet_type(geojson)
style = _get_default_style(layer_type)
# Get bounds
bounds = get_bounds(data) if fit_bounds else None
# Call JavaScript
self.call_js_method(
"addGeoJSON",
data=geojson,
name=layer_id,
style=style,
fitBounds=fit_bounds,
bounds=bounds,
**kwargs,
)
# Track layer
self._layers = {
**self._layers,
layer_id: {
"id": layer_id,
"type": "geojson",
"style": style,
},
}
remove_control(self, control_type)
¶
Remove a map control.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
control_type |
str |
Type of control to remove. |
required |
Source code in anymap_ts/leaflet.py
def remove_control(self, control_type: str) -> None:
"""Remove a map control.
Args:
control_type: Type of control to remove.
"""
self.call_js_method("removeControl", control_type)
if control_type in self._controls:
controls = dict(self._controls)
del controls[control_type]
self._controls = controls
remove_layer(self, layer_id)
¶
Remove a layer from the map.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer_id |
str |
Layer identifier to remove. |
required |
Source code in anymap_ts/leaflet.py
def remove_layer(self, layer_id: str) -> None:
"""Remove a layer from the map.
Args:
layer_id: Layer identifier to remove.
"""
if layer_id in self._layers:
layers = dict(self._layers)
del layers[layer_id]
self._layers = layers
self.call_js_method("removeLayer", layer_id)
remove_marker(self, marker_id)
¶
Remove a marker from the map.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
marker_id |
str |
Marker ID to remove. |
required |
Source code in anymap_ts/leaflet.py
def remove_marker(self, marker_id: str) -> None:
"""Remove a marker from the map.
Args:
marker_id: Marker ID to remove.
"""
self.call_js_method("removeMarker", marker_id)
set_opacity(self, layer_id, opacity)
¶
Set layer opacity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer_id |
str |
Layer identifier. |
required |
opacity |
float |
Opacity value between 0 and 1. |
required |
Source code in anymap_ts/leaflet.py
def set_opacity(self, layer_id: str, opacity: float) -> None:
"""Set layer opacity.
Args:
layer_id: Layer identifier.
opacity: Opacity value between 0 and 1.
"""
self.call_js_method("setOpacity", layer_id, opacity)
set_visibility(self, layer_id, visible)
¶
Set layer visibility.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer_id |
str |
Layer identifier. |
required |
visible |
bool |
Whether layer should be visible. |
required |
Source code in anymap_ts/leaflet.py
def set_visibility(self, layer_id: str, visible: bool) -> None:
"""Set layer visibility.
Args:
layer_id: Layer identifier.
visible: Whether layer should be visible.
"""
self.call_js_method("setVisibility", layer_id, visible)