Skip to content

map

map

Use Mapyta to generate beatiful interactive maps.

We are standing on the shoulders of giants by using Folium as the underlying mapping library, which provides a Python interface to Leaflet.js and OpenStreetMap tiles.

Provides a high-level API for creating interactive OpenStreetMap visualizations with support for Shapely geometries, GeoPandas DataFrames, emoji/icon markers, text annotations, markdown tooltip text, choropleth coloring, heatmaps, and export to HTML, PNG, and SVG formats.

Examples:

>>> from shapely.geometry import Point, Polygon
>>> from mapyta import Map
>>> m = Map(title="My Map")
>>> m.add_point(Point(4.9, 52.37), marker="πŸ“", tooltip="**Amsterdam**")
>>> m.add_polygon(Polygon([(4.9, 52.3), (5.0, 52.3), (5.0, 52.4), (4.9, 52.4)]))
>>> m.to_html("map.html")

Classes:

  • Map –

    Interactive map builder backed by Folium and OpenStreetMap tiles.

map.Map

Map(
    center: tuple[float, float] | None = None,
    title: str | None = None,
    config: MapConfig | None = None,
    source_crs: str | None = None,
)

Interactive map builder backed by Folium and OpenStreetMap tiles.

Parameters:

  • center (tuple[float, float] | None, default: None ) –

    Map center (latitude, longitude). Auto-fits if None.

  • title (str | None, default: None ) –

    Title rendered at the top of the map.

  • config (MapConfig | None, default: None ) –

    Global map configuration.

  • source_crs (str | None, default: None ) –

    Default source CRS (e.g. "EPSG:28992"). Auto-detects if None.

Examples:

>>> m = Map(title="Demo")
>>> m.add_point(Point(5.0, 52.0), tooltip="**Hello**")
>>> m.to_html("demo.html")
Source code in mapyta/map.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def __init__(
    self,
    center: tuple[float, float] | None = None,
    title: str | None = None,
    config: MapConfig | None = None,
    source_crs: str | None = None,
) -> None:
    self._config = config or MapConfig()
    self._center = center
    self._title = title
    self._source_crs = source_crs
    self._map = self._create_base_map()
    self._bounds: list[tuple[float, float]] = []
    self._feature_groups: dict[str, folium.FeatureGroup] = {}
    self._active_group: folium.FeatureGroup | folium.Map = self._map
    self._colormaps: list[cm.LinearColormap | cm.StepColormap] = []
    self._zoom_controlled_markers: list[dict[str, Any]] = []
    self._zoom_controlled_captions: list[dict[str, Any]] = []
    self._zoom_js_injected: bool = False
    self._draw_config: DrawConfig | None = None
    self._draw_injected: bool = False
    self._geojson_features: list[dict] = []
    self._export_button_config: dict[str, Any] | None = None
    self._export_button_injected: bool = False
    self._layer_dropdown_config: dict[str, Any] | None = None
    self._layer_dropdown_injected: bool = False

map.Map.folium_map property

folium_map: Map

Access the underlying Folium Map for advanced customization.

map.Map.add_ant_path

add_ant_path(
    line: LineString | list[Point],
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    color: str = "#0000FF",
    pulse_color: str = "#FFFFFF",
    weight: int = 5,
    delay: int = 400,
    dash_array: list[int] | None = None,
    paused: bool = False,
    reverse: bool = False,
    popup_style: PopupStyle | dict[str, Any] | None = None,
) -> Self

Add an animated dashed line (ant path) along a route.

The path is drawn as a marching-ants animation: a dashed line whose gaps appear to travel in the direction of travel.

Parameters:

  • line (LineString | list[Point]) –

    Route geometry. Pass a Shapely LineString or a list of Shapely Point objects as waypoints.

  • tooltip (str | RawHTML | None, default: None ) –

    Markdown tooltip, or RawHTML for pre-formatted HTML.

  • popup (str | RawHTML | None, default: None ) –

    Markdown popup, or RawHTML for pre-formatted HTML.

  • color (str, default: '#0000FF' ) –

    Line colour (CSS hex or name).

  • pulse_color (str, default: '#FFFFFF' ) –

    Colour of the travelling pulse / gap fill.

  • weight (int, default: 5 ) –

    Line width in pixels.

  • delay (int, default: 400 ) –

    Animation step interval in milliseconds. Lower = faster.

  • dash_array (list[int] | None, default: None ) –

    [dash_length, gap_length] pattern in pixels. Defaults to [10, 20] when None.

  • paused (bool, default: False ) –

    Start the animation paused.

  • reverse (bool, default: False ) –

    Reverse the direction of travel.

  • popup_style (PopupStyle | dict[str, Any] | None, default: None ) –

    Popup dimensions.

Returns:

Source code in mapyta/map.py
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
def add_ant_path(  # noqa: PLR0913
    self,
    line: LineString | list[Point],
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    color: str = "#0000FF",
    pulse_color: str = "#FFFFFF",
    weight: int = 5,
    delay: int = 400,
    dash_array: list[int] | None = None,
    paused: bool = False,
    reverse: bool = False,
    popup_style: PopupStyle | dict[str, Any] | None = None,
) -> Self:
    """Add an animated dashed line (ant path) along a route.

    The path is drawn as a marching-ants animation: a dashed line
    whose gaps appear to travel in the direction of travel.

    Parameters
    ----------
    line : LineString | list[Point]
        Route geometry.  Pass a Shapely ``LineString`` or a list of
        Shapely ``Point`` objects as waypoints.
    tooltip : str | RawHTML | None
        Markdown tooltip, or ``RawHTML`` for pre-formatted HTML.
    popup : str | RawHTML | None
        Markdown popup, or ``RawHTML`` for pre-formatted HTML.
    color : str
        Line colour (CSS hex or name).
    pulse_color : str
        Colour of the travelling pulse / gap fill.
    weight : int
        Line width in pixels.
    delay : int
        Animation step interval in milliseconds.  Lower = faster.
    dash_array : list[int] | None
        ``[dash_length, gap_length]`` pattern in pixels.
        Defaults to ``[10, 20]`` when ``None``.
    paused : bool
        Start the animation paused.
    reverse : bool
        Reverse the direction of travel.
    popup_style : PopupStyle | dict[str, Any] | None
        Popup dimensions.

    Returns
    -------
    Map
    """
    if isinstance(line, LineString):
        transformed = cast(LineString, self._transform(line))
        self._extend_bounds(transformed)
        locations = [(c[1], c[0]) for c in transformed.coords]
        self._record_feature(
            transformed,
            {"color": color, "tooltip": self._raw_text(tooltip), "popup": self._raw_text(popup)},
        )
    else:
        locations = []
        for pt in line:
            t = cast(Point, self._transform(pt))
            self._extend_bounds(t)
            locations.append((t.y, t.x))
            self._record_feature(t, {"color": color})

    kwargs: dict[str, Any] = {
        "color": color,
        "pulseColor": pulse_color,
        "weight": weight,
        "delay": delay,
        "paused": paused,
        "reverse": reverse,
    }
    if dash_array is not None:
        kwargs["dashArray"] = dash_array

    folium.plugins.AntPath(
        locations=locations,
        tooltip=self._make_tooltip(tooltip),
        popup=self._make_popup(popup, popup_style),
        **kwargs,
    ).add_to(self._target())
    return self

map.Map.add_choropleth

add_choropleth(
    geojson_data: dict | str | Path,
    value_column: str,
    key_on: str,
    values: Mapping[str, float | str] | None = None,
    vmin: float | None = None,
    vmax: float | None = None,
    legend_name: str | None = None,
    nan_fill_color: str = "#cccccc",
    nan_fill_opacity: float = 0.4,
    line_weight: float = 1.0,
    line_opacity: float = 0.5,
    fill_opacity: float = 0.7,
    hover_fields: list[str] | None = None,
    colors: list[str] | str | None = None,
    categorical: bool | None = None,
) -> Self

Add a choropleth (color-coded) layer.

Parameters:

  • geojson_data (dict | str | Path) –

    GeoJSON FeatureCollection.

  • value_column (str) –

    Property name with numeric or categorical values.

  • key_on (str) –

    Join key, e.g. "feature.properties.id".

  • values (dict[str, float | str] | None, default: None ) –

    Key -> value mapping. Reads from properties if None.

  • vmin (float | None, default: None ) –

    Color scale range for numeric data. Auto-calculated if None.

  • vmax (float | None, default: None ) –

    Color scale range for numeric data. Auto-calculated if None.

  • legend_name (str | None, default: None ) –

    Legend label.

  • nan_fill_color (str, default: '#cccccc' ) –

    Color for missing values.

  • nan_fill_opacity (float, default: 0.4 ) –

    Opacity for missing values.

  • line_weight (float, default: 1.0 ) –

    Styling parameters.

  • line_opacity (float, default: 1.0 ) –

    Styling parameters.

  • fill_opacity (float, default: 1.0 ) –

    Styling parameters.

  • hover_fields (list[str] | None, default: None ) –

    Tooltip property fields.

  • colors (list[str] | str | None, default: None ) –

    Color palette. Pass a palette name (e.g. "blues", "viridis") or a list of hex color strings. See mapyta.PALETTES for available names. Defaults to "ylrd" (yellow β†’ red).

  • categorical (bool | None, default: None ) –

    Force categorical mode (True), numeric mode (False), or auto-detect from values (None). In categorical mode, each unique value gets a distinct color from the palette.

Returns:

Source code in mapyta/map.py
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
def add_choropleth(  # noqa: C901, PLR0913, PLR0912, PLR0915
    self,
    geojson_data: dict | str | Path,
    value_column: str,
    key_on: str,
    values: Mapping[str, float | str] | None = None,
    vmin: float | None = None,
    vmax: float | None = None,
    legend_name: str | None = None,
    nan_fill_color: str = "#cccccc",
    nan_fill_opacity: float = 0.4,
    line_weight: float = 1.0,
    line_opacity: float = 0.5,
    fill_opacity: float = 0.7,
    hover_fields: list[str] | None = None,
    colors: list[str] | str | None = None,
    categorical: bool | None = None,
) -> Self:
    """Add a choropleth (color-coded) layer.

    Parameters
    ----------
    geojson_data : dict | str | Path
        GeoJSON FeatureCollection.
    value_column : str
        Property name with numeric or categorical values.
    key_on : str
        Join key, e.g. ``"feature.properties.id"``.
    values : dict[str, float | str] | None
        Key -> value mapping. Reads from properties if ``None``.
    vmin, vmax : float | None
        Color scale range for numeric data. Auto-calculated if ``None``.
    legend_name : str | None
        Legend label.
    nan_fill_color : str
        Color for missing values.
    nan_fill_opacity : float
        Opacity for missing values.
    line_weight, line_opacity, fill_opacity : float
        Styling parameters.
    hover_fields : list[str] | None
        Tooltip property fields.
    colors : list[str] | str | None
        Color palette. Pass a palette name (e.g. ``"blues"``, ``"viridis"``) or
        a list of hex color strings. See ``mapyta.PALETTES`` for available names.
        Defaults to ``"ylrd"`` (yellow β†’ red).
    categorical : bool | None
        Force categorical mode (``True``), numeric mode (``False``), or auto-detect
        from values (``None``). In categorical mode, each unique value gets a
        distinct color from the palette.

    Returns
    -------
    Map
    """
    geojson_data = load_geojson_input(geojson_data)
    if geojson_data.get("type") == "FeatureCollection":
        self._geojson_features.extend(geojson_data.get("features", []))
    elif geojson_data.get("type") == "Feature":
        self._geojson_features.append(geojson_data)

    # Extract values if not provided
    if values is None:
        extracted: dict[str, float | str] = {}
        key_parts = key_on.split(".")
        for feat in geojson_data.get("features", []):
            obj = feat
            for part in key_parts[1:]:
                obj = obj.get(part, {})
            key = obj if isinstance(obj, str) else str(obj)
            raw_val = feat.get("properties", {}).get(value_column)
            if raw_val is not None:
                extracted[key] = raw_val
        values = extracted

    # Determine if categorical
    raw_vals = list(values.values())
    is_categorical = categorical if categorical is not None else any(isinstance(v, str) for v in raw_vals)

    caption = legend_name or value_column

    if is_categorical:
        # Build discrete color mapping per unique category
        categories = list(dict.fromkeys(str(v) for v in raw_vals))
        palette_colors = self._resolve_colors(colors)
        # Cycle colors if more categories than palette entries
        cat_color_map: dict[str, str] = {cat: palette_colors[i % len(palette_colors)] for i, cat in enumerate(categories)}

        # Use the actual cycled colors per category so the legend matches the map
        legend_colors = [cat_color_map[cat] for cat in categories] if categories else palette_colors[:1]
        colormap = cm.StepColormap(
            colors=legend_colors,
            vmin=0,
            vmax=max(len(categories) - 1, 1),
            caption=caption,
        )

        _str_vals = {k: str(v) for k, v in values.items()}
        _cat_map = cat_color_map

        def style_fn(feature: dict) -> dict:
            obj = feature
            for part in key_on.split(".")[1:]:
                obj = obj.get(part, {})
            key = obj if isinstance(obj, str) else str(obj)
            val_str = _str_vals.get(key)
            fill_color = _cat_map.get(val_str, nan_fill_color) if val_str is not None else nan_fill_color
            return {"fillColor": fill_color, "color": "#333", "weight": line_weight, "fillOpacity": fill_opacity, "opacity": line_opacity}

    else:
        # Numeric mode
        num_vals: list[float] = []
        for v in raw_vals:
            if isinstance(v, str):
                continue
            try:
                num_vals.append(float(v))
            except (TypeError, ValueError) as exc:
                raise ValueError(f"Non-numeric value {v!r} in choropleth values cannot be converted to float") from exc
        if num_vals:
            vmin = vmin if vmin is not None else min(num_vals)
            vmax = vmax if vmax is not None else max(num_vals)

        colormap = self._build_colormap(
            colors=colors,
            vmin=vmin if vmin is not None else 0,
            vmax=vmax if vmax is not None else 1,
            caption=caption,
        )

        _num_vals = {k: float(v) for k, v in values.items() if not isinstance(v, str)}
        _cmap = colormap

        def style_fn(feature: dict) -> dict:
            obj = feature
            for part in key_on.split(".")[1:]:
                obj = obj.get(part, {})
            key = obj if isinstance(obj, str) else str(obj)
            val = _num_vals.get(key)
            if val is not None:
                return {"fillColor": _cmap(val), "color": "#333", "weight": line_weight, "fillOpacity": fill_opacity, "opacity": line_opacity}
            return {"fillColor": nan_fill_color, "color": "#333", "weight": line_weight, "fillOpacity": nan_fill_opacity, "opacity": line_opacity}

    tooltip = folium.GeoJsonTooltip(fields=hover_fields, localize=True) if hover_fields else None
    layer = folium.GeoJson(
        geojson_data,
        style_function=style_fn,
        highlight_function=lambda _: {"weight": 3, "fillOpacity": min(fill_opacity + 0.15, 1.0)},
        tooltip=tooltip,
    )
    layer.add_to(self._target())
    self._register_colormap(colormap)

    try:
        layer_bounds = layer.get_bounds()
        if layer_bounds:
            self._bounds.extend(cast(list[tuple[float, float]], layer_bounds))
    except Exception:
        pass
    return self

