Skip to content

GIS

golit.gis — reactive maps as ordinary views: vector data (GeoDataFrames), raster data (georeferenced arrays), and DuckDB spatial SQL, all rendered with native MapLibre GL. Install with pip install "golit[gis]" (vector) and "golit[gis-raster]" (raster); everything heavy is imported lazily, so import golit never pulls it in. See the Maps & GIS tutorial for usage.

maplibre

A native MapLibre GL map from a style and a camera.

maplibre

maplibre(
    style: Any,
    *,
    center: Any = None,
    zoom: float | None = None,
    pitch: float = 0,
    bearing: float = 0,
    height: str = "420px",
    **opts: Any,
) -> str

A native MapLibre GL map from a style and camera, as a reactive view fragment.

style is either a style-URL string (e.g. a vector tile style) or a full MapLibre style dict ({"version": 8, "sources": {...}, "layers": [...]}). center is [lng, lat]; zoom the zoom level; pitch/bearing tilt and rotate for a 3D view (combine with a style carrying fill-extrusion or terrain layers). Extra keyword args (minZoom, maxZoom, bounds, …) pass straight to the map spec::

@app.view
def map(city):
    return gis.maplibre(
        "https://demotiles.maplibre.org/style.json",
        center=[city["lng"], city["lat"]], zoom=11, pitch=45,
    )

Pure dict assembly — imports nothing. The mount hydrates on first paint and on every POST/SSE swap, and its WebGL context is freed when the fragment is replaced.

Source code in python/golit/gis.py
def maplibre(
    style: Any,
    *,
    center: Any = None,
    zoom: float | None = None,
    pitch: float = 0,
    bearing: float = 0,
    height: str = "420px",
    **opts: Any,
) -> str:
    """A native MapLibre GL map from a ``style`` and camera, as a reactive view fragment.

    ``style`` is either a style-URL string (e.g. a vector tile style) or a full MapLibre
    style ``dict`` (``{"version": 8, "sources": {...}, "layers": [...]}``). ``center`` is
    ``[lng, lat]``; ``zoom`` the zoom level; ``pitch``/``bearing`` tilt and rotate for a
    3D view (combine with a style carrying ``fill-extrusion`` or ``terrain`` layers). Extra
    keyword args (``minZoom``, ``maxZoom``, ``bounds``, …) pass straight to the map spec::

        @app.view
        def map(city):
            return gis.maplibre(
                "https://demotiles.maplibre.org/style.json",
                center=[city["lng"], city["lat"]], zoom=11, pitch=45,
            )

    Pure dict assembly — imports nothing. The mount hydrates on first paint and on every
    POST/SSE swap, and its WebGL context is freed when the fragment is replaced."""
    spec: dict[str, Any] = {"style": style, "height": height}
    if center is not None:
        spec["center"] = list(center)
    if zoom is not None:
        spec["zoom"] = zoom
    if pitch:
        spec["pitch"] = pitch
    if bearing:
        spec["bearing"] = bearing
    spec.update(opts)
    return chart_spec("maplibre", spec)

geo_map

A GeoPandas GeoDataFrame straight to a MapLibre choropleth / line / point map. A view may also just return a GeoDataFramerender_value routes it here with defaults.

geo_map

geo_map(
    gdf: Any,
    *,
    color: str | None = None,
    tooltip: Any = None,
    tooltip_trigger: str = "click",
    basemap: Any = "default",
    fit: bool = True,
    fit_padding: int = 24,
    legend: bool = True,
    geometry: str | None = None,
    height: str = "420px",
    **opts: Any,
) -> str

Render a GeoPandas GeoDataFrame as a native MapLibre map.

The frame is reprojected to EPSG:4326 if needed, serialized to GeoJSON, and added to a MapLibre style as a source with a fill (polygons), line (lines), or circle (points) layer picked from the geometry type. color names a column to drive the fill — a blue ramp for a numeric column (a choropleth), a categorical palette for a text one; when color is set, a legend is overlaid (gradient bar or swatches) unless turned off. tooltip shows feature properties: True for every attribute, or a column name / list of names — on click, or on hover with tooltip_trigger="hover". basemap is a vector preset ("default"/"positron", "liberty", "bright", "dark" — free OpenFreeMap styles, no API key), a raster preset ("osm", "carto-light", "carto-dark"), "none", a full style dict, or any style-URL string. With fit the camera frames the data (fit_padding px)::

@app.view
def map(regions):                  # regions is a filtered GeoDataFrame
    return gis.geo_map(regions, color="revenue", tooltip=["name", "revenue"])

A plain Polars/pandas frame works too if you name its geometry column with geometry= (WKB/WKT/shapely) — the bridge from :func:spatial_sql::

return gis.geo_map(gis.spatial_sql("SELECT name, geom FROM p …", p=places),
                   geometry="geom", color="name")

A view may also just return a GeoDataFrame — :func:golit.rendering.render_value routes it here with defaults. Requires pip install "golit[gis]".

