Playing with anymap-ts + Panel

I came across anymap-ts recently and had to try it with Panel. It’s an anywidget-based Python package that gives you a unified API across 8 different mapping libraries:

Backend Type Notes
MapLibre Vector tiles Free, GPU-accelerated, 3D pitch
Leaflet Raster tiles Classic, lightweight
DeckGL GPU layers Large-scale data viz
OpenLayers Full-featured OGC standards support
Mapbox Vector tiles Requires API token
Cesium 3D globe Requires ion token
KeplerGL Geospatial analytics Uber’s vis framework
Potree Point clouds LiDAR visualization

The cool part: you write Map(center=..., zoom=...) once, and swap backends by changing the class. Draw controls, basemaps, GeoJSON, arc layers — same API everywhere.

Installation

pip install panel watchfiles ipywidgets_bokeh setuptools anymap-ts

Note: setuptools is needed until ipywidgets_bokeh#125 has been resolved.

Getting Started

Here’s a minimal example — a MapLibre map of San Francisco with draw tools:

import panel as pn
from anymap_ts import Map

pn.extension("ipywidgets", sizing_mode="stretch_width")

m = Map(center=[-122.4194, 37.7749], zoom=12)
m.add_basemap("OpenStreetMap")
m.add_draw_control()
m.add_layer_control()

map_pane = pn.pane.IPyWidget(m, sizing_mode="stretch_width", height=600)

pn.Column(
    "# anymap-ts with Panel",
    "Drag to pan, scroll to zoom, and use the draw tools on the left.",
    map_pane,
    sizing_mode="stretch_width",
).servable()

Run it with panel serve anymap_panel_getting_started.py --dev — you get a MapLibre map with drawing tools and layer control.

A Bigger Example: Multi-Backend Flight Map

This is where anymap-ts really shines. The same flight-route data rendered on three different backends, switchable with a Panel dropdown:

  • MapLibre / DeckGL — arc layers showing flight routes from SFO with 3D pitch
  • Leaflet — markers at each destination city

The app uses a FastListTemplate with a dark theme for a polished look, sidebar controls, a dynamic status bar showing the current settings, and a theme toggle.

Full code (click to expand)
import panel as pn
from anymap_ts import DeckGLMap, LeafletMap, MapLibreMap

pn.extension("ipywidgets", sizing_mode="stretch_width", throttled=True)

# Flight routes from San Francisco to major world cities
SFO = [-122.4194, 37.7749]
DESTINATIONS = {
    "New York": [-73.9857, 40.7484],
    "London": [-0.1278, 51.5074],
    "Tokyo": [139.6917, 35.6895],
    "Sydney": [151.2093, -33.8688],
    "Dubai": [55.2708, 25.2048],
    "Sao Paulo": [-46.6333, -23.5505],
}

ARC_DATA = [
    {"source": SFO, "target": dest, "name": name}
    for name, dest in DESTINATIONS.items()
]

# -- Widgets --
backend_select = pn.widgets.Select(
    name="Map Backend",
    options=["MapLibre", "Leaflet", "DeckGL"],
    value="MapLibre",
)
basemap_select = pn.widgets.Select(
    name="Basemap",
    options=["CartoDB.DarkMatter", "OpenStreetMap"],
    value="CartoDB.DarkMatter",
)
zoom_slider = pn.widgets.FloatSlider(
    name="Zoom", start=1, end=15, step=0.5, value=2,
)
pitch_slider = pn.widgets.IntSlider(
    name="Pitch", start=0, end=60, step=5, value=30,
)

SOURCE_COLOR = [0, 180, 235, 200]
TARGET_COLOR = [255, 100, 50, 200]


def create_map(backend, basemap, zoom, pitch):
    """Create a map widget for the selected backend with flight data."""
    kwargs = dict(center=SFO, zoom=zoom)

    if backend == "MapLibre":
        m = MapLibreMap(**kwargs, pitch=pitch)
        m.add_basemap(basemap)
        m.add_arc_layer(
            ARC_DATA,
            name="flights",
            get_source_position="source",
            get_target_position="target",
            get_source_color=SOURCE_COLOR,
            get_target_color=TARGET_COLOR,
            get_width=2,
        )
    elif backend == "DeckGL":
        m = DeckGLMap(**kwargs, pitch=pitch)
        m.add_basemap(basemap)
        m.add_arc_layer(
            ARC_DATA,
            name="flights",
            get_source_position="source",
            get_target_position="target",
            get_source_color=SOURCE_COLOR,
            get_target_color=TARGET_COLOR,
            get_width=2,
        )
    else:  # Leaflet
        m = LeafletMap(**kwargs)
        m.add_basemap(basemap)
        for route in ARC_DATA:
            lng, lat = route["target"]
            m.add_marker(lng, lat, popup=route["name"])

    return pn.pane.IPyWidget(m, sizing_mode="stretch_width", height=550)


map_pane = pn.bind(
    create_map,
    backend=backend_select,
    basemap=basemap_select,
    zoom=zoom_slider,
    pitch=pitch_slider,
)


def status_text(backend, basemap, zoom, pitch):
    return (
        f"**Backend:** {backend} · **Basemap:** {basemap} · "
        f"**Zoom:** {round(zoom,2)} · **Pitch:** {pitch}°"
    )


status_md = pn.pane.Markdown(
    pn.bind(status_text, backend_select, basemap_select, zoom_slider, pitch_slider),
    styles={"font-size": "18px"},
)

sidebar_info = pn.pane.Markdown(
    "**Backends**\n\n"
    "- **MapLibre / DeckGL**: Arc layers show flight routes from SFO\n"
    "- **Leaflet**: Markers at each destination\n\n"
    "*Pitch only affects MapLibre & DeckGL.*"
)

pn.template.FastListTemplate(
    title="anymap-ts Multi-Backend Showcase",
    sidebar=[backend_select, basemap_select, zoom_slider, pitch_slider, sidebar_info],
    main=[status_md, map_pane],
    accent_base_color="#0072B5",
    theme="dark",
    theme_toggle=True,
    main_layout=None,
).servable()

The integration pattern is the same one used in the PyGlobeGL showcase: pn.extension("ipywidgets") + pn.pane.IPyWidget(widget). Since anymap-ts is built on anywidget, which implements the ipywidgets protocol, it just works.

pn.bind() recreates the map whenever a widget changes — necessary since switching backends means creating a new widget instance. The throttled=True option in pn.extension() ensures sliders only fire on mouse-up, not on every pixel drag.

Links

1 Like