map.Map.add_circle

add_circle(
    point: Point,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    style: CircleStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self

Add a circle marker (fixed pixel size).

Parameters:

  • point (Point) –

    Shapely Point.

  • tooltip (str | RawHTML | None, default: None ) –

    Markdown tooltip, or RawHTML for pre-formatted HTML.

  • popup (str | RawHTML | None, default: None ) –

    Markdown popup, or RawHTML for pre-formatted HTML.

  • style (CircleStyle | dict[str, Any] | None, default: None ) –

    Circle appearance.

  • min_zoom (int | None, default: None ) –

    Minimum zoom level at which the marker is visible.

  • popup_style (PopupStyle | dict[str, Any] | None, default: None ) –

    Popup dimensions.

  • tooltip_style (TooltipStyle | dict[str, Any] | None, default: None ) –

    Tooltip appearance.

Returns:

Source code in mapyta/map.py
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
def add_circle(
    self,
    point: Point,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    style: CircleStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self:
    """Add a circle marker (fixed pixel size).

    Parameters
    ----------
    point : Point
        Shapely Point.
    tooltip : str | RawHTML | None
        Markdown tooltip, or ``RawHTML`` for pre-formatted HTML.
    popup : str | RawHTML | None
        Markdown popup, or ``RawHTML`` for pre-formatted HTML.
    style : CircleStyle | dict[str, Any] | None
        Circle appearance.
    min_zoom : int | None
        Minimum zoom level at which the marker is visible.
    popup_style : PopupStyle | dict[str, Any] | None
        Popup dimensions.
    tooltip_style : TooltipStyle | dict[str, Any] | None
        Tooltip appearance.

    Returns
    -------
    Map
    """
    point = cast(Point, self._transform(point))
    self._extend_bounds(point)
    cs = resolve_style(style, CircleStyle) or CircleStyle()
    marker = folium.CircleMarker(
        location=[point.y, point.x],
        radius=cs.radius,
        color=cs.stroke.color,
        weight=cs.stroke.weight,
        opacity=cs.stroke.opacity,
        fill=True,
        fill_color=cs.fill.color,
        fill_opacity=cs.fill.opacity,
        tooltip=self._make_tooltip(tooltip, tooltip_style),
        popup=self._make_popup(popup, popup_style),
        dash_array=cs.stroke.dash_array,
    )
    marker.add_to(self._target())
    self._record_feature(
        point,
        {
            "radius": cs.radius,
            "stroke_color": cs.stroke.color,
            "stroke_weight": cs.stroke.weight,
            "stroke_opacity": cs.stroke.opacity,
            "stroke_dash_array": cs.stroke.dash_array,
            "fill_color": cs.fill.color,
            "fill_opacity": cs.fill.opacity,
            "tooltip": self._raw_text(tooltip),
            "popup": self._raw_text(popup),
            "min_zoom": min_zoom,
        },
    )
    if min_zoom is not None and min_zoom > 0:
        self._zoom_controlled_markers.append(
            {
                "var_name": marker.get_name(),
                "min_zoom": min_zoom,
            }
        )
    return self

map.Map.add_colorbar

add_colorbar(
    colors: list[str] | str | None,
    vmin: float,
    vmax: float,
    legend_name: str | RawHTML,
) -> LinearColormap

Add a standalone color-scale legend (colorbar) to the map.

Unlike :meth:add_choropleth and :meth:from_geodataframe, this adds a legend on its own β€” for maps whose features are coloured by hand (e.g. :meth:add_circle / :meth:add_point with per-feature styles). The returned colormap is callable: colormap(value) yields the hex colour for value, so callers colour their own features consistently with the legend without rebuilding the scale.

The legend is a readable HTML <div> pinned to the right edge of the map, spanning ~90% of its height (5% clear at top and bottom): a vertical gradient bar with the legend_name above and evenly spaced value ticks alongside, high at the top, rather than branca's default SVG colorbar. The legend sits clear of the top-centre :paramref:title instead of overlapping it.

Parameters:

  • colors (list[str] | str | None) –

    Palette name (e.g. "blues"), list of hex colors, or None for the default palette. Same handling as :meth:add_choropleth.

  • vmin (float) –

    Color scale range.

  • vmax (float) –

    Color scale range.

  • legend_name (str | RawHTML) –

    Legend label. Plain strings are HTML-escaped and shown literally; wrap in :class:~mapyta.markdown.RawHTML to render inline markup such as <sub>/<sup> (e.g. RawHTML("R<sub>c;cal</sub>")).

Returns:

  • LinearColormap –

    The colormap added to the map. Call it with a value to get a colour.

Source code in mapyta/map.py
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
def add_colorbar(
    self,
    colors: list[str] | str | None,
    vmin: float,
    vmax: float,
    legend_name: str | RawHTML,
) -> cm.LinearColormap:
    """Add a standalone color-scale legend (colorbar) to the map.

    Unlike :meth:`add_choropleth` and :meth:`from_geodataframe`, this adds a
    legend on its own β€” for maps whose features are coloured by hand (e.g.
    :meth:`add_circle` / :meth:`add_point` with per-feature styles). The
    returned colormap is callable: ``colormap(value)`` yields the hex colour
    for ``value``, so callers colour their own features consistently with
    the legend without rebuilding the scale.

    The legend is a readable HTML ``<div>`` pinned to the right edge of the
    map, spanning ~90% of its height (5% clear at top and bottom): a vertical
    gradient bar with the ``legend_name`` above and evenly spaced value ticks
    alongside, high at the top, rather than branca's default SVG colorbar. The
    legend sits clear of the top-centre :paramref:`title` instead of
    overlapping it.

    Parameters
    ----------
    colors : list[str] | str | None
        Palette name (e.g. ``"blues"``), list of hex colors, or ``None`` for
        the default palette. Same handling as :meth:`add_choropleth`.
    vmin, vmax : float
        Color scale range.
    legend_name : str | RawHTML
        Legend label. Plain strings are HTML-escaped and shown literally; wrap
        in :class:`~mapyta.markdown.RawHTML` to render inline markup such as
        ``<sub>``/``<sup>`` (e.g. ``RawHTML("R<sub>c;cal</sub>")``).

    Returns
    -------
    branca.colormap.LinearColormap
        The colormap added to the map. Call it with a value to get a colour.
    """
    color_list = self._resolve_colors(colors)
    # Build the colormap from the already-resolved ramp (don't route back through
    # ``_build_colormap``, which would resolve a second time).
    colormap = cm.LinearColormap(colors=color_list, vmin=vmin, vmax=vmax, caption=legend_name)
    # Track the colormap in ``self._colormaps`` for consistency with the other
    # colormap methods, but skip ``_register_colormap``: it calls ``colormap.add_to``,
    # which would emit branca's SVG colorbar. We render our own HTML legend instead.
    self._colormaps.append(colormap)
    self._add_html_colorbar(colors=color_list, vmin=vmin, vmax=vmax, legend_name=legend_name)
    return colormap

map.Map.add_dataframe

add_dataframe(
    df: Any,
    geometry_col: str = "geometry",
    hover_fields: list[str] | None = None,
    style: dict[str, Any] | None = None,
    highlight: dict[str, Any] | None = None,
) -> Self

Add a Pandas or Polars DataFrame as a GeoJSON layer.

The DataFrame must contain WKT geometry strings in WGS84 (EPSG:4326) in geometry_col. All other columns become GeoJSON Feature properties and are accessible via hover_fields.

Parameters:

  • df (DataFrame | DataFrame) –

    Input DataFrame. GeoPandas GeoDataFrames are not supported; use :meth:from_geodataframe instead.

  • geometry_col (str, default: 'geometry' ) –

    Column name that holds WKT geometry strings. Default "geometry".

  • hover_fields (list[str] | None, default: None ) –

    Property fields to show in the hover tooltip.

  • style (dict[str, Any] | None, default: None ) –

    Style kwargs forwarded to Folium GeoJson (e.g. color, weight).

  • highlight (dict[str, Any] | None, default: None ) –

    Highlight kwargs for mouse-over.

Returns:

Raises:

  • TypeError –

    If df is not a pandas or polars DataFrame.

  • ValueError –

    If geometry_col is missing, the DataFrame is empty, or a WKT string cannot be parsed.

Examples:

>>> import pandas as pd
>>> from mapyta import Map
>>> df = pd.DataFrame({"geometry": ["POINT (4.9 52.37)"], "name": ["Amsterdam"]})
>>> m = Map().add_dataframe(df, hover_fields=["name"])
Source code in mapyta/map.py
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
def add_dataframe(
    self,
    df: Any,  # noqa: ANN401
    geometry_col: str = "geometry",
    hover_fields: list[str] | None = None,
    style: dict[str, Any] | None = None,
    highlight: dict[str, Any] | None = None,
) -> Self:
    """Add a Pandas or Polars DataFrame as a GeoJSON layer.

    The DataFrame must contain WKT geometry strings in WGS84 (EPSG:4326) in ``geometry_col``.
    All other columns become GeoJSON Feature properties and are accessible via ``hover_fields``.

    Parameters
    ----------
    df : pandas.DataFrame | polars.DataFrame
        Input DataFrame. GeoPandas GeoDataFrames are not supported; use
        :meth:`from_geodataframe` instead.
    geometry_col : str
        Column name that holds WKT geometry strings. Default ``"geometry"``.
    hover_fields : list[str] | None
        Property fields to show in the hover tooltip.
    style : dict[str, Any] | None
        Style kwargs forwarded to Folium GeoJson (e.g. ``color``, ``weight``).
    highlight : dict[str, Any] | None
        Highlight kwargs for mouse-over.

    Returns
    -------
    Map

    Raises
    ------
    TypeError
        If ``df`` is not a pandas or polars DataFrame.
    ValueError
        If ``geometry_col`` is missing, the DataFrame is empty, or a WKT string cannot be parsed.

    Examples
    --------
    >>> import pandas as pd
    >>> from mapyta import Map
    >>> df = pd.DataFrame({"geometry": ["POINT (4.9 52.37)"], "name": ["Amsterdam"]})
    >>> m = Map().add_dataframe(df, hover_fields=["name"])
    """
    from mapyta.dataframe import dataframe_to_geojson  # noqa: PLC0415

    fc = dataframe_to_geojson(df, geometry_col=geometry_col)
    return self.add_geojson(fc, hover_fields=hover_fields, style=style, highlight=highlight)

map.Map.add_export_button

add_export_button(
    label: str = "Download GeoJSON",
    filename: str = "export.geojson",
    position: str = "topright",
) -> Self

Add a download button to the map that exports all features as a GeoJSON file.

When clicked, the button triggers a browser download of a GeoJSON FeatureCollection containing all features added to the map.

Parameters:

  • label (str, default: 'Download GeoJSON' ) –

    Button label text.

  • filename (str, default: 'export.geojson' ) –

    Default filename for the downloaded file.

  • position (str, default: 'topright' ) –

    Leaflet control position: "topleft", "topright", "bottomleft", or "bottomright".

Returns:

Source code in mapyta/map.py
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
def add_export_button(
    self,
    label: str = "Download GeoJSON",
    filename: str = "export.geojson",
    position: str = "topright",
) -> Self:
    """Add a download button to the map that exports all features as a GeoJSON file.

    When clicked, the button triggers a browser download of a GeoJSON
    ``FeatureCollection`` containing all features added to the map.

    Parameters
    ----------
    label : str
        Button label text.
    filename : str
        Default filename for the downloaded file.
    position : str
        Leaflet control position: ``"topleft"``, ``"topright"``,
        ``"bottomleft"``, or ``"bottomright"``.

    Returns
    -------
    Map
    """
    self._export_button_config = {"label": label, "filename": filename, "position": position}
    return self

map.Map.add_geojson

add_geojson(
    data: dict | str | Path,
    hover_fields: list[str] | None = None,
    style: dict[str, Any] | None = None,
    highlight: dict[str, Any] | None = None,
) -> Self

Add a GeoJSON layer.

Parameters:

  • data (dict | str | Path) –

    GeoJSON as dict, JSON string, or file path.

  • hover_fields (list[str] | None, default: None ) –

    Property fields for the tooltip.

  • style (dict[str, Any] | None, default: None ) –

    Style kwargs.

  • highlight (dict[str, Any] | None, default: None ) –

    Highlight kwargs for mouse-over.

Returns:

Source code in mapyta/map.py
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
def add_geojson(
    self,
    data: dict | str | Path,
    hover_fields: list[str] | None = None,
    style: dict[str, Any] | None = None,
    highlight: dict[str, Any] | None = None,
) -> Self:
    """Add a GeoJSON layer.

    Parameters
    ----------
    data : dict | str | Path
        GeoJSON as dict, JSON string, or file path.
    hover_fields : list[str] | None
        Property fields for the tooltip.
    style : dict[str, Any] | None
        Style kwargs.
    highlight : dict[str, Any] | None
        Highlight kwargs for mouse-over.

    Returns
    -------
    Map
    """
    data = load_geojson_input(data)
    if data.get("type") == "FeatureCollection":
        self._geojson_features.extend(data.get("features", []))
    elif data.get("type") == "Feature":
        self._geojson_features.append(data)

    ds = {"color": "#3388ff", "weight": 2, "fillOpacity": 0.2}
    if style:
        ds.update(style)
    dh = {"weight": 5, "fillOpacity": 0.4}
    if highlight:
        dh.update(highlight)

    tooltip = folium.GeoJsonTooltip(fields=hover_fields, localize=True) if hover_fields else None

    layer = folium.GeoJson(
        data,
        style_function=lambda _: ds,
        highlight_function=lambda _: dh,
        tooltip=tooltip,
    )
    layer.add_to(self._target())
    try:
        layer_bounds = layer.get_bounds()
        if layer_bounds:
            self._bounds.extend(cast(list[tuple[float, float]], layer_bounds))
    except Exception:
        pass
    return self

map.Map.add_geometry

add_geometry(
    geom: BaseGeometry,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    label: str | None = None,
    stroke: StrokeStyle | dict[str, Any] | None = None,
    fill: FillStyle | dict[str, Any] | None = None,
    marker_style: dict[str, str] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self

Add any Shapely geometry (auto-dispatches by type).

Parameters:

  • geom (BaseGeometry) –

    Any supported Shapely geometry.

  • tooltip (str | RawHTML | None, default: None ) –

    Style and interaction parameters.

  • popup (str | RawHTML | None, default: None ) –

    Style and interaction parameters.

  • label (str | RawHTML | None, default: None ) –

    Style and interaction parameters.

  • stroke (str | RawHTML | None, default: None ) –

    Style and interaction parameters.

  • fill (str | RawHTML | None, default: None ) –

    Style and interaction parameters.

  • marker_style (str | RawHTML | None, default: None ) –

    Style and interaction parameters.

  • popup_style (str | RawHTML | None, default: None ) –

    Style and interaction parameters.

  • tooltip_style (str | RawHTML | None, default: None ) –

    Style and interaction parameters.

  • min_zoom (int | None, default: None ) –

    Minimum zoom level at which the geometry is visible.

Returns:

Raises:

  • TypeError –

    If geometry type is unsupported.

Source code in mapyta/map.py
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
def add_geometry(
    self,
    geom: BaseGeometry,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    label: str | None = None,
    stroke: StrokeStyle | dict[str, Any] | None = None,
    fill: FillStyle | dict[str, Any] | None = None,
    marker_style: dict[str, str] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self:
    """Add any Shapely geometry (auto-dispatches by type).

    Parameters
    ----------
    geom : BaseGeometry
        Any supported Shapely geometry.
    tooltip, popup, label, stroke, fill, marker_style, popup_style, tooltip_style
        Style and interaction parameters.
    min_zoom : int | None
        Minimum zoom level at which the geometry is visible.

    Returns
    -------
    Map

    Raises
    ------
    TypeError
        If geometry type is unsupported.
    """
    tip = tooltip
    ts = tooltip_style
    if isinstance(geom, Point):
        self.add_point(
            geom, tooltip=tip, popup=popup, marker=label, marker_style=marker_style, popup_style=popup_style, min_zoom=min_zoom, tooltip_style=ts
        )
    elif isinstance(geom, LinearRing):
        # LinearRing is a subclass of LineString, check first
        self.add_linestring(
            LineString(geom.coords), tooltip=tip, popup=popup, stroke=stroke, popup_style=popup_style, min_zoom=min_zoom, tooltip_style=ts
        )
    elif isinstance(geom, LineString):
        self.add_linestring(geom, tooltip=tip, popup=popup, stroke=stroke, popup_style=popup_style, min_zoom=min_zoom, tooltip_style=ts)
    elif isinstance(geom, Polygon):
        self.add_polygon(geom, tooltip=tip, popup=popup, stroke=stroke, fill=fill, popup_style=popup_style, min_zoom=min_zoom, tooltip_style=ts)
    elif isinstance(geom, MultiPolygon):
        self.add_multipolygon(
            geom, tooltip=tip, popup=popup, stroke=stroke, fill=fill, popup_style=popup_style, min_zoom=min_zoom, tooltip_style=ts
        )
    elif isinstance(geom, MultiLineString):
        self.add_multilinestring(geom, tooltip=tip, popup=popup, stroke=stroke, popup_style=popup_style, min_zoom=min_zoom, tooltip_style=ts)
    elif isinstance(geom, MultiPoint):
        self.add_multipoint(
            geom, tooltip=tip, popup=popup, label=label, marker_style=marker_style, popup_style=popup_style, min_zoom=min_zoom, tooltip_style=ts
        )
    else:
        raise TypeError(f"Unsupported geometry type: {type(geom).__name__}")
    return self

map.Map.add_heatmap

add_heatmap(
    points: list[Point]
    | list[tuple[float, float]]
    | list[tuple[float, float, float]],
    style: HeatmapStyle | dict[str, Any] | None = None,
    name: str | None = None,
) -> Self

Add a heatmap layer.

Parameters:

  • points (list[Point] | list[tuple]) –

    Shapely Points (lon, lat) or tuples (lat, lon[, intensity]).

  • style (HeatmapStyle | dict[str, Any] | None, default: None ) –

    Heatmap appearance.

  • name (str | None, default: None ) –

    Layer name.

Returns:

Source code in mapyta/map.py
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
def add_heatmap(
    self,
    points: list[Point] | list[tuple[float, float]] | list[tuple[float, float, float]],
    style: HeatmapStyle | dict[str, Any] | None = None,
    name: str | None = None,
) -> Self:
    """Add a heatmap layer.

    Parameters
    ----------
    points : list[Point] | list[tuple]
        Shapely Points ``(lon, lat)`` or tuples ``(lat, lon[, intensity])``.
    style : HeatmapStyle | dict[str, Any] | None
        Heatmap appearance.
    name : str | None
        Layer name.

    Returns
    -------
    Map
    """
    hs = resolve_style(style, HeatmapStyle) or HeatmapStyle()
    heat_data: list[list[float]] = []
    for p in points:
        if isinstance(p, Point):
            pt = cast(Point, self._transform(p))
            heat_data.append([pt.y, pt.x])
            self._extend_bounds(pt)
            self._record_feature(pt, {})
        elif len(p) == 2:
            heat_data.append([p[0], p[1]])
            self._bounds.append((p[0], p[1]))
            self._record_feature(Point(p[1], p[0]), {})
        else:
            heat_data.append(list(p[:3]))
            self._bounds.append((p[0], p[1]))
            self._record_feature(Point(p[1], p[0]), {"intensity": p[2]})  # ty: ignore[index-out-of-bounds]

    kwargs: dict[str, Any] = {
        "radius": hs.radius,
        "blur": hs.blur,
        "min_opacity": hs.min_opacity,
        "max_zoom": hs.max_zoom,
    }
    if hs.gradient:
        kwargs["gradient"] = hs.gradient

    folium.plugins.HeatMap(heat_data, name=name, **kwargs).add_to(self._target())
    return self

map.Map.add_heatmap_with_time

add_heatmap_with_time(
    data: list[
        list[Point]
        | list[tuple[float, float]]
        | list[tuple[float, float, float]]
    ],
    index: list[str],
    radius: int = 15,
    blur: float = 0.8,
    max_opacity: float = 0.6,
    min_opacity: float = 0.0,
    gradient: dict[float, str] | None = None,
    auto_play: bool = False,
    display_index: bool = True,
    position: str = "bottomleft",
) -> Self

Add a heatmap with a time slider.

Displays a heatmap that changes over time. A playback control lets users step or scrub through the time steps.

Parameters:

  • data (list of timestep point lists) –

    Outer list corresponds to time steps (must match index length). Each inner list is a collection of points for that step, in the same format accepted by :meth:add_heatmap: Shapely Point objects or (lat, lon[, intensity]) tuples.

  • index (list[str]) –

    Labels for each time step shown in the slider (e.g. dates: ["2024-01", "2024-02", ...]).

  • radius (int, default: 15 ) –

    Heatmap point radius in pixels.

  • blur (float, default: 0.8 ) –

    Blur amount (0–1).

  • max_opacity (float, default: 0.6 ) –

    Maximum heatmap opacity (0–1).

  • min_opacity (float, default: 0.0 ) –

    Minimum heatmap opacity (0–1).

  • gradient (dict[float, str] | None, default: None ) –

    Colour gradient mapping density values (0–1) to CSS colours, e.g. {0.0: "blue", 0.5: "yellow", 1.0: "red"}.

  • auto_play (bool, default: False ) –

    Start playback automatically when the map loads.

  • display_index (bool, default: True ) –

    Show the current time step label in the control.

  • position (str, default: 'bottomleft' ) –

    Control position: "bottomleft", "bottomright", "topleft", or "topright".

Returns:

Raises:

  • ValueError –

    If the length of data does not match the length of index.

Source code in mapyta/map.py
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
def add_heatmap_with_time(
    self,
    data: list[list[Point] | list[tuple[float, float]] | list[tuple[float, float, float]]],
    index: list[str],
    radius: int = 15,
    blur: float = 0.8,
    max_opacity: float = 0.6,
    min_opacity: float = 0.0,
    gradient: dict[float, str] | None = None,
    auto_play: bool = False,
    display_index: bool = True,
    position: str = "bottomleft",
) -> Self:
    """Add a heatmap with a time slider.

    Displays a heatmap that changes over time.  A playback control
    lets users step or scrub through the time steps.

    Parameters
    ----------
    data : list of timestep point lists
        Outer list corresponds to time steps (must match *index*
        length).  Each inner list is a collection of points for that
        step, in the same format accepted by :meth:`add_heatmap`:
        Shapely ``Point`` objects or ``(lat, lon[, intensity])``
        tuples.
    index : list[str]
        Labels for each time step shown in the slider (e.g. dates:
        ``["2024-01", "2024-02", ...]``).
    radius : int
        Heatmap point radius in pixels.
    blur : float
        Blur amount (0–1).
    max_opacity : float
        Maximum heatmap opacity (0–1).
    min_opacity : float
        Minimum heatmap opacity (0–1).
    gradient : dict[float, str] | None
        Colour gradient mapping density values (0–1) to CSS colours,
        e.g. ``{0.0: "blue", 0.5: "yellow", 1.0: "red"}``.
    auto_play : bool
        Start playback automatically when the map loads.
    display_index : bool
        Show the current time step label in the control.
    position : str
        Control position: ``"bottomleft"``, ``"bottomright"``,
        ``"topleft"``, or ``"topright"``.

    Returns
    -------
    Map

    Raises
    ------
    ValueError
        If the length of *data* does not match the length of *index*.
    """
    if len(data) != len(index):
        msg = f"add_heatmap_with_time(): data has {len(data)} time step(s) but index has {len(index)} label(s). They must be equal."
        raise ValueError(msg)

    time_data: list[list[list[float]]] = []
    for timestep in data:
        step_points: list[list[float]] = []
        for p in timestep:
            if isinstance(p, Point):
                pt = cast(Point, self._transform(p))
                self._extend_bounds(pt)
                step_points.append([pt.y, pt.x])
            elif len(p) == 3:
                self._bounds.append((p[0], p[1]))
                step_points.append([p[0], p[1], p[2]])  # ty: ignore[index-out-of-bounds]
            else:
                self._bounds.append((p[0], p[1]))
                step_points.append([p[0], p[1]])
        time_data.append(step_points)

    kwargs: dict[str, Any] = {
        "radius": radius,
        "blur": blur,
        "max_opacity": max_opacity,
        "min_opacity": min_opacity,
        "auto_play": auto_play,
        "display_index": display_index,
        "position": position,
    }
    if gradient:
        kwargs["gradient"] = gradient

    folium.plugins.HeatMapWithTime(time_data, index=index, **kwargs).add_to(self._target())
    return self

map.Map.add_layer_control

add_layer_control(collapsed: bool = True, position: str = 'topright') -> Self

Add a layer control toggle.

Parameters:

  • collapsed (bool, default: True ) –

    Start collapsed.

  • position (str, default: 'topright' ) –

    Control position.

Returns:

Source code in mapyta/map.py
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
def add_layer_control(self, collapsed: bool = True, position: str = "topright") -> Self:
    """Add a layer control toggle.

    Parameters
    ----------
    collapsed : bool
        Start collapsed.
    position : str
        Control position.

    Returns
    -------
    Map
    """
    folium.LayerControl(collapsed=collapsed, position=position).add_to(self._map)
    return self

map.Map.add_layer_dropdown

add_layer_dropdown(
    names: list[str] | None = None,
    position: str = "topleft",
    label: str | None = None,
) -> Self

Add a single-select dropdown that switches between feature groups.

Where :meth:add_layer_control lists feature groups as checkboxes (any number visible at once), this renders a single <select> whose options are feature groups: choosing one shows that group and hides the others, so exactly one is visible and the dropdown displays the active group's name.

The chosen groups are removed from :meth:add_layer_control's overlay list (their control flag is cleared at render time), so a group shows up in either the dropdown or the checkbox control, never both. Base/tile layers are untouched and keep their radio control. Use the two together to get radio tile layers plus a single-select overlay switcher.

Parameters:

  • names (list[str] | None, default: None ) –

    Feature group names to include, in display order. None (the default) uses every feature group created so far, in creation order. Unknown names are ignored; if none match, the call is a no-op.

  • position (str, default: 'topleft' ) –

    Leaflet control position: "topleft", "topright", "bottomleft", or "bottomright".

  • label (str | None, default: None ) –

    Optional caption rendered above the dropdown.

Returns:

Source code in mapyta/map.py
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
def add_layer_dropdown(
    self,
    names: list[str] | None = None,
    position: str = "topleft",
    label: str | None = None,
) -> Self:
    """Add a single-select dropdown that switches between feature groups.

    Where :meth:`add_layer_control` lists feature groups as checkboxes (any
    number visible at once), this renders a single ``<select>`` whose options
    are feature groups: choosing one shows that group and hides the others, so
    exactly one is visible and the dropdown displays the active group's name.

    The chosen groups are removed from :meth:`add_layer_control`'s overlay
    list (their ``control`` flag is cleared at render time), so a group shows
    up in either the dropdown or the checkbox control, never both. Base/tile
    layers are untouched and keep their radio control. Use the two together to
    get radio tile layers plus a single-select overlay switcher.

    Parameters
    ----------
    names : list[str] | None
        Feature group names to include, in display order. ``None`` (the
        default) uses every feature group created so far, in creation order.
        Unknown names are ignored; if none match, the call is a no-op.
    position : str
        Leaflet control position: ``"topleft"``, ``"topright"``,
        ``"bottomleft"``, or ``"bottomright"``.
    label : str | None
        Optional caption rendered above the dropdown.

    Returns
    -------
    Map
    """
    self._layer_dropdown_config = {"names": names, "position": position, "label": label}
    self._layer_dropdown_injected = False
    return self

map.Map.add_linestring

add_linestring(
    line: LineString,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    stroke: StrokeStyle | dict[str, Any] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self

Add a LineString.

Parameters:

  • line (LineString) –

    Shapely LineString.

  • tooltip (str | RawHTML | None, default: None ) –

    Markdown tooltip, or RawHTML for pre-formatted HTML.

  • popup (str | RawHTML | None, default: None ) –

    Markdown popup, or RawHTML for pre-formatted HTML.

  • stroke (StrokeStyle | dict[str, Any] | None, default: None ) –

    Line style.

  • popup_style (PopupStyle | dict[str, Any] | None, default: None ) –

    Popup dimensions.

  • min_zoom (int | None, default: None ) –

    Minimum zoom level at which the line is visible.

  • tooltip_style (TooltipStyle | dict[str, Any] | None, default: None ) –

    Tooltip appearance.

Returns:

Source code in mapyta/map.py
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
def add_linestring(
    self,
    line: LineString,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    stroke: StrokeStyle | dict[str, Any] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self:
    """Add a LineString.

    Parameters
    ----------
    line : LineString
        Shapely LineString.
    tooltip : str | RawHTML | None
        Markdown tooltip, or ``RawHTML`` for pre-formatted HTML.
    popup : str | RawHTML | None
        Markdown popup, or ``RawHTML`` for pre-formatted HTML.
    stroke : StrokeStyle | dict[str, Any] | None
        Line style.
    popup_style : PopupStyle | dict[str, Any] | None
        Popup dimensions.
    min_zoom : int | None
        Minimum zoom level at which the line is visible.
    tooltip_style : TooltipStyle | dict[str, Any] | None
        Tooltip appearance.

    Returns
    -------
    Map
    """
    line = cast(LineString, self._transform(line))
    self._extend_bounds(line)
    s = resolve_style(stroke, StrokeStyle) or StrokeStyle()
    locations = [(c[1], c[0]) for c in line.coords]
    layer = folium.PolyLine(
        locations=locations,
        color=s.color,
        weight=s.weight,
        opacity=s.opacity,
        dash_array=s.dash_array,
        tooltip=self._make_tooltip(tooltip, tooltip_style),
        popup=self._make_popup(popup, popup_style),
    )
    layer.add_to(self._target())
    self._record_feature(
        line,
        {
            "stroke_color": s.color,
            "stroke_weight": s.weight,
            "stroke_opacity": s.opacity,
            "stroke_dash_array": s.dash_array,
            "tooltip": self._raw_text(tooltip),
            "popup": self._raw_text(popup),
            "min_zoom": min_zoom,
        },
    )
    if min_zoom is not None and min_zoom > 0:
        self._zoom_controlled_markers.append({"var_name": layer.get_name(), "min_zoom": min_zoom})
    return self

map.Map.add_marker_cluster

add_marker_cluster(
    points: list[Point],
    labels: list[str] | None = None,
    tooltips: list[str] | None = None,
    popups: list[str] | None = None,
    marker_style: dict[str, str] | None = None,
    name: str | None = None,
    min_zoom: int | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    captions: list[str] | None = None,
    caption_style: dict[str, str] | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
    min_zoom_caption: int | None = None,
) -> Self

Add clustered markers that group at low zoom.

Parameters:

  • points (list[Point]) –

    Shapely Points.

  • labels (list[str] | None, default: None ) –

    Per-location marker content (icon names or emoji/text).

  • tooltips (list[str] | None, default: None ) –

    Per-location markdown tooltips.

  • popups (list[str] | None, default: None ) –

    Per-location markdown popups.

  • marker_style (dict[str, str] | None, default: None ) –

    CSS property overrides for each marker.

  • name (str | None, default: None ) –

    Layer name.

  • min_zoom (int | None, default: None ) –

    Minimum zoom level at which the cluster is visible.

  • popup_style (PopupStyle | dict[str, Any] | None, default: None ) –

    Popup dimensions.

  • captions (list[str] | None, default: None ) –

    Per-location text annotations placed below each marker.

  • caption_style (dict[str, str] | None, default: None ) –

    CSS property overrides for captions.

  • tooltip_style (TooltipStyle | dict[str, Any] | None, default: None ) –

    Tooltip appearance (font size, width, etc.).

  • min_zoom_caption (int | None, default: None ) –

    Minimum zoom level at which captions are visible. Applies only to the caption text β€” the marker icons remain visible. None or 0 means always visible. Ignored for entries without a caption. Note: caption visibility is also bounded below by each entry's min_zoom β€” the caption lives inside the marker's DivIcon DOM, which is removed when the marker is hidden, so setting min_zoom_caption lower than min_zoom does not reveal the caption at those zooms.

Returns:

Source code in mapyta/map.py
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
def add_marker_cluster(  # noqa: PLR0913
    self,
    points: list[Point],
    labels: list[str] | None = None,
    tooltips: list[str] | None = None,
    popups: list[str] | None = None,
    marker_style: dict[str, str] | None = None,
    name: str | None = None,
    min_zoom: int | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    captions: list[str] | None = None,
    caption_style: dict[str, str] | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
    min_zoom_caption: int | None = None,
) -> Self:
    """Add clustered markers that group at low zoom.

    Parameters
    ----------
    points : list[Point]
        Shapely Points.
    labels : list[str] | None
        Per-location marker content (icon names or emoji/text).
    tooltips : list[str] | None
        Per-location markdown tooltips.
    popups : list[str] | None
        Per-location markdown popups.
    marker_style : dict[str, str] | None
        CSS property overrides for each marker.
    name : str | None
        Layer name.
    min_zoom : int | None
        Minimum zoom level at which the cluster is visible.
    popup_style : PopupStyle | dict[str, Any] | None
        Popup dimensions.
    captions : list[str] | None
        Per-location text annotations placed below each marker.
    caption_style : dict[str, str] | None
        CSS property overrides for ``captions``.
    tooltip_style : TooltipStyle | dict[str, Any] | None
        Tooltip appearance (font size, width, etc.).
    min_zoom_caption : int | None
        Minimum zoom level at which captions are visible. Applies only
        to the caption text β€” the marker icons remain visible. ``None``
        or ``0`` means always visible. Ignored for entries without a
        caption. Note: caption visibility is also bounded below by
        each entry's ``min_zoom`` β€” the caption lives inside the
        marker's DivIcon DOM, which is removed when the marker is
        hidden, so setting ``min_zoom_caption`` lower than ``min_zoom``
        does not reveal the caption at those zooms.

    Returns
    -------
    Map
    """
    css = marker_style or {}
    cap_css = {**DEFAULT_MARKER_CAPTION_CSS, **(caption_style or {})}
    cluster = folium.plugins.MarkerCluster(name=name)
    track_captions = min_zoom_caption is not None and min_zoom_caption > 0

    for i, point in enumerate(points):
        pt = cast(Point, self._transform(point))
        self._extend_bounds(pt)
        lat, lon = pt.y, pt.x

        label = labels[i] if labels and i < len(labels) else None
        tip = tooltips[i] if tooltips and i < len(tooltips) else None
        popup = popups[i] if popups and i < len(popups) else None
        txt = captions[i] if captions and i < len(captions) else None

        caption_id: str | None = None
        if txt is not None and track_captions:
            caption_id = f"caption_{uuid.uuid4().hex[:15]}"

        kind = classify_marker(label) if label else "icon_name"
        if kind == "emoji":
            assert label is not None  # guarded by classify_marker above
            icon = build_text_marker(label, css, txt, cap_css, caption_id)
        else:
            icon_name = label or "arrow-down"
            icon = build_icon_marker(icon_name, css, txt, cap_css, caption_id)

        folium.Marker(
            location=[lat, lon],
            icon=icon,
            tooltip=self._make_tooltip(tip, tooltip_style),
            popup=self._make_popup(popup, popup_style),
        ).add_to(cluster)
        self._record_feature(
            pt,
            {
                "marker": label,
                "caption": txt,
                "tooltip": tip,
                "popup": popup,
                "min_zoom": min_zoom,
                "min_zoom_caption": min_zoom_caption,
            },
        )

        if caption_id is not None:
            assert min_zoom_caption is not None  # guarded by track_captions above
            self._zoom_controlled_captions.append(
                {
                    "caption_id": caption_id,
                    "min_zoom": min_zoom_caption,
                }
            )

    cluster.add_to(self._target())
    if min_zoom is not None and min_zoom > 0:
        self._zoom_controlled_markers.append(
            {
                "var_name": cluster.get_name(),
                "min_zoom": min_zoom,
            }
        )
    return self

map.Map.add_multilinestring

add_multilinestring(
    ml: MultiLineString,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    stroke: StrokeStyle | dict[str, Any] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self

Add a MultiLineString.

Parameters:

  • ml (MultiLineString) –

    Shapely MultiLineString.

  • tooltip (str | RawHTML | None, default: None ) –

    See add_linestring.

  • popup (str | RawHTML | None, default: None ) –

    See add_linestring.

  • stroke (str | RawHTML | None, default: None ) –

    See add_linestring.

  • popup_style (str | RawHTML | None, default: None ) –

    See add_linestring.

  • tooltip_style (str | RawHTML | None, default: None ) –

    See add_linestring.

  • min_zoom (int | None, default: None ) –

    Minimum zoom level at which each line is visible.

Returns:

Source code in mapyta/map.py
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
def add_multilinestring(
    self,
    ml: MultiLineString,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    stroke: StrokeStyle | dict[str, Any] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self:
    """Add a MultiLineString.

    Parameters
    ----------
    ml : MultiLineString
        Shapely MultiLineString.
    tooltip, popup, stroke, popup_style, tooltip_style
        See ``add_linestring``.
    min_zoom : int | None
        Minimum zoom level at which each line is visible.

    Returns
    -------
    Map
    """
    for line in ml.geoms:
        self.add_linestring(
            line, tooltip=tooltip, popup=popup, stroke=stroke, popup_style=popup_style, min_zoom=min_zoom, tooltip_style=tooltip_style
        )
    return self

map.Map.add_multipoint

add_multipoint(
    mp: MultiPoint,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    label: str | None = None,
    marker_style: dict[str, str] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self

Add a MultiPoint.

Parameters:

  • mp (MultiPoint) –

    Shapely MultiPoint.

  • tooltip (str | RawHTML | None, default: None ) –

    See add_point.

  • popup (str | RawHTML | None, default: None ) –

    See add_point.

  • label (str | RawHTML | None, default: None ) –

    See add_point.

  • marker_style (str | RawHTML | None, default: None ) –

    See add_point.

  • popup_style (str | RawHTML | None, default: None ) –

    See add_point.

  • tooltip_style (str | RawHTML | None, default: None ) –

    See add_point.

  • min_zoom (int | None, default: None ) –

    Minimum zoom level at which each point is visible.

Returns:

Source code in mapyta/map.py
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
def add_multipoint(
    self,
    mp: MultiPoint,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    label: str | None = None,
    marker_style: dict[str, str] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self:
    """Add a MultiPoint.

    Parameters
    ----------
    mp : MultiPoint
        Shapely MultiPoint.
    tooltip, popup, label, marker_style, popup_style, tooltip_style
        See ``add_point``.
    min_zoom : int | None
        Minimum zoom level at which each point is visible.

    Returns
    -------
    Map
    """
    for pt in mp.geoms:
        self.add_point(
            pt,
            tooltip=tooltip,
            popup=popup,
            marker=label,
            marker_style=marker_style,
            popup_style=popup_style,
            min_zoom=min_zoom,
            tooltip_style=tooltip_style,
        )
    return self

map.Map.add_multipolygon

add_multipolygon(
    mp: MultiPolygon,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    stroke: StrokeStyle | dict[str, Any] | None = None,
    fill: FillStyle | dict[str, Any] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self

Add a MultiPolygon.

Parameters:

  • mp (MultiPolygon) –

    Shapely MultiPolygon.

  • tooltip (str | RawHTML | None, default: None ) –

    See add_polygon.

  • popup (str | RawHTML | None, default: None ) –

    See add_polygon.

  • stroke (str | RawHTML | None, default: None ) –

    See add_polygon.

  • fill (str | RawHTML | None, default: None ) –

    See add_polygon.

  • popup_style (str | RawHTML | None, default: None ) –

    See add_polygon.

  • tooltip_style (str | RawHTML | None, default: None ) –

    See add_polygon.

  • min_zoom (int | None, default: None ) –

    Minimum zoom level at which each polygon is visible.

Returns:

Source code in mapyta/map.py
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
def add_multipolygon(
    self,
    mp: MultiPolygon,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    stroke: StrokeStyle | dict[str, Any] | None = None,
    fill: FillStyle | dict[str, Any] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self:
    """Add a MultiPolygon.

    Parameters
    ----------
    mp : MultiPolygon
        Shapely MultiPolygon.
    tooltip, popup, stroke, fill, popup_style, tooltip_style
        See ``add_polygon``.
    min_zoom : int | None
        Minimum zoom level at which each polygon is visible.

    Returns
    -------
    Map
    """
    for poly in mp.geoms:
        self.add_polygon(
            poly, tooltip=tooltip, popup=popup, stroke=stroke, fill=fill, popup_style=popup_style, min_zoom=min_zoom, tooltip_style=tooltip_style
        )
    return self

map.Map.add_point

add_point(
    point: Point,
    marker: str | None = None,
    caption: str | None = None,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    marker_style: dict[str, str] | None = None,
    caption_style: dict[str, str] | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    min_zoom_caption: int | None = None,
) -> Self

Add a location marker.

Parameters:

  • point (Point) –

    Shapely Point (x, y) in source CRS.

  • marker (str | None, default: None ) –

    Desired marker symbol. Rendering path is auto-detected:

    • Bare icon name (e.g. "home") β†’ Glyphicon prefix.
    • Bare FA name (e.g. "fa-arrow-right") β†’ fa-solid prefix.
    • Full CSS class (e.g. "fa-solid fa-house") β†’ used as-is.
    • Emoji / unicode text β†’ rendered as text DivIcon.
    • None β†’ default "arrow-down" icon.
  • caption (str | None, default: None ) –

    Text annotation placed below the marker. Works with any marker type (emoji, icon). Can be styled via caption_style.

  • tooltip (str | RawHTML | None, default: None ) –

    Information shown on mouse tooltip. Markdown supported for strings, or use RawHTML for pre-formatted HTML.

  • popup (str | RawHTML | None, default: None ) –

    Information shown on click. Markdown supported for strings, or use RawHTML for pre-formatted HTML.

  • marker_style (dict[str, str] | None, default: None ) –

    CSS property overrides for the marker element.

  • caption_style (dict[str, str] | None, default: None ) –

    CSS property overrides for the caption.

  • tooltip_style (TooltipStyle | dict[str, Any] | None, default: None ) –

    Tooltip appearance.

  • popup_style (PopupStyle | dict[str, Any] | None, default: None ) –

    Popup dimensions.

  • min_zoom (int | None, default: None ) –

    Minimum zoom level at which the marker is visible. None or 0 means always visible.

  • min_zoom_caption (int | None, default: None ) –

    Minimum zoom level at which the caption is visible. Works like min_zoom but applies only to the caption text β€” the marker icon itself remains visible. None or 0 means always visible. Ignored when caption is not set. Note: caption visibility is also bounded below by min_zoom β€” the caption lives inside the marker's DivIcon DOM, which is removed when the marker is hidden, so setting min_zoom_caption lower than min_zoom does not reveal the caption at those zooms.

Returns:

Source code in mapyta/map.py
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
def add_point(  # noqa: PLR0913
    self,
    point: Point,
    marker: str | None = None,
    caption: str | None = None,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    marker_style: dict[str, str] | None = None,
    caption_style: dict[str, str] | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    min_zoom_caption: int | None = None,
) -> Self:
    """Add a location marker.

    Parameters
    ----------
    point : Point
        Shapely Point ``(x, y)`` in source CRS.
    marker : str | None
        Desired marker symbol.  Rendering path is auto-detected:

        - Bare icon name (e.g. ``"home"``) β†’ Glyphicon prefix.
        - Bare FA name (e.g. ``"fa-arrow-right"``) β†’ ``fa-solid`` prefix.
        - Full CSS class (e.g. ``"fa-solid fa-house"``) β†’ used as-is.
        - Emoji / unicode text β†’ rendered as text DivIcon.
        - ``None`` β†’ default ``"arrow-down"`` icon.
    caption : str | None
        Text annotation placed below the marker.  Works with any marker
        type (emoji, icon).  Can be styled via ``caption_style``.
    tooltip : str | RawHTML | None
        Information shown on mouse tooltip.  Markdown supported for
        strings, or use ``RawHTML`` for pre-formatted HTML.
    popup : str | RawHTML | None
        Information shown on click.  Markdown supported for strings,
        or use ``RawHTML`` for pre-formatted HTML.
    marker_style : dict[str, str] | None
        CSS property overrides for the marker element.
    caption_style : dict[str, str] | None
        CSS property overrides for the caption.
    tooltip_style : TooltipStyle | dict[str, Any] | None
        Tooltip appearance.
    popup_style : PopupStyle | dict[str, Any] | None
        Popup dimensions.
    min_zoom : int | None
        Minimum zoom level at which the marker is visible.
        ``None`` or ``0`` means always visible.
    min_zoom_caption : int | None
        Minimum zoom level at which the caption is visible. Works like
        ``min_zoom`` but applies only to the caption text β€” the marker
        icon itself remains visible. ``None`` or ``0`` means always
        visible. Ignored when ``caption`` is not set. Note: caption
        visibility is also bounded below by ``min_zoom`` β€” the caption
        lives inside the marker's DivIcon DOM, which is removed when
        the marker is hidden, so setting ``min_zoom_caption`` lower
        than ``min_zoom`` does not reveal the caption at those zooms.

    Returns
    -------
    Map
    """
    point = cast(Point, self._transform(point))
    self._extend_bounds(point)
    lat, lon = point.y, point.x

    css = marker_style or {}
    cap_css = {**DEFAULT_MARKER_CAPTION_CSS, **(caption_style or {})}

    caption_id: str | None = None
    if caption is not None and min_zoom_caption is not None and min_zoom_caption > 0:
        caption_id = f"caption_{uuid.uuid4().hex[:15]}"

    kind = classify_marker(marker) if marker else "icon_name"
    if kind == "emoji":
        assert marker is not None  # guarded by classify_marker above
        icon = build_text_marker(marker, css, caption, cap_css, caption_id)
    else:
        icon_name = marker or "arrow-down"
        icon = build_icon_marker(icon_name, css, caption, cap_css, caption_id)

    m = folium.Marker(
        location=[lat, lon],
        icon=icon,
        tooltip=self._make_tooltip(tooltip, tooltip_style),
        popup=self._make_popup(popup, popup_style),
    )
    m.add_to(self._target())
    self._record_feature(
        point,
        {
            "marker": marker,
            "caption": caption,
            "tooltip": self._raw_text(tooltip),
            "popup": self._raw_text(popup),
            "min_zoom": min_zoom,
            "min_zoom_caption": min_zoom_caption,
        },
    )

    if min_zoom is not None and min_zoom > 0:
        self._zoom_controlled_markers.append(
            {
                "var_name": m.get_name(),
                "min_zoom": min_zoom,
            }
        )

    if caption_id is not None:
        assert min_zoom_caption is not None  # guarded above
        self._zoom_controlled_captions.append(
            {
                "caption_id": caption_id,
                "min_zoom": min_zoom_caption,
            }
        )

    return self

map.Map.add_polygon

add_polygon(
    polygon: Polygon,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    stroke: StrokeStyle | dict[str, Any] | None = None,
    fill: FillStyle | dict[str, Any] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self

Add a Polygon.

Parameters:

  • polygon (Polygon) –

    Shapely Polygon.

  • tooltip (str | RawHTML | None, default: None ) –

    Markdown tooltip, or RawHTML for pre-formatted HTML.

  • popup (str | RawHTML | None, default: None ) –

    Markdown popup, or RawHTML for pre-formatted HTML.

  • stroke (StrokeStyle | dict[str, Any] | None, default: None ) –

    Border style.

  • fill (FillStyle | dict[str, Any] | None, default: None ) –

    Fill style.

  • popup_style (PopupStyle | dict[str, Any] | None, default: None ) –

    Popup dimensions.

  • min_zoom (int | None, default: None ) –

    Minimum zoom level at which the polygon is visible.

  • tooltip_style (TooltipStyle | dict[str, Any] | None, default: None ) –

    Tooltip appearance.

Returns:

Source code in mapyta/map.py
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
def add_polygon(
    self,
    polygon: Polygon,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    stroke: StrokeStyle | dict[str, Any] | None = None,
    fill: FillStyle | dict[str, Any] | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self:
    """Add a Polygon.

    Parameters
    ----------
    polygon : Polygon
        Shapely Polygon.
    tooltip : str | RawHTML | None
        Markdown tooltip, or ``RawHTML`` for pre-formatted HTML.
    popup : str | RawHTML | None
        Markdown popup, or ``RawHTML`` for pre-formatted HTML.
    stroke : StrokeStyle | dict[str, Any] | None
        Border style.
    fill : FillStyle | dict[str, Any] | None
        Fill style.
    popup_style : PopupStyle | dict[str, Any] | None
        Popup dimensions.
    min_zoom : int | None
        Minimum zoom level at which the polygon is visible.
    tooltip_style : TooltipStyle | dict[str, Any] | None
        Tooltip appearance.

    Returns
    -------
    Map
    """
    polygon = cast(Polygon, self._transform(polygon))
    self._extend_bounds(polygon)
    s = resolve_style(stroke, StrokeStyle) or StrokeStyle()
    f = resolve_style(fill, FillStyle) or FillStyle()
    exterior = [(c[1], c[0]) for c in polygon.exterior.coords]
    locations: list[list[tuple[float, float]]] = [exterior] + [[(c[1], c[0]) for c in interior.coords] for interior in polygon.interiors]
    layer = folium.Polygon(
        locations=locations,
        color=s.color,
        weight=s.weight,
        opacity=s.opacity,
        dash_array=s.dash_array,
        fill=True,
        fill_color=f.color,
        fill_opacity=f.opacity,
        tooltip=self._make_tooltip(tooltip, tooltip_style),
        popup=self._make_popup(popup, popup_style),
    )
    layer.add_to(self._target())
    self._record_feature(
        polygon,
        {
            "stroke_color": s.color,
            "stroke_weight": s.weight,
            "stroke_opacity": s.opacity,
            "stroke_dash_array": s.dash_array,
            "fill_color": f.color,
            "fill_opacity": f.opacity,
            "tooltip": self._raw_text(tooltip),
            "popup": self._raw_text(popup),
            "min_zoom": min_zoom,
        },
    )
    if min_zoom is not None and min_zoom > 0:
        self._zoom_controlled_markers.append({"var_name": layer.get_name(), "min_zoom": min_zoom})
    return self

map.Map.add_search_control

add_search_control(
    layer_name: str | None = None,
    property_name: str | None = None,
    placeholder: str = "Search...",
    position: str = "topright",
    zoom: int | None = None,
    geom_type: str = "Point",
) -> Self

Add a search control to find features by property value.

Users can type in the search box to locate and zoom to a matching feature.

When layer_name is None (the default), the search runs over all features added to the map β€” markers, GeoJSON, choropleth, and cluster layers β€” without requiring a feature group. Labels are inferred automatically from the feature's properties (caption, name, naam, etc.), or taken from property_name if supplied. If no features have been added yet, the call is a silent no-op.

When layer_name is provided, the search targets only that named feature group, preserving the original behaviour.

Parameters:

  • layer_name (str | None, default: None ) –

    Name of the feature group to search (as passed to create_feature_group). Pass None (the default) to search all features on the map automatically.

  • property_name (str | None, default: None ) –

    GeoJSON property name to use as the search label (e.g. "name", "gemeente"). When None and layer_name is also None, the label is inferred from common property names (caption, label, text, name, naam, title).

  • placeholder (str, default: 'Search...' ) –

    Placeholder text in the search input box.

  • position (str, default: 'topright' ) –

    Control position: "topleft", "topright", "bottomleft", "bottomright".

  • zoom (int | None, default: None ) –

    Zoom level to use when a result is selected. Uses the map's current zoom if None.

  • geom_type (str, default: 'Point' ) –

    Geometry type of the features being searched: "Point" or "Polygon". Use "Polygon" when searching choropleth or polygon layers.

Returns:

Raises:

  • KeyError –

    If layer_name is provided but not found in the map's feature groups.

Source code in mapyta/map.py
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
def add_search_control(
    self,
    layer_name: str | None = None,
    property_name: str | None = None,
    placeholder: str = "Search...",
    position: str = "topright",
    zoom: int | None = None,
    geom_type: str = "Point",
) -> Self:
    """Add a search control to find features by property value.

    Users can type in the search box to locate and zoom to a matching feature.

    When ``layer_name`` is ``None`` (the default), the search runs over **all** features
    added to the map β€” markers, GeoJSON, choropleth, and cluster layers β€” without
    requiring a feature group. Labels are inferred automatically from the feature's
    properties (``caption``, ``name``, ``naam``, etc.), or taken from ``property_name``
    if supplied. If no features have been added yet, the call is a silent no-op.

    When ``layer_name`` is provided, the search targets only that named feature group,
    preserving the original behaviour.

    Parameters
    ----------
    layer_name : str | None
        Name of the feature group to search (as passed to ``create_feature_group``).
        Pass ``None`` (the default) to search all features on the map automatically.
    property_name : str | None
        GeoJSON property name to use as the search label (e.g. ``"name"``, ``"gemeente"``).
        When ``None`` and ``layer_name`` is also ``None``, the label is inferred from
        common property names (``caption``, ``label``, ``text``, ``name``, ``naam``, ``title``).
    placeholder : str
        Placeholder text in the search input box.
    position : str
        Control position: ``"topleft"``, ``"topright"``, ``"bottomleft"``, ``"bottomright"``.
    zoom : int | None
        Zoom level to use when a result is selected. Uses the map's current zoom if ``None``.
    geom_type : str
        Geometry type of the features being searched: ``"Point"`` or ``"Polygon"``.
        Use ``"Polygon"`` when searching choropleth or polygon layers.

    Returns
    -------
    Map

    Raises
    ------
    KeyError
        If ``layer_name`` is provided but not found in the map's feature groups.
    """
    if layer_name is not None:
        if layer_name not in self._feature_groups:
            available = ", ".join(f'"{n}"' for n in self._feature_groups) or "(none)"
            raise KeyError(f"Feature group {layer_name!r} not found. Available groups: {available}")
        layer: folium.FeatureGroup | folium.GeoJson = self._feature_groups[layer_name]
        search_label = property_name
    else:
        if not self._geojson_features:
            return self
        layer = self._build_hidden_search_layer(property_name)
        layer.add_to(self._map)
        search_label = "_search_label"
    search_kwargs: dict[str, Any] = {
        "layer": layer,
        "geom_type": geom_type,
        "placeholder": placeholder,
        "collapsed": False,
        "search_label": search_label,
        "position": position,
    }
    if zoom is not None:
        search_kwargs["search_zoom"] = zoom
    folium.plugins.Search(**search_kwargs).add_to(self._map)
    return self

map.Map.add_text

add_text(
    point: tuple[float, float] | Point,
    text: str,
    style: dict[str, str] | None = None,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self

Add a text marker at a location.

Parameters:

  • point (tuple[float, float] | Point) –

    (lat, lon) tuple or Shapely Point (lon, lat).

  • text (str) –

    Label text.

  • style (dict[str, str] | None, default: None ) –

    CSS property overrides.

  • tooltip (str | RawHTML | None, default: None ) –

    Markdown tooltip, or RawHTML for pre-formatted HTML.

  • popup (str | RawHTML | None, default: None ) –

    Markdown popup, or RawHTML for pre-formatted HTML.

  • popup_style (PopupStyle | dict[str, Any] | None, default: None ) –

    Popup dimensions.

  • tooltip_style (TooltipStyle | dict[str, Any] | None, default: None ) –

    Tooltip appearance.

  • min_zoom (int | None, default: None ) –

    Minimum zoom level at which the text is visible.

Returns:

Source code in mapyta/map.py
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
def add_text(
    self,
    point: tuple[float, float] | Point,
    text: str,
    style: dict[str, str] | None = None,
    tooltip: str | RawHTML | None = None,
    popup: str | RawHTML | None = None,
    popup_style: PopupStyle | dict[str, Any] | None = None,
    min_zoom: int | None = None,
    tooltip_style: TooltipStyle | dict[str, Any] | None = None,
) -> Self:
    """Add a text marker at a location.

    Parameters
    ----------
    point : tuple[float, float] | Point
        ``(lat, lon)`` tuple or Shapely Point ``(lon, lat)``.
    text : str
        Label text.
    style : dict[str, str] | None
        CSS property overrides.
    tooltip : str | RawHTML | None
        Markdown tooltip, or ``RawHTML`` for pre-formatted HTML.
    popup : str | RawHTML | None
        Markdown popup, or ``RawHTML`` for pre-formatted HTML.
    popup_style : PopupStyle | dict[str, Any] | None
        Popup dimensions.
    tooltip_style : TooltipStyle | dict[str, Any] | None
        Tooltip appearance.
    min_zoom : int | None
        Minimum zoom level at which the text is visible.

    Returns
    -------
    Map
    """
    merged = {**DEFAULT_CAPTION_CSS, "border-radius": "3px", "overflow-wrap": "break-word", **(style or {})}
    if isinstance(point, Point):
        loc = cast(Point, self._transform(point))
        lat, lon = loc.y, loc.x
        self._extend_bounds(loc)
    else:
        lat, lon = point
        self._bounds.append((lat, lon))

    css_str = css_to_style(merged)
    # icon_size is a small fixed box around the anchor (depends on
    # font-size only, not on text length). The text is absolutely
    # centered on the anchor and overflows via overflow:visible, so
    # long labels render fully without widening the Leaflet icon box.
    fs = px_to_int(merged.get("font-size", "12px"), 12)
    w = fs + 10
    h = fs + 10
    html = (
        f'<div style="position:relative;width:{w}px;height:{h}px;overflow:visible;">'
        f'<div style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);'
        f'white-space:nowrap;{css_str}">{text}</div>'
        f"</div>"
    )
    icon = folium.DivIcon(
        html=html,
        icon_size=(w, h),
        icon_anchor=(w // 2, h // 2),
        class_name="",
    )
    marker = folium.Marker(
        location=[lat, lon],
        icon=icon,
        tooltip=self._make_tooltip(tooltip, tooltip_style),
        popup=self._make_popup(popup, popup_style),
    )
    marker.add_to(self._target())
    self._record_feature(
        Point(lon, lat), {"text": text, "tooltip": self._raw_text(tooltip), "popup": self._raw_text(popup), "min_zoom": min_zoom}
    )
    if min_zoom is not None and min_zoom > 0:
        self._zoom_controlled_markers.append(
            {
                "var_name": marker.get_name(),
                "min_zoom": min_zoom,
            }
        )
    return self

map.Map.add_tile_layer

add_tile_layer(
    name: str,
    tiles: str | None = None,
    attribution: str | None = None,
    overlay: bool = False,
) -> Self

Add an additional tile layer.

Parameters:

  • name (str) –

    Display name or TILE_PROVIDERS key.

  • tiles (str | None, default: None ) –

    Tile URL. Looks up name in providers if None.

  • attribution (str | None, default: None ) –

    Tile attribution.

  • overlay (bool, default: False ) –

    Add as overlay vs base layer.

Returns:

Source code in mapyta/map.py
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
def add_tile_layer(
    self,
    name: str,
    tiles: str | None = None,
    attribution: str | None = None,
    overlay: bool = False,
) -> Self:
    """Add an additional tile layer.

    Parameters
    ----------
    name : str
        Display name or ``TILE_PROVIDERS`` key.
    tiles : str | None
        Tile URL. Looks up ``name`` in providers if ``None``.
    attribution : str | None
        Tile attribution.
    overlay : bool
        Add as overlay vs base layer.

    Returns
    -------
    Map
    """
    cfg = self._config
    max_native_zoom = cfg.max_native_zoom if cfg.max_native_zoom is not None else cfg.max_zoom
    provider = TILE_PROVIDERS.get(name.lower())
    display_name = name
    if provider and tiles is None:
        tiles = provider["tiles"]
        attribution = attribution or provider.get("attr")
        display_name = provider.get("name", name)
    folium.TileLayer(
        tiles=tiles or name,
        name=display_name,
        attr=attribution,
        overlay=overlay,
        max_zoom=cfg.max_zoom,
        max_native_zoom=max_native_zoom,
    ).add_to(self._map)
    return self

map.Map.add_timestamped_geojson

add_timestamped_geojson(
    data: dict | str | Path,
    auto_play: bool = True,
    loop: bool = True,
    transition_time: int = 200,
    period: str = "P1D",
    date_options: str = "YYYY-MM-DD HH:mm:ss",
    duration: str | None = None,
) -> Self

Add a GeoJSON layer animated over time.

Each feature in data must carry a times property β€” an array of timestamps (ISO 8601 strings or milliseconds since epoch) with one entry per coordinate in the geometry. A playback control is injected into the map automatically.

Parameters:

  • data (dict | str | Path) –

    GeoJSON FeatureCollection as a Python dict, a JSON string, or a file path. Every feature must have a times property whose length matches its coordinate count.

  • auto_play (bool, default: True ) –

    Start playback when the map loads.

  • loop (bool, default: True ) –

    Loop animation continuously.

  • transition_time (int, default: 200 ) –

    Duration of each frame transition in milliseconds.

  • period (str, default: 'P1D' ) –

    ISO 8601 duration string controlling the slider step (e.g. "P1D" = 1 day, "PT1H" = 1 hour, "PT1M" = 1 minute).

  • date_options (str, default: 'YYYY-MM-DD HH:mm:ss' ) –

    moment.js <https://momentjs.com/docs/#/displaying/>_ format string for the displayed timestamp label.

  • duration (str | None, default: None ) –

    ISO 8601 duration for how long each feature remains visible after its timestamp. None means it stays visible forever once shown.

Returns:

Notes

Supported geometry types: LineString, MultiPoint, MultiLineString, Polygon, MultiPolygon.

Example GeoJSON feature with timestamps::

{
    "type": "Feature",
    "geometry": {
        "type": "LineString",
        "coordinates": [
            [4.90, 52.37],
            [4.91, 52.38],
            [4.92, 52.39],
        ],
    },
    "properties": {
        "times": ["2024-01-01", "2024-01-02", "2024-01-03"],
        "tooltip": "Route segment",
    },
}
Source code in mapyta/map.py
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
def add_timestamped_geojson(
    self,
    data: dict | str | Path,
    auto_play: bool = True,
    loop: bool = True,
    transition_time: int = 200,
    period: str = "P1D",
    date_options: str = "YYYY-MM-DD HH:mm:ss",
    duration: str | None = None,
) -> Self:
    """Add a GeoJSON layer animated over time.

    Each feature in *data* must carry a ``times`` property β€” an array
    of timestamps (ISO 8601 strings or milliseconds since epoch) with
    one entry per coordinate in the geometry.  A playback control is
    injected into the map automatically.

    Parameters
    ----------
    data : dict | str | Path
        GeoJSON ``FeatureCollection`` as a Python dict, a JSON
        string, or a file path.  Every feature must have a ``times``
        property whose length matches its coordinate count.
    auto_play : bool
        Start playback when the map loads.
    loop : bool
        Loop animation continuously.
    transition_time : int
        Duration of each frame transition in milliseconds.
    period : str
        ISO 8601 duration string controlling the slider step
        (e.g. ``"P1D"`` = 1 day, ``"PT1H"`` = 1 hour,
        ``"PT1M"`` = 1 minute).
    date_options : str
        `moment.js <https://momentjs.com/docs/#/displaying/>`_ format
        string for the displayed timestamp label.
    duration : str | None
        ISO 8601 duration for how long each feature remains visible
        after its timestamp.  ``None`` means it stays visible forever
        once shown.

    Returns
    -------
    Map

    Notes
    -----
    Supported geometry types: ``LineString``, ``MultiPoint``,
    ``MultiLineString``, ``Polygon``, ``MultiPolygon``.

    Example GeoJSON feature with timestamps::

        {
            "type": "Feature",
            "geometry": {
                "type": "LineString",
                "coordinates": [
                    [4.90, 52.37],
                    [4.91, 52.38],
                    [4.92, 52.39],
                ],
            },
            "properties": {
                "times": ["2024-01-01", "2024-01-02", "2024-01-03"],
                "tooltip": "Route segment",
            },
        }
    """
    if isinstance(data, Path):
        raw: dict | str = data.read_text(encoding="utf-8")
    else:
        raw = data

    layer = folium.plugins.TimestampedGeoJson(
        data=raw,
        auto_play=auto_play,
        loop=loop,
        transition_time=transition_time,
        period=period,
        date_options=date_options,
        duration=duration,
    )
    layer.add_to(self._target())

    try:
        bounds = layer.get_bounds()
        if bounds:
            self._bounds.extend(cast(list[tuple[float, float]], bounds))
    except Exception:
        pass

    return self

map.Map.create_feature_group

create_feature_group(name: str, show: bool = True) -> Self

Create a named feature group for layer toggling.

Subsequent add_* calls target this group until changed.

To stop targeting a group and return to the base map, call reset_target().

Parameters:

  • name (str) –

    Display name for layer control.

  • show (bool, default: True ) –

    Visible by default.

Returns:

  • Map –

    Self, for chaining.

Source code in mapyta/map.py
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
def create_feature_group(self, name: str, show: bool = True) -> Self:
    """Create a named feature group for layer toggling.

    Subsequent ``add_*`` calls target this group until changed.

    To stop targeting a group and return to the base map, call ``reset_target()``.

    Parameters
    ----------
    name : str
        Display name for layer control.
    show : bool
        Visible by default.

    Returns
    -------
    Map
        Self, for chaining.
    """
    fg = folium.FeatureGroup(name=name, show=show)
    fg.add_to(self._map)
    self._feature_groups[name] = fg
    self._active_group = fg
    return self

map.Map.enable_draw

enable_draw(
    tools: list[DrawTool] | None = None,
    on_submit: str | RawJS | None = None,
    position: str = "topleft",
    submit_label: str = "Submit",
    draw_style: dict[str, Any] | None = None,
    edit: bool = True,
    delete_confirm_message: str = "Delete this shape?",
    delete_confirm_yes: str = "Delete",
    delete_confirm_no: str = "Cancel",
) -> Self

Enable drawing controls on the map.

Parameters:

  • tools (list[DrawTool] | None, default: None ) –

    Active drawing tools. Valid values: "marker", "polyline", "polygon", "rectangle", "circle". Defaults to ["polyline", "polygon", "marker"].

  • on_submit (str | RawJS | None, default: None ) –

    Callback when the user clicks Submit. None downloads a GeoJSON file; a URL string sends a POST request; a plain string calls window["name"](geojson); a :class:RawJS instance is inlined verbatim.

  • position (str, default: 'topleft' ) –

    Leaflet control position.

  • submit_label (str, default: 'Submit' ) –

    Label for the submit button.

  • draw_style (dict[str, Any] | None, default: None ) –

    shapeOptions override for drawn shapes.

  • edit (bool, default: True ) –

    Whether per-shape editing/deletion is active. When True, clicking a drawn shape makes its vertices editable in place (click empty map space to stop editing) and shows a trashbin at its last vertex; clicking the trashbin and confirming deletes that shape. No global edit/delete toolbar buttons are rendered. When False, clicking a shape stays inert.

  • delete_confirm_message (str, default: 'Delete this shape?' ) –

    Text shown in the in-map deletion confirmation popup.

  • delete_confirm_yes (str, default: 'Delete' ) –

    Label of the confirm (delete) button in that popup.

  • delete_confirm_no (str, default: 'Cancel' ) –

    Label of the cancel button in that popup.

Returns:

Raises:

  • ValueError –

    If tools contains an invalid tool name.

Source code in mapyta/map.py
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
def enable_draw(
    self,
    tools: list[DrawTool] | None = None,
    on_submit: str | RawJS | None = None,
    position: str = "topleft",
    submit_label: str = "Submit",
    draw_style: dict[str, Any] | None = None,
    edit: bool = True,
    delete_confirm_message: str = "Delete this shape?",
    delete_confirm_yes: str = "Delete",
    delete_confirm_no: str = "Cancel",
) -> Self:
    """Enable drawing controls on the map.

    Parameters
    ----------
    tools : list[DrawTool] | None
        Active drawing tools.  Valid values: ``"marker"``,
        ``"polyline"``, ``"polygon"``, ``"rectangle"``, ``"circle"``.
        Defaults to ``["polyline", "polygon", "marker"]``.
    on_submit : str | RawJS | None
        Callback when the user clicks Submit.  ``None`` downloads a
        GeoJSON file; a URL string sends a ``POST`` request; a plain
        string calls ``window["name"](geojson)``; a :class:`RawJS`
        instance is inlined verbatim.
    position : str
        Leaflet control position.
    submit_label : str
        Label for the submit button.
    draw_style : dict[str, Any] | None
        ``shapeOptions`` override for drawn shapes.
    edit : bool
        Whether per-shape editing/deletion is active. When ``True``,
        clicking a drawn shape makes its vertices editable in place (click
        empty map space to stop editing) and shows a trashbin at its last
        vertex; clicking the trashbin and confirming deletes that shape.
        No global edit/delete toolbar buttons are rendered. When ``False``,
        clicking a shape stays inert.
    delete_confirm_message : str
        Text shown in the in-map deletion confirmation popup.
    delete_confirm_yes : str
        Label of the confirm (delete) button in that popup.
    delete_confirm_no : str
        Label of the cancel button in that popup.

    Returns
    -------
    Map

    Raises
    ------
    ValueError
        If *tools* contains an invalid tool name.
    """
    if tools is None:
        tools = ["polyline", "polygon", "marker"]

    invalid = set(tools) - VALID_DRAW_TOOLS
    if invalid:
        msg = f"Invalid draw tool(s): {', '.join(sorted(invalid))}. Valid: {', '.join(sorted(VALID_DRAW_TOOLS))}"
        raise ValueError(msg)

    self._draw_config = DrawConfig(
        tools=tools,
        on_submit=on_submit,
        position=position,
        submit_label=submit_label,
        draw_style=draw_style,
        edit=edit,
        delete_confirm_message=delete_confirm_message,
        delete_confirm_yes=delete_confirm_yes,
        delete_confirm_no=delete_confirm_no,
    )
    self._draw_injected = False
    return self

map.Map.from_geodataframe classmethod

from_geodataframe(
    gdf: Any,
    hover_columns: list[str] | None = None,
    popup_columns: list[str] | None = None,
    label_column: str | None = None,
    color_column: str | None = None,
    stroke: StrokeStyle | dict[str, Any] | None = None,
    fill: FillStyle | dict[str, Any] | None = None,
    marker_style: dict[str, str] | None = None,
    title: str | None = None,
    config: MapConfig | None = None,
    legend_name: str | None = None,
    colors: list[str] | str | None = None,
) -> Self

Create a GeoMap from a GeoPandas GeoDataFrame.

Parameters:

  • gdf (GeoDataFrame) –

    GeoDataFrame with a geometry column.

  • hover_columns (list[str] | None, default: None ) –

    Columns for tooltip.

  • popup_columns (list[str] | None, default: None ) –

    Columns for click popup.

  • label_column (str | None, default: None ) –

    Column with emoji/text labels for points.

  • color_column (str | None, default: None ) –

    Numeric column for choropleth coloring.

  • stroke (StrokeStyle | None, default: None ) –

    Default border style.

  • fill (FillStyle | None, default: None ) –

    Default fill style.

  • marker_style (dict[str, str] | None, default: None ) –

    CSS property overrides for location markers.

  • title (str | None, default: None ) –

    Map title.

  • config (MapConfig | None, default: None ) –

    Map configuration.

  • legend_name (str | None, default: None ) –

    Color scale label.

  • colors (list[str] | str | None, default: None ) –

    Color palette for color_column. Pass a palette name (e.g. "blues") or a list of hex color strings. See mapyta.PALETTES for available names. Defaults to "ylrd" (yellow β†’ red).

Returns:

Raises:

  • ImportError –

    If geopandas is not installed.

Source code in mapyta/map.py
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
@classmethod
def from_geodataframe(  # noqa: PLR0913, C901, PLR0912
    cls,
    gdf: Any,  # noqa: ANN401
    hover_columns: list[str] | None = None,
    popup_columns: list[str] | None = None,
    label_column: str | None = None,
    color_column: str | None = None,
    stroke: StrokeStyle | dict[str, Any] | None = None,
    fill: FillStyle | dict[str, Any] | None = None,
    marker_style: dict[str, str] | None = None,
    title: str | None = None,
    config: MapConfig | None = None,
    legend_name: str | None = None,
    colors: list[str] | str | None = None,
) -> Self:
    """Create a GeoMap from a GeoPandas GeoDataFrame.

    Parameters
    ----------
    gdf : geopandas.GeoDataFrame
        GeoDataFrame with a geometry column.
    hover_columns : list[str] | None
        Columns for tooltip.
    popup_columns : list[str] | None
        Columns for click popup.
    label_column : str | None
        Column with emoji/text labels for points.
    color_column : str | None
        Numeric column for choropleth coloring.
    stroke : StrokeStyle | None
        Default border style.
    fill : FillStyle | None
        Default fill style.
    marker_style : dict[str, str] | None
        CSS property overrides for location markers.
    title : str | None
        Map title.
    config : MapConfig | None
        Map configuration.
    legend_name : str | None
        Color scale label.
    colors : list[str] | str | None
        Color palette for ``color_column``. Pass a palette name (e.g. ``"blues"``)
        or a list of hex color strings. See ``mapyta.PALETTES`` for available names.
        Defaults to ``"ylrd"`` (yellow β†’ red).

    Returns
    -------
    Map

    Raises
    ------
    ImportError
        If geopandas is not installed.
    """
    try:
        import geopandas  # noqa: PLC0415, F401
    except ImportError:
        raise ImportError("from_geodataframe() requires geopandas. Install it with:\n  pip install geopandas") from None

    # Reproject to WGS84 if needed
    if gdf.crs and str(gdf.crs) != "EPSG:4326":
        gdf = gdf.to_crs("EPSG:4326")

    # Validate column references
    available = list(gdf.columns)
    for param_name, cols in [("hover_columns", hover_columns or []), ("popup_columns", popup_columns or [])]:
        missing = [c for c in cols if c not in gdf.columns]
        if missing:
            warnings.warn(
                f"from_geodataframe(): {param_name} contains column(s) not found in the GeoDataFrame: {missing}. Available columns: {available}",
                UserWarning,
                stacklevel=2,
            )
    for param_name, col in [("label_column", label_column), ("color_column", color_column)]:
        if col is not None and col not in gdf.columns:
            warnings.warn(
                f"from_geodataframe(): {param_name}={col!r} not found in the GeoDataFrame. Available columns: {available}",
                UserWarning,
                stacklevel=2,
            )

    m = cls(title=title, config=config)

    # Build colormap
    colormap = None
    if color_column and color_column in gdf.columns:
        vals = gdf[color_column].dropna()
        if len(vals) > 0:
            vmin, vmax = float(vals.min()), float(vals.max())
            colormap = m._build_colormap(
                colors=colors,
                vmin=vmin,
                vmax=vmax,
                caption=legend_name or color_column,
            )
            m._register_colormap(colormap)

    # Iterate rows
    for idx, row in gdf.iterrows():
        geom = row.geometry

        if not isinstance(geom, BaseGeometry):
            continue

        if geom is None or geom.is_empty:
            continue

        # Build tooltip/popup text
        tooltip = None
        if hover_columns:
            parts = [f"**{c}**: {row[c]}" for c in hover_columns if c in row.index]
            tooltip = "\n".join(parts) if parts else None

        popup = None
        if popup_columns:
            parts = [f"**{c}**: {row[c]}" for c in popup_columns if c in row.index]
            popup = "\n".join(parts) if parts else None

        lbl = str(row[label_column]) if label_column and label_column in row.index else None

        # Resolve color
        cur_fill, cur_stroke = fill, stroke
        if colormap and color_column and color_column in row.index:
            val = row[color_column]
            if val is not None and not (isinstance(val, float) and math.isnan(val)):
                c = colormap(float(val))
                _fill_base = fill if isinstance(fill, FillStyle) else FillStyle()
                _stroke_base = stroke if isinstance(stroke, StrokeStyle) else StrokeStyle()
                cur_fill = FillStyle(color=c, opacity=_fill_base.opacity)
                cur_stroke = StrokeStyle(
                    color=c,
                    weight=_stroke_base.weight,
                    opacity=_stroke_base.opacity,
                )

        m.add_geometry(
            geom=geom,
            tooltip=tooltip,
            popup=popup,
            label=lbl,
            stroke=cur_stroke,
            fill=cur_fill,
            marker_style=marker_style,
        )

    return m

map.Map.get_standalone_html

get_standalone_html() -> str

Render map to a full standalone HTML document.

Source code in mapyta/map.py
2947
2948
2949
2950
def get_standalone_html(self) -> str:
    """Render map to a full standalone HTML document."""
    self._ensure_rendered()
    return self._map.get_root().render()

map.Map.reset_target

reset_target() -> Self

Reset target to base map (no feature group).

Returns:

Source code in mapyta/map.py
459
460
461
462
463
464
465
466
467
def reset_target(self) -> Self:
    """Reset target to base map (no feature group).

    Returns
    -------
    Map
    """
    self._active_group = self._map
    return self

map.Map.set_bounds

set_bounds(padding: float = 0.0, restrict: bool = False) -> Self

Fit map view to tracked bounds with optional padding and restriction.

Parameters:

  • padding (float, default: 0.0 ) –

    Padding in degrees around the data bounds.

  • restrict (bool, default: False ) –

    If True, prevent the user from panning/zooming beyond the data bounds (sets maxBounds and maxBoundsViscosity).

Returns:

  • Map –

    Self, for chaining.

Source code in mapyta/map.py
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
def set_bounds(self, padding: float = 0.0, restrict: bool = False) -> Self:
    """Fit map view to tracked bounds with optional padding and restriction.

    Parameters
    ----------
    padding : float
        Padding in degrees around the data bounds.
    restrict : bool
        If ``True``, prevent the user from panning/zooming beyond the
        data bounds (sets ``maxBounds`` and ``maxBoundsViscosity``).

    Returns
    -------
    Map
        Self, for chaining.
    """
    if not self._bounds:
        return self

    lats = [b[0] for b in self._bounds]
    lons = [b[1] for b in self._bounds]
    bounds = [
        [min(lats) - padding, min(lons) - padding],
        [max(lats) + padding, max(lons) + padding],
    ]
    self._map.fit_bounds(bounds)

    if restrict:
        self._map.options["maxBounds"] = bounds
        self._map.options["maxBoundsViscosity"] = 1.0

    return self

map.Map.set_feature_group

set_feature_group(name: str) -> Self

Activate an existing feature group.

Parameters:

  • name (str) –

    Feature group name.

Returns:

Raises:

  • KeyError –

    If group does not exist.

Source code in mapyta/map.py
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
def set_feature_group(self, name: str) -> Self:
    """Activate an existing feature group.

    Parameters
    ----------
    name : str
        Feature group name.

    Returns
    -------
    Map

    Raises
    ------
    KeyError
        If group does not exist.
    """
    if name not in self._feature_groups:
        raise KeyError(f"Feature group '{name}' not found. Available: {list(self._feature_groups.keys())}")
    self._active_group = self._feature_groups[name]
    return self

map.Map.to_bytesio

to_bytesio(
    width: int = 1200,
    height: int = 800,
    delay: float = 2.0,
    hide_controls: bool = True,
    scale: float = 1.0,
) -> BytesIO

Export as PNG in a BytesIO buffer.

Parameters:

  • width (int, default: 1200 ) –

    Viewport width.

  • height (int, default: 800 ) –

    Viewport height.

  • delay (float, default: 2.0 ) –

    Tile loading wait time.

  • hide_controls (bool, default: True ) –

    If True, hide Leaflet UI controls.

  • scale (float, default: 1.0 ) –

    Output resolution multiplier. scale=2.0 produces a 2Γ— (high-DPI) image.

Returns:

  • BytesIO –

    Buffer at position 0.

Source code in mapyta/map.py
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
def to_bytesio(
    self,
    width: int = 1200,
    height: int = 800,
    delay: float = 2.0,
    hide_controls: bool = True,
    scale: float = 1.0,
) -> io.BytesIO:
    """Export as PNG in a BytesIO buffer.

    Parameters
    ----------
    width : int
        Viewport width.
    height : int
        Viewport height.
    delay : float
        Tile loading wait time.
    hide_controls : bool
        If ``True``, hide Leaflet UI controls.
    scale : float
        Output resolution multiplier. ``scale=2.0`` produces a 2Γ— (high-DPI) image.

    Returns
    -------
    io.BytesIO
        Buffer at position 0.
    """
    png = self.to_image(path=None, width=width, height=height, delay=delay, hide_controls=hide_controls, scale=scale)
    buf = io.BytesIO(png)
    buf.seek(0)
    return buf

map.Map.to_geojson

to_geojson(path: None = None) -> dict
to_geojson(path: str | Path) -> Path
to_geojson(path: str | Path | None = None) -> dict | Path

Export tracked features as a GeoJSON FeatureCollection.

Parameters:

  • path (str | Path | None, default: None ) –

    Output file path. When None, the FeatureCollection dict is returned instead of being written to disk.

Returns:

  • dict | Path –

    FeatureCollection dict when path is None, otherwise the resolved output :class:~pathlib.Path.

Source code in mapyta/map.py
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
def to_geojson(self, path: str | Path | None = None) -> dict | Path:
    """Export tracked features as a GeoJSON FeatureCollection.

    Parameters
    ----------
    path : str | Path | None
        Output file path.  When ``None``, the FeatureCollection dict is
        returned instead of being written to disk.

    Returns
    -------
    dict | Path
        FeatureCollection dict when *path* is ``None``, otherwise the
        resolved output :class:`~pathlib.Path`.
    """
    fc = self._build_geojson_collection()
    if path is None:
        return fc
    out = Path(path)
    out.write_text(json.dumps(fc, indent=2, ensure_ascii=False), encoding="utf-8")
    return out

map.Map.to_html

to_html(path: None = None, open_in_browser: bool = False) -> str
to_html(path: str | Path, open_in_browser: bool = False) -> Path
to_html(
    path: str | Path | None = None, open_in_browser: bool = False
) -> str | Path

Export as standalone HTML.

Parameters:

  • path (str | Path | None, default: None ) –

    Output file path. When None, the full HTML document is returned as a string instead of being written to disk.

  • open_in_browser (bool, default: False ) –

    If True and path is given, open the file in the default browser after saving. Ignored when path is None.

Returns:

  • str | Path –

    HTML string when path is None, otherwise the resolved output :class:~pathlib.Path.

Source code in mapyta/map.py
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
def to_html(self, path: str | Path | None = None, open_in_browser: bool = False) -> str | Path:
    """Export as standalone HTML.

    Parameters
    ----------
    path : str | Path | None
        Output file path.  When ``None``, the full HTML document is
        returned as a string instead of being written to disk.
    open_in_browser : bool
        If ``True`` and *path* is given, open the file in the default
        browser after saving.  Ignored when *path* is ``None``.

    Returns
    -------
    str | Path
        HTML string when *path* is ``None``, otherwise the resolved
        output :class:`~pathlib.Path`.
    """
    if path is None:
        return self._get_html()
    out = Path(path)
    out.write_text(self.get_standalone_html(), encoding="utf-8")
    if open_in_browser:
        webbrowser.open(out.resolve().as_uri())
    return out

map.Map.to_image

to_image(
    path: None = None,
    width: int = 1200,
    height: int = 800,
    delay: float = 0.5,
    hide_controls: bool = True,
    scale: float = 1.0,
) -> bytes
to_image(
    path: str | Path,
    width: int = 1200,
    height: int = 800,
    delay: float = 0.5,
    hide_controls: bool = True,
    scale: float = 1.0,
) -> Path
to_image(
    path: str | Path | None = None,
    width: int = 1200,
    height: int = 800,
    delay: float = 0.5,
    hide_controls: bool = True,
    scale: float = 1.0,
) -> bytes | Path

Save the map as a PNG image.

Parameters:

  • path (str | Path | None, default: None ) –

    Output path. Returns bytes if None.

  • width (int, default: 1200 ) –

    Viewport width in px.

  • height (int, default: 800 ) –

    Viewport height in px.

  • delay (float, default: 0.5 ) –

    Seconds to wait for tiles.

  • hide_controls (bool, default: True ) –

    If True, inject CSS to hide Leaflet UI controls in the exported image.

  • scale (float, default: 1.0 ) –

    Output resolution multiplier. scale=2.0 produces a 2Γ— (high-DPI) image at width * 2 Γ— height * 2 pixels. Defaults to 1.0.

Returns:

  • bytes | Path –

Raises:

  • ImportError –

    If selenium not installed.

  • RuntimeError –

    If Chrome/chromedriver not found.

Source code in mapyta/map.py
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
def to_image(
    self,
    path: str | Path | None = None,
    width: int = 1200,
    height: int = 800,
    delay: float = 0.50,
    hide_controls: bool = True,
    scale: float = 1.0,
) -> bytes | Path:
    """Save the map as a PNG image.

    Parameters
    ----------
    path : str | Path | None
        Output path. Returns bytes if ``None``.
    width : int
        Viewport width in px.
    height : int
        Viewport height in px.
    delay : float
        Seconds to wait for tiles.
    hide_controls : bool
        If ``True``, inject CSS to hide Leaflet UI controls in the
        exported image.
    scale : float
        Output resolution multiplier. ``scale=2.0`` produces a 2Γ— (high-DPI)
        image at ``width * 2`` Γ— ``height * 2`` pixels. Defaults to ``1.0``.

    Returns
    -------
    bytes | Path

    Raises
    ------
    ImportError
        If selenium not installed.
    RuntimeError
        If Chrome/chromedriver not found.
    """
    with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as tmp:
        self.to_html(tmp.name)
        tmp_path = tmp.name
    try:
        if hide_controls:
            content = Path(tmp_path).read_text(encoding="utf-8")
            hide_css = "<style>.leaflet-control{display:none !important;}</style>"
            content = content.replace("</head>", f"{hide_css}\n</head>", 1)
            Path(tmp_path).write_text(content, encoding="utf-8")
        png_bytes = capture_screenshot(
            html_path=tmp_path,
            width=width,
            height=height,
            delay=delay,
            scale=scale,
        )
    finally:
        Path(tmp_path).unlink(missing_ok=True)

    if path is None:
        return png_bytes
    out = Path(path)
    out.write_bytes(png_bytes)
    return out

map.Map.to_image_async async

to_image_async(
    path: str | Path | None = None,
    width: int = 1200,
    height: int = 800,
    delay: float = 2.0,
    hide_controls: bool = True,
    scale: float = 1.0,
) -> bytes | Path

Async PNG export (runs Selenium in executor).

Parameters:

  • path (str | Path | None, default: None ) –

    See to_image.

  • width (str | Path | None, default: None ) –

    See to_image.

  • height (str | Path | None, default: None ) –

    See to_image.

  • delay (str | Path | None, default: None ) –

    See to_image.

  • hide_controls (str | Path | None, default: None ) –

    See to_image.

  • scale (str | Path | None, default: None ) –

    See to_image.

Returns:

  • bytes | Path –
Source code in mapyta/map.py
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
async def to_image_async(
    self,
    path: str | Path | None = None,
    width: int = 1200,
    height: int = 800,
    delay: float = 2.0,
    hide_controls: bool = True,
    scale: float = 1.0,
) -> bytes | Path:
    """Async PNG export (runs Selenium in executor).

    Parameters
    ----------
    path, width, height, delay, hide_controls, scale
        See ``to_image``.

    Returns
    -------
    bytes | Path
    """
    loop = asyncio.get_running_loop()
    return await loop.run_in_executor(
        None,
        lambda: self.to_image(
            path=path,
            width=width,
            height=height,
            delay=delay,
            hide_controls=hide_controls,
            scale=scale,
        ),
    )