Source code in python/golit/gis.py
def geo_map(
    gdf: Any,
    *,
    color: str | None = None,
    tooltip: Any = None,
    tooltip_trigger: str = "click",
    basemap: Any = "default",
    fit: bool = True,
    fit_padding: int = 24,
    legend: bool = True,
    geometry: str | None = None,
    height: str = "420px",
    **opts: Any,
) -> str:
    """Render a GeoPandas ``GeoDataFrame`` as a native MapLibre map.

    The frame is reprojected to EPSG:4326 if needed, serialized to GeoJSON, and added to
    a MapLibre style as a source with a fill (polygons), line (lines), or circle (points)
    layer picked from the geometry type. ``color`` names a column to drive the fill — a
    blue ramp for a numeric column (a choropleth), a categorical palette for a text one;
    when ``color`` is set, a ``legend`` is overlaid (gradient bar or swatches) unless
    turned off. ``tooltip`` shows feature properties: ``True`` for every attribute, or a
    column name / list of names — on click, or on hover with ``tooltip_trigger="hover"``.
    ``basemap`` is a vector preset (``"default"``/``"positron"``, ``"liberty"``,
    ``"bright"``, ``"dark"`` — free OpenFreeMap styles, no API key), a raster preset
    (``"osm"``, ``"carto-light"``, ``"carto-dark"``), ``"none"``, a full style ``dict``,
    or any style-**URL** string. With ``fit`` the camera frames the data (``fit_padding`` px)::

        @app.view
        def map(regions):                  # regions is a filtered GeoDataFrame
            return gis.geo_map(regions, color="revenue", tooltip=["name", "revenue"])

    A plain Polars/pandas frame works too if you name its geometry column with
    ``geometry=`` (WKB/WKT/shapely) — the bridge from :func:`spatial_sql`::

        return gis.geo_map(gis.spatial_sql("SELECT name, geom FROM p …", p=places),
                           geometry="geom", color="name")

    A view may also just ``return`` a GeoDataFrame — :func:`golit.rendering.render_value`
    routes it here with defaults. Requires ``pip install "golit[gis]"``."""
    import json

    if not is_geodataframe(gdf):
        if geometry is None:
            raise TypeError(
                "geo_map expects a GeoDataFrame, or a frame plus geometry=<column name>"
            )
        gdf = to_geo(gdf, geometry=geometry)

    crs = getattr(gdf, "crs", None)
    if crs is not None and crs.to_epsg() != 4326:
        gdf = gdf.to_crs(epsg=4326)

    mapping = _color_mapping(gdf, color) if color else None
    geojson = json.loads(gdf.to_json())
    source = {"type": "geojson", "data": geojson}
    layers = _data_layers(gdf, mapping)

    base = _base_style(basemap)
    spec: dict[str, Any] = {"height": height}
    if isinstance(base, str):
        # vector style URL — overlay the data once the remote style finishes loading
        spec["style"] = base
        spec["overlay"] = {"sources": {_SOURCE: source}, "layers": layers}
    else:
        base["sources"][_SOURCE] = source
        base["layers"].extend(layers)
        spec["style"] = base

    if fit and "bounds" not in opts:
        minx, miny, maxx, maxy = (float(v) for v in gdf.total_bounds)
        spec["bounds"] = [[minx, miny], [maxx, maxy]]
        if fit_padding != 24:
            spec["fitPadding"] = fit_padding

    fields = _tooltip_fields(gdf, tooltip)
    if fields:
        spec["tooltip"] = fields
        spec["tooltipLayer"] = _SOURCE
        if tooltip_trigger != "click":
            spec["tooltipTrigger"] = tooltip_trigger

    spec.update(opts)
    mount = chart_spec("maplibre", spec)
    if legend and mapping is not None and color is not None:
        # Overlay the legend on the map; the relative wrapper anchors the absolute legend.
        return f'<div class="golit-map-wrap relative">{mount}{_legend_html(color, mapping)}</div>'
    return mount

vector_tiles

Serve a large GeoDataFrame as on-demand MVT vector tiles — the data stays server-side, only the visible tiles cross the wire (the vector analog of tiles). Needs pip install "golit[gis,gis-vector-tiles]".

vector_tiles

vector_tiles(
    gdf: Any,
    *,
    color: str | None = None,
    tooltip: Any = None,
    tooltip_trigger: str = "click",
    properties: Any = None,
    basemap: Any = "default",
    fit: bool = True,
    fit_padding: int = 24,
    legend: bool = True,
    geometry: str | None = None,
    min_zoom: int = 0,
    max_zoom: int = 14,
    height: str = "420px",
    **opts: Any,
) -> str

Render a large GeoDataFrame as server-side vector tiles (GIS phase 2.5).

Where :func:geo_map inlines the whole GeoJSON into the page — fine for thousands of features, not for hundreds of thousands — vector_tiles keeps the data server-side and streams only the features in each z/x/y tile as a Mapbox Vector Tile (MVT). The map looks the same (color choropleth, tooltip popups, basemap, legend) but scales: nothing but the visible tiles crosses the wire, and the GPU styles the vector features client-side::

@app.view
def map(parcels):                          # parcels: a big GeoDataFrame
    return gis.vector_tiles(parcels, color="value", tooltip=["id", "value"])

The frame is reprojected to Web Mercator and registered under an opaque token; the /gis/vector route encodes tiles on demand. properties limits which columns ride in the tiles (the color column and tooltip fields are always included); min_zoom / max_zoom bound the tile pyramid (MapLibre over-zooms past max_zoom). A plain Polars/pandas frame works with geometry=<column> (as in :func:geo_map). Requires pip install "golit[gis,gis-vector-tiles]".

