Quickstart#

Elevate your Folium maps with professional-grade interactive plugins. foliplus provides advanced Layer Management, Dynamic H3 Heatmaps, Spatial Measurement tools, and unified search capabilities in a single, easy-to-use toolkit.

What you will learn#

  • Building a multi-theme base map.

  • Preparing spatially-valid datasets using GeoPandas.

  • Integrating the full suite of foliplus plugins.

  • Exporting interactive maps for production use.

1. Installation#

Install the core package via pip:

$ pip install -U foliplus

For the spatial analysis used in this guide, we recommend:

$ pip install -U my-data-toolkit geopandas

Note: Please restart your notebook kernel after installing new packages.

2. Data Preparation & Base Map#

To fully demonstrate the power of foliplus, we’ll prepare a multi-layered urban dataset:

  1. Administrative Context: Loading district boundaries (Polygons) to provide spatial grounding.

  2. Point Density: Generating a synthetic grid of facility points with numeric score weights.

  3. Symbolic Landmarks: Identifying district centers with custom icons to showcase how LayerControl automatically identifies geometry types.

  4. Commuting Routes: Creating a simulated line network using to_line from my-data-toolkit to demonstrate Line geometry support.

  5. Base Themes: Setting up both Light and Dark modes for different visualization needs.

import dtoolkit.geoaccessor  # noqa: F401
import folium
import geopandas as gpd
import numpy as np
import pandas as pd

# ---------------------------------------------------------------------------
# 1. Load & Refine Administrative Boundaries
# ---------------------------------------------------------------------------
# Load the Shanghai district boundaries
boundary = gpd.read_file("data/gadm41_CHN_3_Shanghai.geojson")

# Calculate District Centers (Layer 1)
# Re-project to EPSG:3857 for accurate geometry centers, then back to EPSG:4326 for Folium
districts = boundary.copy()
districts["geometry"] = boundary.to_crs(3857).centroid.to_crs(4326)

# Inject simulated demographic data for visualization
np.random.seed(42)
districts["population_density"] = np.random.randint(5_000, 30_000, len(districts))

# ---------------------------------------------------------------------------
# 2. Vectorized Point Sampling (Layer 2)
# ---------------------------------------------------------------------------
bounds = boundary.total_bounds
facilities = (
    # Generate a dense candidate pool across the city
    pd.DataFrame(
        {
            "x": np.random.uniform(bounds[0], bounds[2], 5_000),
            "y": np.random.uniform(bounds[1], bounds[3], 5_000),
            "score": np.random.randint(1, 5_000, 5_000),
        }
    )
    .from_xy("x", "y", crs=4326)
    # Perform spatial join to keep only points inside the land area
    .sjoin(boundary, how="inner")
    .groupby("NAME_3")
    .sample(n=500, random_state=42, replace=True)
    .reset_index(drop=True)
)

# ---------------------------------------------------------------------------
# 3. Initialize Base Map
# ---------------------------------------------------------------------------
center = districts.geocentroid()
m = folium.Map(location=[center.y, center.x], zoom_start=10, tiles=None)

# Add professional base themes
# Canvas-style base maps from Esri — plain gray canvases with no labels,
# which keeps the data layers (markers, polygons, routes) visually dominant.
# The bare URL is used because `World_Dark_Gray_Base` has no xyzservices entry.
ESRI_ATTR = "Tiles © Esri — Esri, DeLorme, NAVTEQ"
folium.TileLayer(
    "https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}",
    name="Light Canvas",
    attr=ESRI_ATTR,
    max_zoom=16,
).add_to(m)
folium.TileLayer(
    "https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}",
    name="Dark Canvas",
    attr=ESRI_ATTR,
    max_zoom=16,
    show=False,
).add_to(m)
# OpenStreetMap as the labeled fallback, listed last
folium.TileLayer("OpenStreetMap", name="OpenStreetMap", show=False).add_to(m)

# ---------------------------------------------------------------------------
# 4. Layer Orchestration
# ---------------------------------------------------------------------------
# Step 1: Add District Landmarks (Top markers)
districts.explore(
    m=m,
    name="District Landmarks",
    marker_type="marker",
    marker_kwds={"icon": folium.Icon(color="cadetblue", icon="map-pin", prefix="fa")},
    tooltip="NAME_3",
    popup=["NAME_3", "population_density"],
    legend=False,
)

# Step 2: Add Facility Points (Heatmap source)
facilities.explore(
    m=m,
    name="Facility Points",
    column="score",
    cmap="plasma",
    marker_type="circle_marker",
    style_kwds={"radius": 3, "fillOpacity": 0.8, "stroke": False},
    legend=True,
    show=False,
)