Source code in python/golit/gis.py
def vector_tiles(
    gdf: Any,
    *,
    color: str | None = None,
    tooltip: Any = None,
    tooltip_trigger: str = "click",
    properties: Any = None,
    basemap: Any = "default",
    fit: bool = True,
    fit_padding: int = 24,
    legend: bool = True,
    geometry: str | None = None,
    min_zoom: int = 0,
    max_zoom: int = 14,
    height: str = "420px",
    **opts: Any,
) -> str:
    """Render a **large** ``GeoDataFrame`` as server-side **vector tiles** (GIS phase 2.5).

    Where :func:`geo_map` inlines the whole GeoJSON into the page — fine for thousands of
    features, not for hundreds of thousands — ``vector_tiles`` keeps the data **server-side**
    and streams only the features in each ``z/x/y`` tile as a Mapbox Vector Tile (MVT). The
    map looks the same (``color`` choropleth, ``tooltip`` popups, ``basemap``, ``legend``) but
    scales: nothing but the visible tiles crosses the wire, and the GPU styles the vector
    features client-side::

        @app.view
        def map(parcels):                          # parcels: a big GeoDataFrame
            return gis.vector_tiles(parcels, color="value", tooltip=["id", "value"])

    The frame is reprojected to Web Mercator and registered under an opaque token; the
    ``/gis/vector`` route encodes tiles on demand. ``properties`` limits which columns ride in
    the tiles (the ``color`` column and ``tooltip`` fields are always included); ``min_zoom`` /
    ``max_zoom`` bound the tile pyramid (MapLibre over-zooms past ``max_zoom``). A plain
    Polars/pandas frame works with ``geometry=<column>`` (as in :func:`geo_map`). Requires
    ``pip install "golit[gis,gis-vector-tiles]"``."""
    from .server.vector_tiles import register_source

    if not is_geodataframe(gdf):
        if geometry is None:
            raise TypeError(
                "vector_tiles expects a GeoDataFrame, or a frame plus geometry=<column name>"
            )
        gdf = to_geo(gdf, geometry=geometry)

    mapping = _color_mapping(gdf, color) if color else None

    # Columns to embed as MVT feature properties — everything by default, or an explicit list
    # plus whatever the choropleth/tooltips need so they keep working client-side.
    geom_name = gdf.geometry.name
    if properties is None:
        cols = [c for c in gdf.columns if c != geom_name]
    else:
        wanted = set(properties)
        if color:
            wanted.add(color)
        wanted.update(_tooltip_fields(gdf, tooltip) or [])
        cols = [c for c in gdf.columns if c != geom_name and c in wanted]

    crs = getattr(gdf, "crs", None)
    web = gdf.to_crs(epsg=3857) if crs is not None else gdf  # the tiling CRS (Web Mercator)

    from pyproj import Transformer

    to4326 = Transformer.from_crs(3857, 4326, always_xy=True)
    minx, miny, maxx, maxy = (float(v) for v in web.total_bounds)
    west, south = to4326.transform(minx, miny)
    east, north = to4326.transform(maxx, maxy)

    token = _tile_token("vector", len(web), sorted(map(str, cols)), color,
                        round(west, 5), round(south, 5), round(east, 5), round(north, 5),
                        min_zoom, max_zoom, _attrs_fingerprint(web, cols))
    register_source(token, {"gdf": web, "properties": cols, "layer": _VTILE_LAYER})

    source = {
        "type": "vector",
        "tiles": [f"/gis/vector/{token}/{{z}}/{{x}}/{{y}}"],
        "minzoom": min_zoom,
        "maxzoom": max_zoom,
    }
    layers = _data_layers(gdf, mapping, source_layer=_VTILE_LAYER)

    base = _base_style(basemap)
    basemap_key = _basemap_key(base)
    spec: dict[str, Any] = {"height": height}
    if isinstance(base, str):
        spec["style"] = base
        spec["overlay"] = {"sources": {_SOURCE: source}, "layers": layers}
    else:
        base["sources"][_SOURCE] = source
        base["layers"].extend(layers)
        spec["style"] = base

    # Lets the client update the vector overlay in place when the basemap is unchanged:
    # swap the tile URLs (setTiles) and re-apply the data layers' paint (e.g. the choropleth
    # colour expression), keeping the basemap and camera; otherwise fall back to a remount.
    spec["vectorLayer"] = {"source": _SOURCE, "tiles": source["tiles"], "layers": layers}
    spec["basemapKey"] = basemap_key

    if fit and "bounds" not in opts:
        spec["bounds"] = [[west, south], [east, north]]
        if fit_padding != 24:
            spec["fitPadding"] = fit_padding

    fields = _tooltip_fields(gdf, tooltip)
    if fields:
        spec["tooltip"] = fields
        spec["tooltipLayer"] = _SOURCE
        if tooltip_trigger != "click":
            spec["tooltipTrigger"] = tooltip_trigger

    spec.update(opts)
    mount = chart_spec("maplibre", spec)
    if legend and mapping is not None and color is not None:
        return f'<div class="golit-map-wrap relative">{mount}{_legend_html(color, mapping)}</div>'
    return mount

raster

Render a georeferenced raster (rioxarray/xarray DataArray, GeoTIFF path, or NumPy array + bounds) as a native MapLibre image layer.

raster

raster(
    data: Any,
    *,
    cmap: str = "viridis",
    opacity: float = 0.85,
    band: int | None = None,
    vmin: float | None = None,
    vmax: float | None = None,
    bounds: Any = None,
    label: str = "value",
    basemap: Any = "default",
    fit: bool = True,
    fit_padding: int = 24,
    legend: bool = True,
    max_size: int = 1024,
    height: str = "420px",
    **opts: Any,
) -> str

Render a georeferenced raster as a native MapLibre image layer (GIS phase 2).

data is a rioxarray/xarray DataArray (reprojected to EPSG:4326 via its .rio CRS), a GeoTIFF path, or a NumPy 2-D array with explicit bounds=[w, s, e, n]. A single band is colormapped (cmap: "viridis", "magma", "blues", "terrain", "greys"; vmin/vmax set the range, NaN nodata is transparent), encoded to a PNG, and placed as an image source on the basemap with opacity. A colorbar legend (titled label) is overlaid. Large rasters are downsampled to max_size px on the long edge to keep the fragment small::

@app.view
def map(elevation, cmap=select(["viridis", "terrain"], default="terrain")):
    return gis.raster(elevation, cmap=cmap, label="Elevation (m)")

A view may also just return a georeferenced DataArray. For a multiband raster (satellite imagery), see :func:rgb. Requires pip install "golit[gis-raster]".

Source code in python/golit/gis.py
def raster(
    data: Any,
    *,
    cmap: str = "viridis",
    opacity: float = 0.85,
    band: int | None = None,
    vmin: float | None = None,
    vmax: float | None = None,
    bounds: Any = None,
    label: str = "value",
    basemap: Any = "default",
    fit: bool = True,
    fit_padding: int = 24,
    legend: bool = True,
    max_size: int = 1024,
    height: str = "420px",
    **opts: Any,
) -> str:
    """Render a georeferenced raster as a native MapLibre image layer (GIS phase 2).

    ``data`` is a rioxarray/xarray ``DataArray`` (reprojected to EPSG:4326 via its ``.rio``
    CRS), a **GeoTIFF path**, or a NumPy 2-D array with explicit ``bounds=[w, s, e, n]``. A
    single band is colormapped (``cmap``: ``"viridis"``, ``"magma"``, ``"blues"``,
    ``"terrain"``, ``"greys"``; ``vmin``/``vmax`` set the range, NaN nodata is transparent),
    encoded to a PNG, and placed as an image source on the ``basemap`` with ``opacity``. A
    colorbar ``legend`` (titled ``label``) is overlaid. Large rasters are downsampled to
    ``max_size`` px on the long edge to keep the fragment small::

        @app.view
        def map(elevation, cmap=select(["viridis", "terrain"], default="terrain")):
            return gis.raster(elevation, cmap=cmap, label="Elevation (m)")

    A view may also just ``return`` a georeferenced ``DataArray``. For a multiband raster
    (satellite imagery), see :func:`rgb`. Requires ``pip install "golit[gis-raster]"``."""
    arr, bnds = _load_raster(data, bounds, max_size)
    if arr.ndim == 3:
        arr = arr[0 if band is None else band]  # one band -> colormap; rgb() takes three
    if arr.ndim != 2:
        raise ValueError(f"raster: expected a 2-D band, got shape {arr.shape}")
    rgba, (lo, hi) = _colormap_to_rgba(arr, cmap, vmin, vmax)
    mount = _image_layer_spec(
        _png_data_uri(rgba), bnds, opacity=opacity, basemap=basemap,
        fit=fit, fit_padding=fit_padding, height=height, opts=opts,
    )
    if legend:
        mapping = {"kind": "numeric", "vmin": lo, "vmax": hi, "colors": _RASTER_CMAPS[cmap]}
        return f'<div class="golit-map-wrap relative">{mount}{_legend_html(label, mapping)}</div>'
    return mount

rgb

Render a multiband raster (satellite imagery) as a true/false-color RGB composite — three selected bands, each contrast-stretched independently, as a MapLibre image layer.

rgb

rgb(
    data: Any,
    *,
    bands: Any = (0, 1, 2),
    percentiles: Any = (2.0, 98.0),
    vmin: Any = None,
    vmax: Any = None,
    gamma: float | None = None,
    opacity: float = 1.0,
    bounds: Any = None,
    basemap: Any = "default",
    fit: bool = True,
    fit_padding: int = 24,
    max_size: int = 1024,
    height: str = "420px",
    **opts: Any,
) -> str

Render a multiband raster as a true/false-color RGB composite (GIS phase 2.5).

data is a rioxarray/xarray DataArray (reprojected to EPSG:4326 via its .rio CRS), a multiband GeoTIFF path, or a NumPy array with explicit bounds=[w, s, e, n] — band-first (band, y, x) (the rasterio/rioxarray layout) or channel-last (y, x, band) (auto-detected). bands selects the three bands mapped to red, green, blue: the default (0, 1, 2) is a natural-color composite for an R-G-B raster; pick others for false color (e.g. NIR-R-G to highlight vegetation).

Each band is contrast-stretched independently — by default to its 2nd–98th percentiles (robust to outliers), or to an explicit vmin/vmax (a scalar for all three, or a 3-sequence per band). gamma brightens (>1) or darkens (<1) the midtones; pixels that are nodata (non-finite) in any band are transparent. The composite is encoded to a PNG and placed as an image layer on the basemap with opacity. Large rasters are downsampled to max_size px on the long edge::

@app.view
def scene(stack, combo=select(["natural", "false-color"], default="natural")):
    bands = (0, 1, 2) if combo == "natural" else (3, 0, 1)  # NIR-R-G
    return gis.rgb(stack, bands=bands)

Requires pip install "golit[gis-raster]".

Source code in python/golit/gis.py
def rgb(
    data: Any,
    *,
    bands: Any = (0, 1, 2),
    percentiles: Any = (2.0, 98.0),
    vmin: Any = None,
    vmax: Any = None,
    gamma: float | None = None,
    opacity: float = 1.0,
    bounds: Any = None,
    basemap: Any = "default",
    fit: bool = True,
    fit_padding: int = 24,
    max_size: int = 1024,
    height: str = "420px",
    **opts: Any,
) -> str:
    """Render a multiband raster as a true/false-color **RGB composite** (GIS phase 2.5).

    ``data`` is a rioxarray/xarray ``DataArray`` (reprojected to EPSG:4326 via its ``.rio``
    CRS), a multiband **GeoTIFF path**, or a NumPy array with explicit ``bounds=[w, s, e, n]``
    — band-first ``(band, y, x)`` (the rasterio/rioxarray layout) or channel-last
    ``(y, x, band)`` (auto-detected). ``bands`` selects the three bands mapped to red,
    green, blue: the default ``(0, 1, 2)`` is a natural-color composite for an R-G-B raster;
    pick others for false color (e.g. NIR-R-G to highlight vegetation).

    Each band is contrast-stretched independently — by default to its 2nd–98th
    ``percentiles`` (robust to outliers), or to an explicit ``vmin``/``vmax`` (a scalar for
    all three, or a 3-sequence per band). ``gamma`` brightens (>1) or darkens (<1) the
    midtones; pixels that are nodata (non-finite) in any band are transparent. The composite
    is encoded to a PNG and placed as an image layer on the ``basemap`` with ``opacity``.
    Large rasters are downsampled to ``max_size`` px on the long edge::

        @app.view
        def scene(stack, combo=select(["natural", "false-color"], default="natural")):
            bands = (0, 1, 2) if combo == "natural" else (3, 0, 1)  # NIR-R-G
            return gis.rgb(stack, bands=bands)

    Requires ``pip install "golit[gis-raster]"``."""
    arr, bnds = _load_raster(data, bounds, max_size)
    composite = _select_bands(arr, bands)
    rgba, _ranges = _composite_to_rgba(composite, percentiles, vmin, vmax, gamma)
    return _image_layer_spec(
        _png_data_uri(rgba), bnds, opacity=opacity, basemap=basemap,
        fit=fit, fit_padding=fit_padding, height=height, opts=opts,
    )