# Step 3: Add Administrative Polygons as the base overlay
boundary.explore(
    m=m,
    name="Municipal Boundaries",
    color="gray",
    style_kwds={"fillOpacity": 0.05, "weight": 1.5, "dashArray": "5, 5"},
    tooltip="NAME_3",
    popup=True,
    legend=False,
)

# ---------------------------------------------------------------------------
# Step 5: Add Commuting Routes (Line layer via to_line)
# ---------------------------------------------------------------------------
# Create a simulated route network connecting district centers
# to_line creates LineString geometries from (x1,y1) → (x2,y2) columns
(
    pd.DataFrame(
        {
            "x1": districts.geometry.x[::2].values,
            "y1": districts.geometry.y[::2].values,
            "x2": districts.geometry.x[1::2].values,
            "y2": districts.geometry.y[1::2].values,
            "route_name": [f"Route {i}" for i in range(len(districts.geometry.x[::2]))],
        }
    )
    .to_line("x1", "y1", "x2", "y2")
    .explore(
        m=m,
        name="Commuting Routes",
        color="#e74c3c",
        style_kwds={"weight": 6, "opacity": 0.9, "dashArray": "10 10"},
        highlight_kwds={"weight": 8},
        tooltip="route_name",
        legend=False,
        show=False,
    )
)

print("🌍 Map is ready")
🌍 Map is ready

3. Enable foliplus Plugins#

Now, we activate the professional toolkit. Observe how the plugins interact with our data:

  • ExportControl: Enables high-resolution map exports with customizable DPI scaling and size limits. Perfect for presentations or publications.

  • HeatmapControl: Aggregates facility points into H3 hexagons. We’ve configured it to use the sum of the score field for a weighted density analysis.

  • LayerControl: Allows real-time reordering of layers and quick adjustments to transparency or base themes.

  • MeasureControl: Measures distances, areas, circles, and geocoded markers, then edit them by dragging nodes.

  • SearchControl: Provides a unified interface for coordinate jumping and keyword search (e.g., search for “Shanghai”).

from foliplus import (
    ExportControl,
    FullscreenControl,
    HeatmapControl,
    LayerControl,
    LocateControl,
    MeasureControl,
    ScaleControl,
    SearchControl,
)

# Add all plugins with default settings to keep it simple but powerful
SearchControl().add_to(m)
LayerControl().add_to(m)
HeatmapControl(agg="sum").add_to(m)
ScaleControl().add_to(m)
MeasureControl().add_to(m)
ExportControl().add_to(m)
FullscreenControl(hide_self=False, hide_others=False).add_to(m)
LocateControl().add_to(m)

print("📦 foliplus's plugins are active")
📦 foliplus's plugins are active

4. Display Map#

Run the next cell to render the interactive map in Notebook.

If you want to share the result, use the optional HTML export section below.

m
Make this Notebook Trusted to load map: File -> Trust Notebook

If you want to view the map in a browser or host it as a static file, export it to HTML.

file = "map.html"
m.save(file)
print(f"📃 HTML is saved: {file}")
📃 HTML is saved: map.html

5. Troubleshooting & Tips#

  • Map Blank or Not Rendering: Ensure the notebook is trusted and that the map display cell (Cell 7) was the last one executed.

  • Dependency Issues: If dtoolkit or geopandas are missing, install them via the suggested command in Section 1 and restart the kernel.

  • Custom Localization: While plugins auto-detect your browser language, you can force a locale globally: LayerControl(locale="en").

6. Building from Source#

foliplus bundles its controls as compiled JS/CSS in foliplus/dist/, which is not committed to the repo. Always build with make dist — it compiles the bundles and packages them, and it asserts every expected artifact is present before uv build runs.

Do not run a bare python -m build (or uv build alone) in a development checkout. Both succeed silently when foliplus/dist/ is absent, producing a wheel that installs and imports fine but renders maps with no controls at all. If an artifact is genuinely missing, _build_shared_header() raises MissingAssetsError at render time rather than emitting an empty bundle.

$ make env        # bootstrap .venv + install deps
$ make dist       # build-js + build-python (the only correct release path)
$ pip install dist/*.whl

7. Next Steps#

  • API Exploration: Refer to the API Reference for a deep dive into available properties.

  • Real-World Integration: Swap the simulated data for your own local datasets to start generating professional spatial insights immediately.

Happy Mapping! You are now ready to build scalable, interactive maps with foliplus.