tiles

Stream a very large Cloud-Optimized GeoTIFF as on-demand z/x/y map tiles via rio-tiler — only the visible window crosses the wire. Needs pip install "golit[gis-tiles]".

tiles

tiles(
    source: Any,
    *,
    bands: Any = None,
    cmap: str = "viridis",
    rescale: Any = None,
    opacity: float = 1.0,
    basemap: Any = "default",
    fit: bool = True,
    fit_padding: int = 24,
    min_zoom: int | None = None,
    max_zoom: int | None = None,
    tile_size: int = 256,
    height: str = "420px",
    **opts: Any,
) -> str

Stream a very large raster as on-demand map tiles (GIS phase 2.5).

Where :func:raster/:func:rgb ship the whole array as one PNG, tiles serves a Cloud-Optimized GeoTIFF (a local path or a remote http(s) URL) through a server-side tile route: rio-tiler reads only the z/x/y window each MapLibre request needs, colormaps/rescales it, and returns a 256-px PNG. A multi-gigabyte COG renders without ever loading or transmitting the full raster.

bands selects the source band(s): omit (or one index) for a single band, colormapped with cmap (any rio-tiler colormap — "viridis", "magma", "terrain", …); three indexes for an RGB composite (no colormap). Each band is contrast-stretched to rescale — a (min, max) per band, or auto from the COG's 2nd–98th percentile statistics when omitted. The data's geographic footprint frames the camera (fit)::

@app.view
def scene(layer=select(["elevation", "landcover"], default="elevation")):
    return gis.tiles(f"/data/{layer}.tif", cmap="terrain")   # a COG path or URL

Reading the source's metadata needs pip install "golit[gis-tiles]" (rio-tiler). The tile route is part of every Golit server; tiles for a session are served by the worker that rendered the view (the usual session affinity).

Source code in python/golit/gis.py
def tiles(
    source: Any,
    *,
    bands: Any = None,
    cmap: str = "viridis",
    rescale: Any = None,
    opacity: float = 1.0,
    basemap: Any = "default",
    fit: bool = True,
    fit_padding: int = 24,
    min_zoom: int | None = None,
    max_zoom: int | None = None,
    tile_size: int = 256,
    height: str = "420px",
    **opts: Any,
) -> str:
    """Stream a **very large** raster as on-demand map tiles (GIS phase 2.5).

    Where :func:`raster`/:func:`rgb` ship the whole array as one PNG, ``tiles`` serves a
    **Cloud-Optimized GeoTIFF** (a local path or a remote ``http(s)`` URL) through a
    server-side tile route: rio-tiler reads only the ``z/x/y`` window each MapLibre request
    needs, colormaps/rescales it, and returns a 256-px PNG. A multi-gigabyte COG renders
    without ever loading or transmitting the full raster.

    ``bands`` selects the source band(s): omit (or one index) for a single band, colormapped
    with ``cmap`` (any rio-tiler colormap — ``"viridis"``, ``"magma"``, ``"terrain"``, …);
    three indexes for an RGB composite (no colormap). Each band is contrast-stretched to
    ``rescale`` — a ``(min, max)`` per band, or auto from the COG's 2nd–98th percentile
    statistics when omitted. The data's geographic footprint frames the camera (``fit``)::

        @app.view
        def scene(layer=select(["elevation", "landcover"], default="elevation")):
            return gis.tiles(f"/data/{layer}.tif", cmap="terrain")   # a COG path or URL

    Reading the source's metadata needs ``pip install "golit[gis-tiles]"`` (rio-tiler). The
    tile route is part of every Golit server; tiles for a session are served by the worker
    that rendered the view (the usual session affinity)."""
    from rasterio.crs import CRS
    from rio_tiler.colormap import cmap as _cmaps
    from rio_tiler.io import Reader

    from .server.tiles import register_source

    path = os.fspath(source) if isinstance(source, os.PathLike) else str(source)
    indexes = tuple(int(b) for b in bands) if bands is not None else (1,)
    is_rgb = len(indexes) >= 3
    cmap_name = None if is_rgb else cmap
    if cmap_name is not None and cmap_name not in _cmaps.list():
        raise ValueError(f"unknown cmap {cmap_name!r}; see rio_tiler.colormap.cmap.list()")

    with Reader(path) as src:
        west, south, east, north = src.get_geographic_bounds(CRS.from_epsg(4326))
        lo_zoom = src.minzoom if min_zoom is None else min_zoom
        hi_zoom = src.maxzoom if max_zoom is None else max_zoom
        if rescale is None:
            stats = src.statistics(indexes=indexes)
            rescale = [[float(b.percentile_2), float(b.percentile_98)] for b in stats.values()]
        elif rescale and not isinstance(rescale[0], (list, tuple)):
            rescale = [list(rescale)]  # a single (min, max) -> one band

    token = _tile_token(path, indexes, cmap_name, rescale)
    register_source(token, {"path": path, "indexes": indexes, "cmap": cmap_name,
                            "rescale": rescale})

    raster_source = {
        "type": "raster",
        "tiles": [f"/gis/tiles/{token}/{{z}}/{{x}}/{{y}}"],
        "tileSize": tile_size,
        "minzoom": lo_zoom,
        "maxzoom": hi_zoom,
        "bounds": [west, south, east, north],
    }
    # raster-opacity-transition animates opacity changes; on a reactive update the client
    # swaps these tiles on the *existing* map (setTiles, see updateMapInPlace) rather than
    # remounting, so the basemap below is never torn down and reloaded (no flash).
    layer = {"id": _RASTER, "type": "raster", "source": _RASTER,
             "paint": {"raster-opacity": opacity,
                       "raster-opacity-transition": {"duration": 300, "delay": 0}}}

    base = _base_style(basemap)
    basemap_key = _basemap_key(base)
    spec: dict[str, Any] = {"height": height}
    if isinstance(base, str):
        spec["style"] = base
        spec["overlay"] = {"sources": {_RASTER: raster_source}, "layers": [layer]}
    else:
        base["sources"][_RASTER] = raster_source
        base["layers"].append(layer)
        spec["style"] = base

    # Lets the client update this tiled overlay in place when the basemap is unchanged:
    # same basemapKey -> swap just the tile URLs + opacity, otherwise fall back to remount.
    spec["tileLayer"] = {"source": _RASTER, "tiles": raster_source["tiles"],
                         "opacity": opacity}
    spec["basemapKey"] = basemap_key

    if fit and "bounds" not in opts:
        spec["bounds"] = [[west, south], [east, north]]
        if fit_padding != 24:
            spec["fitPadding"] = fit_padding

    spec.update(opts)
    return chart_spec("maplibre", spec)

terrain

Run a WhiteboxTools terrain operation (hillshade, slope, aspect, fill, flow accumulation, …) on a DEM, returning a georeferenced DataArray that feeds raster/tiles. Needs pip install "golit[gis-terrain]".

terrain

terrain(
    dem: Any,
    op: str = "hillshade",
    *,
    crs: Any = None,
    **params: Any,
) -> Any

Run a WhiteboxTools terrain analysis on a DEM, returning the result as a georeferenced DataArray — feed it to :func:raster/:func:tiles, or just return it.

dem is a DEM as a GeoTIFF path or a georeferenced rioxarray/xarray DataArray (terrain analysis needs the cell size, so a bare NumPy array won't do; use a projected CRS in metres — e.g. a UTM zone — for meaningful slopes). op is one of the curated operations — "hillshade", "slope", "aspect", "fill" (fill depressions), "flow_accumulation" — or any other WhiteboxTools tool name; extra keyword args pass straight to the tool (e.g. azimuth=/altitude= for hillshade)::

@app.reactive
def shaded(dem, azimuth=slider(0, 360, default=315)):
    return gis.terrain(dem, "hillshade", azimuth=azimuth)   # -> a DataArray

@app.view
def relief(shaded):
    return gis.raster(shaded, cmap="greys", label="Hillshade")

The DEM is run through the WhiteboxTools binary (downloaded once on first use) in a temp workspace, and the output is read back into memory. Requires pip install "golit[gis-terrain]".

Source code in python/golit/gis.py
def terrain(dem: Any, op: str = "hillshade", *, crs: Any = None, **params: Any) -> Any:
    """Run a **WhiteboxTools** terrain analysis on a DEM, returning the result as a
    georeferenced ``DataArray`` — feed it to :func:`raster`/:func:`tiles`, or just return it.

    ``dem`` is a DEM as a **GeoTIFF path** or a georeferenced rioxarray/xarray ``DataArray``
    (terrain analysis needs the cell size, so a bare NumPy array won't do; use a **projected**
    CRS in metres — e.g. a UTM zone — for meaningful slopes). ``op`` is one of the curated
    operations — ``"hillshade"``, ``"slope"``, ``"aspect"``, ``"fill"`` (fill depressions),
    ``"flow_accumulation"`` — or any other WhiteboxTools tool name; extra keyword args pass
    straight to the tool (e.g. ``azimuth=``/``altitude=`` for hillshade)::

        @app.reactive
        def shaded(dem, azimuth=slider(0, 360, default=315)):
            return gis.terrain(dem, "hillshade", azimuth=azimuth)   # -> a DataArray

        @app.view
        def relief(shaded):
            return gis.raster(shaded, cmap="greys", label="Hillshade")

    The DEM is run through the WhiteboxTools binary (downloaded once on first use) in a temp
    workspace, and the output is read back into memory. Requires
    ``pip install "golit[gis-terrain]"``."""
    import shutil
    import tempfile

    tool, defaults = _TERRAIN_TOOLS.get(op, (op, {}))
    merged = {**defaults, **params}
    tmp = tempfile.mkdtemp(prefix="golit_wbt_")
    try:
        dem_path = _materialize_dem(dem, tmp, crs)  # validates CRS before touching the binary
        out_path = os.path.join(tmp, f"{op}.tif")
        method = getattr(_wbt(), tool, None)
        if method is None or not callable(method):
            raise ValueError(
                f"unknown WhiteboxTools tool {op!r}; curated: {sorted(_TERRAIN_TOOLS)}, "
                "or any tool name from the WhiteboxTools manual"
            )
        rc = method(dem_path, out_path, **merged)
        if rc != 0 or not os.path.exists(out_path):
            raise RuntimeError(f"WhiteboxTools {tool} failed (return code {rc})")

        import rioxarray

        # open_rasterio on a single GeoTIFF returns a DataArray (its type is a broad union).
        with rioxarray.open_rasterio(out_path, masked=True) as src:  # type: ignore[union-attr]
            result = src.squeeze(drop=True).load()  # read fully so the temp file can be removed
            result_crs = src.rio.crs
        return result.rio.write_crs(result_crs)
    finally:
        shutil.rmtree(tmp, ignore_errors=True)

ee_layer

Overlay a Google Earth Engine image as live XYZ tiles — EE renders the imagery, Golit points a MapLibre raster source at the tile URL from getMapId(vis). Needs pip install "golit[gis-ee]" and an Earth Engine account.

ee_layer

ee_layer(
    image: Any,
    *,
    vis: Any = None,
    opacity: float = 1.0,
    basemap: Any = "default",
    center: Any = None,
    zoom: float | None = None,
    attribution: str = "Map data © Google Earth Engine",
    height: str = "420px",
    **opts: Any,
) -> str

Overlay a Google Earth Engine image as live XYZ tiles (GIS phase 3).

image is anything with a getMapId(vis) method — an ee.Image (reduce a collection first with .median()/.mosaic()). Earth Engine renders the tiles on its own servers; this asks for a tile-URL template (image.getMapId(vis)) and points a MapLibre raster source at it, overlaid on basemap. vis is the usual Earth Engine visualization dict ({"min", "max", "bands"} or "palette"); center / zoom frame the camera (an EE image can be global, so there's no data extent to fit)::

import ee
ee.Initialize(project="my-cloud-project")        # after `earthengine authenticate`

@app.view
def scene(cloud=slider(5, 80, default=30)):
    s2 = (ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
          .filterBounds(aoi)
          .filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", cloud))
          .median())
    return gis.ee_layer(s2, vis={"bands": ["B4", "B3", "B2"], "min": 0, "max": 3000},
                        center=[-0.15, 5.62], zoom=10)

This function imports nothing itself — Earth Engine authentication and the ee objects are the caller's. Install the client with pip install "golit[gis-ee]" and authenticate once (earthengine authenticate + ee.Initialize(project=…)).

Source code in python/golit/gis.py
def ee_layer(
    image: Any,
    *,
    vis: Any = None,
    opacity: float = 1.0,
    basemap: Any = "default",
    center: Any = None,
    zoom: float | None = None,
    attribution: str = "Map data © Google Earth Engine",
    height: str = "420px",
    **opts: Any,
) -> str:
    """Overlay a **Google Earth Engine** image as live XYZ tiles (GIS phase 3).

    ``image`` is anything with a ``getMapId(vis)`` method — an ``ee.Image`` (reduce a
    collection first with ``.median()``/``.mosaic()``). Earth Engine renders the tiles on its
    own servers; this asks for a tile-URL template (``image.getMapId(vis)``) and points a
    MapLibre **raster source** at it, overlaid on ``basemap``. ``vis`` is the usual Earth
    Engine visualization dict (``{"min", "max", "bands"}`` or ``"palette"``); ``center`` /
    ``zoom`` frame the camera (an EE image can be global, so there's no data extent to fit)::

        import ee
        ee.Initialize(project="my-cloud-project")        # after `earthengine authenticate`

        @app.view
        def scene(cloud=slider(5, 80, default=30)):
            s2 = (ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
                  .filterBounds(aoi)
                  .filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", cloud))
                  .median())
            return gis.ee_layer(s2, vis={"bands": ["B4", "B3", "B2"], "min": 0, "max": 3000},
                                center=[-0.15, 5.62], zoom=10)

    This function imports nothing itself — Earth Engine authentication and the ``ee`` objects
    are the caller's. Install the client with ``pip install "golit[gis-ee]"`` and authenticate
    once (``earthengine authenticate`` + ``ee.Initialize(project=…)``)."""
    map_id = image.getMapId(vis or {})
    source = {
        "type": "raster",
        "tiles": [_ee_tile_url(map_id)],
        "tileSize": 256,
        "attribution": attribution,
    }
    layer = {"id": _RASTER, "type": "raster", "source": _RASTER,
             "paint": {"raster-opacity": opacity}}

    base = _base_style(basemap)
    spec: dict[str, Any] = {"height": height}
    if isinstance(base, str):
        spec["style"] = base
        spec["overlay"] = {"sources": {_RASTER: source}, "layers": [layer]}
    else:
        base["sources"][_RASTER] = source
        base["layers"].append(layer)
        spec["style"] = base

    if center is not None:
        spec["center"] = list(center)
    if zoom is not None:
        spec["zoom"] = zoom

    spec.update(opts)
    return chart_spec("maplibre", spec)

explore

The folium/leafmap escape hatch — embed gdf.explore() as a swappable fragment.

explore

explore(gdf: Any, **kwargs: Any) -> str

The folium/leafmap escape hatch: delegate to gdf.explore(**kwargs) and embed its HTML. Returns a self-contained (iframe) folium map wrapped in a sized container, so it swaps cleanly like any other fragment — the cost is folium's client runtime, versus :func:geo_map's native MapLibre. Use this when you want the folium ecosystem (layer control, marker clusters, plugins)::

@app.view
def map(regions):
    return gis.explore(regions, column="revenue", cmap="Blues")

Requires pip install "golit[gis]" (folium ships with the extra).

Source code in python/golit/gis.py
def explore(gdf: Any, **kwargs: Any) -> str:
    """The folium/leafmap escape hatch: delegate to ``gdf.explore(**kwargs)`` and embed
    its HTML. Returns a self-contained (iframe) folium map wrapped in a sized container,
    so it swaps cleanly like any other fragment — the cost is folium's client runtime,
    versus :func:`geo_map`'s native MapLibre. Use this when you want the folium ecosystem
    (layer control, marker clusters, plugins)::

        @app.view
        def map(regions):
            return gis.explore(regions, column="revenue", cmap="Blues")

    Requires ``pip install "golit[gis]"`` (folium ships with the extra)."""
    fmap = gdf.explore(**kwargs)
    inner = fmap._repr_html_()
    return (
        '<div class="golit-chart bg-surface-container-lowest rounded-xl overflow-hidden '
        f'shadow-sm">{inner}</div>'
    )

spatial_sql

DuckDB ST_* SQL over Polars/GeoJSON sources, returning Polars (bridge to geo_map with to_geo).

spatial_sql

spatial_sql(query: str, **frames: Any) -> DataFrame

Run a DuckDB spatial SQL query (ST_* functions) over named frames, returning Polars — the spatial extension is loaded on the connection first, then the query runs through the ordinary :func:golit.sql path, so the result memoizes and feeds :func:geo_map like any frame::

@app.reactive
def nearby(places, radius_km=slider(1, 50, default=10)):
    return gis.spatial_sql(
        "SELECT name, geom FROM p "
        f"WHERE ST_DWithin(geom, ST_Point(-0.19, 5.6), {radius_km} / 111.0)",
        p=places,
    )

Requires the sql extra (pip install "golit[sql]"); DuckDB downloads the spatial extension on first use. Select geometry as ST_AsWKB(geom) (or ST_AsText) and pass it through :func:to_geo to feed :func:geo_map.

Source code in python/golit/gis.py
def spatial_sql(query: str, **frames: Any) -> pl.DataFrame:
    """Run a DuckDB **spatial** SQL ``query`` (``ST_*`` functions) over named frames,
    returning Polars — the spatial extension is loaded on the connection first, then the
    query runs through the ordinary :func:`golit.sql` path, so the result memoizes and
    feeds :func:`geo_map` like any frame::

        @app.reactive
        def nearby(places, radius_km=slider(1, 50, default=10)):
            return gis.spatial_sql(
                "SELECT name, geom FROM p "
                f"WHERE ST_DWithin(geom, ST_Point(-0.19, 5.6), {radius_km} / 111.0)",
                p=places,
            )

    Requires the ``sql`` extra (``pip install "golit[sql]"``); DuckDB downloads the
    spatial extension on first use. Select geometry as ``ST_AsWKB(geom)`` (or
    ``ST_AsText``) and pass it through :func:`to_geo` to feed :func:`geo_map`."""
    import warnings

    from .data import load_extension, sql

    load_extension("spatial")
    with warnings.catch_warnings():
        # A DuckDB GEOMETRY column comes back as a geoarrow.wkb extension type Polars
        # doesn't register; it loads fine as its Binary storage, so quiet the notice.
        warnings.filterwarnings("ignore", message=".*geoarrow.wkb.*")
        return sql(query, **frames)

to_geo

Turn a frame with a WKB/WKT/shapely geometry column into a GeoDataFrame — the bridge from spatial_sql to geo_map.

to_geo

to_geo(
    data: Any,
    *,
    geometry: str = "geometry",
    crs: Any = 4326,
) -> Any

Turn a frame with a geometry column into a GeoPandas GeoDataFrame.

The bridge from :func:spatial_sql (which returns Polars) to :func:geo_map. The geometry column may be WKB bytes (a DuckDB GEOMETRY / ST_AsWKB column — the usual spatial_sql output), WKT text (ST_AsText), or shapely objects. Accepts a Polars or pandas frame; crs defaults to EPSG:4326 (lon/lat)::

frame = gis.spatial_sql("SELECT name, ST_AsWKB(geom) AS geometry FROM p …", p=places)
gdf = gis.to_geo(frame, geometry="geometry")

Requires pip install "golit[gis]".

Source code in python/golit/gis.py
def to_geo(data: Any, *, geometry: str = "geometry", crs: Any = 4326) -> Any:
    """Turn a frame with a geometry column into a GeoPandas ``GeoDataFrame``.

    The bridge from :func:`spatial_sql` (which returns Polars) to :func:`geo_map`. The
    ``geometry`` column may be **WKB** bytes (a DuckDB ``GEOMETRY`` / ``ST_AsWKB`` column —
    the usual ``spatial_sql`` output), **WKT** text (``ST_AsText``), or shapely objects.
    Accepts a Polars or pandas frame; ``crs`` defaults to EPSG:4326 (lon/lat)::

        frame = gis.spatial_sql("SELECT name, ST_AsWKB(geom) AS geometry FROM p …", p=places)
        gdf = gis.to_geo(frame, geometry="geometry")

    Requires ``pip install "golit[gis]"``."""
    import geopandas as gpd
    import shapely

    pdf = data.to_pandas() if isinstance(data, pl.DataFrame) else data.copy()
    column = pdf[geometry]
    sample = next((v for v in column if v is not None), None)
    if isinstance(sample, (bytes, bytearray)):
        geom = shapely.from_wkb(list(column))
    elif isinstance(sample, str):
        geom = shapely.from_wkt(list(column))
    else:
        geom = list(column)  # already shapely geometries (or all-null)
    return gpd.GeoDataFrame(pdf.drop(columns=[geometry]), geometry=list(geom), crs=crs)

is_geodataframe

is_geodataframe

is_geodataframe(value: Any) -> bool

Whether value is a GeoPandas GeoDataFrame — by class, without importing geopandas (mirrors the matplotlib/Lets-Plot detectors). Used by :func:golit.rendering.render_value and :func:golit.hashing.hash_value so a view can return a GeoDataFrame directly.

Source code in python/golit/gis.py
def is_geodataframe(value: Any) -> bool:
    """Whether ``value`` is a GeoPandas ``GeoDataFrame`` — by class, without importing
    geopandas (mirrors the matplotlib/Lets-Plot detectors). Used by
    :func:`golit.rendering.render_value` and :func:`golit.hashing.hash_value` so a view
    can return a GeoDataFrame directly."""
    cls = type(value)
    return cls.__name__ == "GeoDataFrame" and (cls.__module__ or "").startswith("geopandas")

is_dataarray

is_dataarray

is_dataarray(value: Any) -> bool

Whether value is an xarray DataArray — by class, without importing xarray. Used by :func:golit.rendering.render_value so a view can return a (georeferenced) raster directly.

Source code in python/golit/gis.py
def is_dataarray(value: Any) -> bool:
    """Whether ``value`` is an xarray ``DataArray`` — by class, without importing xarray.
    Used by :func:`golit.rendering.render_value` so a view can return a (georeferenced)
    raster directly."""
    cls = type(value)
    return cls.__name__ == "DataArray" and (cls.__module__ or "").startswith("xarray")