znlgis 博客

GIS开发与技术分享

第22章:交互式地图(Folium)

在前面的章节中,我们学习了如何使用 Matplotlib 创建静态地图。然而,在实际的 GIS 应用中,交互式地图往往能提供更好的用户体验——用户可以缩放、平移、点击要素查看属性信息。本章将介绍如何使用 GeoPandas 的 explore() 方法和 Folium 库创建功能丰富的交互式 Web 地图。


22.1 交互式地图概述

22.1.1 静态地图 vs 交互式地图

静态地图和交互式地图各有其适用场景,了解它们的区别有助于我们做出正确的选择。

特性 静态地图 交互式地图
缩放平移 不支持 支持
属性查询 不支持 点击/悬停查看
底图切换 不支持 支持多种底图
输出格式 PNG/SVG/PDF HTML
适用场景 论文、报告、印刷 Web 应用、数据探索
渲染方式 服务端渲染 客户端(浏览器)渲染
数据量限制 较大 受浏览器性能限制
依赖库 Matplotlib Folium / Leaflet.js
# 静态地图示例
import geopandas as gpd
import matplotlib.pyplot as plt

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
world.plot(figsize=(12, 6))
plt.title("静态世界地图")
plt.show()
# 交互式地图示例
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
m = world.explore()  # 返回一个交互式地图对象
m

22.1.2 Folium 简介与安装

Folium 是一个基于 Leaflet.js 的 Python 库,能够将数据可视化为交互式 Web 地图。GeoPandas 从 0.10 版本开始内置了对 Folium 的支持,通过 explore() 方法即可快速创建交互式地图。

# 安装 Folium
# pip install folium

# 安装 mapclassify(用于分类着色)
# pip install mapclassify
# 验证安装
import folium
import geopandas as gpd

print(f"Folium 版本: {folium.__version__}")
print(f"GeoPandas 版本: {gpd.__version__}")

输出:

Folium 版本: 0.14.0
GeoPandas 版本: 0.14.1

注意: explore() 方法需要额外安装 foliummapclassify 两个包。如果只使用基本的交互式地图功能,只需安装 folium 即可;如果需要按属性分类着色,则还需要 mapclassify


22.2 explore() 基础用法

22.2.1 GeoDataFrame.explore() 方法

explore() 是 GeoPandas 为 GeoDataFrame 提供的快捷方法,可以一行代码生成交互式地图。

import geopandas as gpd

# 读取示例数据
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))

# 最简单的用法——一行代码创建交互式地图
m = world.explore()
m  # 在 Jupyter Notebook 中直接显示
# explore() 返回的是 folium.Map 对象
print(type(m))

输出:

<class 'folium.folium.Map'>
# 也可以对筛选后的数据使用 explore()
asia = world[world['continent'] == 'Asia']
asia.explore()

22.2.2 基本参数设置

explore() 方法支持多种参数来定制地图的外观和行为。

# 常用参数一览
m = world.explore(
    column='pop_est',         # 按人口着色
    cmap='YlOrRd',            # 颜色映射
    legend=True,              # 显示图例
    legend_kwds={
        'caption': '人口数量'  # 图例标题
    },
    style_kwds={
        'fillOpacity': 0.7,   # 填充透明度
        'weight': 1,          # 边框宽度
        'color': 'gray'       # 边框颜色
    },
    tooltip=['name', 'continent', 'pop_est'],  # 悬停提示字段
    popup=True,               # 点击弹出所有属性
    tiles='CartoDB positron', # 底图样式
    zoom_start=2,             # 初始缩放级别
    width='100%',             # 地图宽度
    height='500px'            # 地图高度
)
m
参数 类型 说明 默认值
column str 用于着色的属性列名 None
cmap str Matplotlib 颜色映射名称 None
color str 统一填充颜色 None
legend bool 是否显示图例 True
tooltip bool/list/str 悬停时显示的字段 True
popup bool/list/str 点击时弹出的字段 False
tiles str 底图瓦片样式 ‘OpenStreetMap’
style_kwds dict 样式参数字典 None
zoom_start int 初始缩放级别 10
m folium.Map 已有地图对象(用于叠加) None

22.3 底图选择(tiles)

22.3.1 内置底图选项

Folium 提供了多种内置底图样式,可以通过 tiles 参数指定。

底图名称 说明 适用场景
OpenStreetMap 默认底图,信息丰富 通用场景
CartoDB positron 浅色简洁底图 数据可视化、专题地图
CartoDB dark_matter 深色底图 夜景效果、亮色数据展示
Stamen Terrain 地形底图 地形分析、自然地理
Stamen Toner 黑白风格 简约设计、印刷风格
Stamen Watercolor 水彩风格 艺术效果展示
import geopandas as gpd

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
asia = world[world['continent'] == 'Asia']

# 使用 CartoDB 浅色底图
m1 = asia.explore(
    tiles='CartoDB positron',
    color='steelblue',
    tooltip='name'
)

# 使用 CartoDB 暗色底图
m2 = asia.explore(
    tiles='CartoDB dark_matter',
    color='#ff6b6b',
    tooltip='name'
)
# 使用 Stamen 地形底图
m3 = asia.explore(
    tiles='Stamen Terrain',
    style_kwds={'fillOpacity': 0.3},
    tooltip='name'
)

22.3.2 自定义瓦片服务

除了内置底图,还可以使用自定义瓦片服务 URL。

import folium
import geopandas as gpd

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
china = world[world['name'] == 'China']

# 使用自定义瓦片 URL
# 天地图示例(需要申请 token)
tianditu_url = (
    'http://t0.tianditu.gov.cn/vec_w/wmts?'
    'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0'
    '&LAYER=vec&STYLE=default&TILEMATRIXSET=w'
    '&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}'
    '&tk=YOUR_TOKEN'
)

m = china.explore(
    tiles=tianditu_url,
    attr='天地图',  # 底图版权信息
    tooltip='name'
)
m
# 也可以在 folium.Map 中直接设置自定义瓦片
m = folium.Map(
    location=[35, 105],
    zoom_start=4,
    tiles=None  # 不使用默认底图
)

# 添加多个底图图层
folium.TileLayer('OpenStreetMap', name='OSM 标准').add_to(m)
folium.TileLayer('CartoDB positron', name='CartoDB 浅色').add_to(m)
folium.TileLayer('CartoDB dark_matter', name='CartoDB 暗色').add_to(m)

# 添加图层控制器以切换底图
folium.LayerControl().add_to(m)
m

注意: 使用第三方瓦片服务时,请遵守其使用条款和访问限制。部分服务(如天地图)需要注册获取 API Token。


22.4 按属性着色

22.4.1 column 和 cmap 参数

通过 column 参数指定着色字段,cmap 参数指定颜色映射方案。

import geopandas as gpd

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))

# 按 GDP 估值着色
m = world.explore(
    column='gdp_md_est',          # 使用 GDP 字段着色
    cmap='Greens',                # 绿色系颜色映射
    legend=True,
    legend_kwds={
        'caption': 'GDP(百万美元)'  # 图例标题
    },
    tiles='CartoDB positron',
    tooltip=['name', 'gdp_md_est'],
    style_kwds={
        'fillOpacity': 0.8,
        'weight': 0.5
    }
)
m
# 常用颜色映射方案
cmap_options = {
    '连续型': ['viridis', 'plasma', 'inferno', 'magma', 'cividis'],
    '顺序型': ['Blues', 'Greens', 'Reds', 'YlOrRd', 'YlGnBu'],
    '发散型': ['RdYlGn', 'RdBu', 'coolwarm', 'BrBG', 'PiYG'],
    '定性型': ['Set1', 'Set2', 'Set3', 'Paired', 'tab10']
}

for category, cmaps in cmap_options.items():
    print(f"{category}: {', '.join(cmaps)}")

输出:

连续型: viridis, plasma, inferno, magma, cividis
顺序型: Blues, Greens, Reds, YlOrRd, YlGnBu
发散型: RdYlGn, RdBu, coolwarm, BrBG, PiYG
定性型: Set1, Set2, Set3, Paired, tab10

22.4.2 分类着色与连续着色

GeoPandas 的 explore() 方法支持分类着色和连续着色两种模式。

import geopandas as gpd

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))

# 分类着色——按大洲着色(分类变量)
m_categorical = world.explore(
    column='continent',
    cmap='Set2',              # 定性颜色映射
    legend=True,
    tooltip=['name', 'continent'],
    tiles='CartoDB positron'
)
# 连续着色——按人口着色(连续变量)
m_continuous = world.explore(
    column='pop_est',
    cmap='YlOrRd',
    scheme='NaturalBreaks',   # 自然断点分类法
    k=5,                      # 分为 5 类
    legend=True,
    legend_kwds={
        'caption': '人口数量',
        'fmt': '{:.0f}'       # 数字格式
    },
    tiles='CartoDB positron'
)
# 支持的分类方案(需要 mapclassify)
classification_schemes = [
    'BoxPlot',          # 箱线图分类
    'EqualInterval',    # 等间隔分类
    'FisherJenks',      # Fisher-Jenks 分类
    'HeadTailBreaks',   # 头尾断裂分类
    'JenksCaspall',     # Jenks-Caspall 分类
    'MaximumBreaks',    # 最大断裂分类
    'NaturalBreaks',    # 自然断点分类
    'Quantiles',        # 分位数分类
    'StdMean',          # 标准差分类
]

for scheme in classification_schemes:
    print(f"  - {scheme}")

输出:

  - BoxPlot
  - EqualInterval
  - FisherJenks
  - HeadTailBreaks
  - JenksCaspall
  - MaximumBreaks
  - NaturalBreaks
  - Quantiles
  - StdMean

注意: 使用 scheme 参数进行分类着色时,需要安装 mapclassify 库。连续着色适合数值型数据,分类着色适合类别型数据。


22.5 弹出框与提示

22.5.1 tooltip 参数 (hover)

tooltip 控制鼠标悬停时显示的信息。

import geopandas as gpd

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))

# tooltip=True 时显示所有属性
m1 = world.explore(
    tooltip=True,
    tiles='CartoDB positron'
)

# tooltip 指定显示特定字段
m2 = world.explore(
    tooltip=['name', 'continent', 'pop_est'],
    tiles='CartoDB positron'
)

# tooltip=False 禁用悬停提示
m3 = world.explore(
    tooltip=False,
    tiles='CartoDB positron'
)
# 使用 GeoJsonTooltip 自定义提示样式
import folium

m = world.explore(
    tooltip=folium.GeoJsonTooltip(
        fields=['name', 'continent', 'pop_est'],
        aliases=['国家名称:', '所在大洲:', '人口数量:'],  # 字段别名
        localize=True,             # 本地化数字格式
        sticky=True,               # 跟随鼠标
        style="""
            background-color: #F0EFEF;
            border: 2px solid #333;
            border-radius: 5px;
            padding: 8px;
            font-size: 13px;
        """
    ),
    tiles='CartoDB positron'
)
m

22.5.2 popup 参数 (click)

popup 控制鼠标点击时弹出的信息窗口。

import geopandas as gpd

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))

# popup=True 显示所有属性
m1 = world.explore(
    popup=True,
    tooltip='name',
    tiles='CartoDB positron'
)

# popup 指定显示特定字段
m2 = world.explore(
    popup=['name', 'pop_est', 'gdp_md_est'],
    tooltip='name',
    tiles='CartoDB positron'
)
# 使用 GeoJsonPopup 自定义弹出内容
import folium

m = world.explore(
    popup=folium.GeoJsonPopup(
        fields=['name', 'continent', 'pop_est', 'gdp_md_est'],
        aliases=['国家:', '大洲:', '人口:', 'GDP(百万$):'],
        localize=True,
        labels=True,
        style='background-color: white; font-size: 12px;'
    ),
    tooltip='name',
    tiles='CartoDB positron'
)
m

22.5.3 自定义弹出内容

可以通过创建新列或使用 HTML 来自定义弹出框中的内容。

import geopandas as gpd

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))

# 创建格式化的信息列
world['info'] = world.apply(
    lambda row: f"国家:{row['name']}<br>"
                f"大洲:{row['continent']}<br>"
                f"人口:{row['pop_est']:,.0f}<br>"
                f"GDP:${row['gdp_md_est']:,.0f}M",
    axis=1
)

# 使用自定义信息列作为弹出内容
m = world.explore(
    popup=['info'],
    tooltip='name',
    tiles='CartoDB positron',
    style_kwds={'fillOpacity': 0.6}
)
m
# 计算人均 GDP 并在弹出框中展示
world['gdp_per_capita'] = (
    world['gdp_md_est'] * 1e6 / world['pop_est']
).round(0)

m = world.explore(
    column='gdp_per_capita',
    cmap='RdYlGn',
    legend=True,
    legend_kwds={'caption': '人均 GDP(美元)'},
    popup=['name', 'gdp_per_capita'],
    tooltip=['name', 'gdp_per_capita'],
    tiles='CartoDB positron'
)
m

22.6 多图层叠加

22.6.1 使用 m 参数叠加图层

通过 m 参数可以将多个 GeoDataFrame 叠加到同一张地图上。

import geopandas as gpd
from shapely.geometry import Point

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
cities = gpd.read_file(gpd.datasets.get_path('naturalearth_cities'))

# 第一层:世界国家边界
m = world.explore(
    color='lightblue',
    style_kwds={
        'fillOpacity': 0.3,
        'weight': 1,
        'color': 'gray'
    },
    tooltip='name',
    name='国家边界',           # 图层名称
    tiles='CartoDB positron'
)

# 第二层:城市点(叠加到已有地图上)
cities.explore(
    m=m,                       # 关键参数:传入已有地图对象
    color='red',
    marker_kwds={
        'radius': 5,
        'fill': True
    },
    tooltip='name',
    name='主要城市'             # 图层名称
)

m
# 叠加不同类型的几何要素
import geopandas as gpd
from shapely.geometry import LineString

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))

# 创建一些示例航线数据
routes = gpd.GeoDataFrame(
    {
        'route': ['北京-东京', '北京-新加坡', '上海-悉尼'],
        'geometry': [
            LineString([(116.4, 39.9), (139.7, 35.7)]),
            LineString([(116.4, 39.9), (103.8, 1.35)]),
            LineString([(121.5, 31.2), (151.2, -33.9)])
        ]
    },
    crs='EPSG:4326'
)

# 底图:国家
m = world.explore(
    color='lightyellow',
    style_kwds={'weight': 0.5, 'fillOpacity': 0.2},
    tiles='CartoDB positron',
    name='国家'
)

# 叠加航线
routes.explore(
    m=m,
    color='red',
    style_kwds={'weight': 3, 'dashArray': '10 5'},
    tooltip='route',
    name='航线'
)

m

22.6.2 图层控制 (LayerControl)

添加图层控制器允许用户在浏览器中切换图层的显示与隐藏。

import geopandas as gpd
import folium

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
cities = gpd.read_file(gpd.datasets.get_path('naturalearth_cities'))

# 创建多图层地图
m = world.explore(
    column='continent',
    cmap='Set3',
    tooltip='name',
    name='国家(按大洲着色)',
    tiles='CartoDB positron'
)

cities.explore(
    m=m,
    color='darkred',
    marker_kwds={'radius': 4},
    tooltip='name',
    name='主要城市'
)

# 添加图层控制器
folium.LayerControl(
    collapsed=False     # 默认展开图层列表
).add_to(m)

m
# 按大洲分别创建图层
continents = ['Asia', 'Europe', 'Africa', 'North America', 'South America']
colors = ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00']

m = folium.Map(location=[20, 0], zoom_start=2, tiles='CartoDB positron')

for continent, color in zip(continents, colors):
    subset = world[world['continent'] == continent]
    subset.explore(
        m=m,
        color=color,
        style_kwds={'fillOpacity': 0.5, 'weight': 1},
        tooltip='name',
        name=continent  # 每个大洲作为独立图层
    )

folium.LayerControl().add_to(m)
m

22.7 使用 Folium 直接创建地图

22.7.1 folium.Map 基础

有时需要更精细地控制地图,可以直接使用 Folium API。

import folium

# 创建基础地图(以北京为中心)
m = folium.Map(
    location=[39.9042, 116.4074],  # 中心点坐标 [纬度, 经度]
    zoom_start=10,                  # 初始缩放级别
    tiles='OpenStreetMap',          # 底图
    width='100%',                   # 宽度
    height='500px',                 # 高度
    min_zoom=2,                     # 最小缩放级别
    max_zoom=18,                    # 最大缩放级别
    control_scale=True              # 显示比例尺
)
m
# folium.Map 的常用属性和方法
print(f"地图中心: {m.location}")
print(f"缩放级别: {m.options.get('zoom', 'N/A')}")

# 获取地图的 HTML 表示
html = m._repr_html_()
print(f"HTML 长度: {len(html)} 字符")

输出:

地图中心: [39.9042, 116.4074]
缩放级别: N/A
HTML 长度: 5832 字符

22.7.2 添加标记 (Marker)

folium.Marker 用于在地图上添加位置标记。

import folium

m = folium.Map(location=[39.9, 116.4], zoom_start=11)

# 添加基本标记
folium.Marker(
    location=[39.9042, 116.4074],
    popup='天安门广场',
    tooltip='点击查看详情'
).add_to(m)

# 添加带自定义弹出窗口的标记
popup_html = """
<div style="font-family: sans-serif; width: 200px;">
    <h4 style="color: #333;">故宫博物院</h4>
    <p>世界上现存规模最大的宫殿建筑群</p>
    <p><b>开放时间:</b>8:30-17:00</p>
</div>
"""

folium.Marker(
    location=[39.9163, 116.3972],
    popup=folium.Popup(popup_html, max_width=250),
    tooltip='故宫博物院'
).add_to(m)

# 添加多个标记
landmarks = [
    {'name': '颐和园', 'lat': 39.9998, 'lon': 116.2755},
    {'name': '天坛', 'lat': 39.8822, 'lon': 116.4066},
    {'name': '鸟巢', 'lat': 39.9929, 'lon': 116.3966},
]

for lm in landmarks:
    folium.Marker(
        location=[lm['lat'], lm['lon']],
        popup=lm['name'],
        tooltip=lm['name']
    ).add_to(m)

m

22.7.3 添加圆形标记 (CircleMarker)

CircleMarkerCircle 用于在地图上绘制圆形。

import folium

m = folium.Map(location=[35, 105], zoom_start=4)

# 城市数据(名称、纬度、经度、人口-万)
cities = [
    ('北京', 39.90, 116.41, 2189),
    ('上海', 31.23, 121.47, 2428),
    ('广州', 23.13, 113.26, 1868),
    ('深圳', 22.54, 114.06, 1756),
    ('成都', 30.57, 104.07, 2094),
]

for name, lat, lon, pop in cities:
    # CircleMarker —— 半径以像素为单位(不随缩放变化)
    folium.CircleMarker(
        location=[lat, lon],
        radius=pop / 200,           # 半径与人口成正比
        color='#3186cc',            # 边框颜色
        fill=True,
        fill_color='#3186cc',       # 填充颜色
        fill_opacity=0.6,
        popup=f'{name}{pop}万人',
        tooltip=name
    ).add_to(m)

m
# Circle —— 半径以米为单位(随缩放变化)
m = folium.Map(location=[39.9, 116.4], zoom_start=11)

# 在天安门广场周围画同心圆,表示距离范围
for radius_km in [3, 6, 9]:
    folium.Circle(
        location=[39.9042, 116.4074],
        radius=radius_km * 1000,     # 半径(米)
        color='red',
        weight=2,
        fill=True,
        fill_opacity=0.05,
        popup=f'{radius_km}公里范围'
    ).add_to(m)

m

22.8 GeoJSON 与 Choropleth

22.8.1 folium.GeoJson

folium.GeoJson 允许在地图上叠加 GeoJSON 数据并自定义样式。

import folium
import geopandas as gpd
import json

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
asia = world[world['continent'] == 'Asia']

m = folium.Map(location=[35, 90], zoom_start=3, tiles='CartoDB positron')

# 使用 GeoJson 添加矢量数据
folium.GeoJson(
    data=asia.to_json(),            # GeoJSON 字符串
    name='亚洲国家',
    style_function=lambda feature: {
        'fillColor': '#3186cc',
        'color': 'white',
        'weight': 1,
        'fillOpacity': 0.5,
    },
    highlight_function=lambda feature: {
        'weight': 3,
        'color': '#666',
        'fillOpacity': 0.8,
    },
    tooltip=folium.GeoJsonTooltip(
        fields=['name', 'pop_est'],
        aliases=['国家:', '人口:'],
        localize=True
    )
).add_to(m)

folium.LayerControl().add_to(m)
m
# 根据属性动态设置样式
import branca.colormap as cm

# 创建颜色映射
colormap = cm.LinearColormap(
    colors=['green', 'yellow', 'red'],
    vmin=asia['pop_est'].min(),
    vmax=asia['pop_est'].max(),
    caption='人口数量'
)

m = folium.Map(location=[35, 90], zoom_start=3, tiles='CartoDB positron')

folium.GeoJson(
    data=asia.to_json(),
    style_function=lambda feature: {
        'fillColor': colormap(feature['properties']['pop_est']),
        'color': 'gray',
        'weight': 1,
        'fillOpacity': 0.7,
    },
    tooltip=folium.GeoJsonTooltip(
        fields=['name', 'pop_est'],
        aliases=['国家:', '人口:']
    )
).add_to(m)

colormap.add_to(m)
m

22.8.2 folium.Choropleth

folium.Choropleth 是创建分级统计图的便捷方法。

import folium
import geopandas as gpd
import pandas as pd

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))

m = folium.Map(location=[20, 0], zoom_start=2, tiles='CartoDB positron')

# 创建 Choropleth 地图
folium.Choropleth(
    geo_data=world.to_json(),             # GeoJSON 数据
    data=world,                            # 属性数据
    columns=['name', 'pop_est'],           # [关联字段, 值字段]
    key_on='feature.properties.name',      # GeoJSON 中的关联键
    fill_color='YlOrRd',                   # 填充颜色方案
    fill_opacity=0.7,
    line_opacity=0.3,
    nan_fill_color='lightgray',            # 无数据区域颜色
    legend_name='世界各国人口数量',          # 图例名称
    bins=6                                 # 分类数
).add_to(m)

m
# 结合外部 CSV 数据创建 Choropleth
# 假设有一个包含各国指标的 CSV 文件
import pandas as pd
import geopandas as gpd
import folium

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))

# 模拟外部数据(互联网普及率)
internet_data = pd.DataFrame({
    'country': ['China', 'India', 'United States of America',
                'Brazil', 'Russia', 'Japan'],
    'internet_rate': [73.0, 43.0, 87.0, 74.0, 80.0, 92.0]
})

m = folium.Map(location=[20, 0], zoom_start=2, tiles='CartoDB positron')

folium.Choropleth(
    geo_data=world.to_json(),
    data=internet_data,
    columns=['country', 'internet_rate'],
    key_on='feature.properties.name',
    fill_color='BuGn',
    fill_opacity=0.7,
    line_opacity=0.3,
    nan_fill_color='#f0f0f0',
    legend_name='互联网普及率(%)'
).add_to(m)

m

注意: 使用 Choropleth 时,columns 中的关联字段必须与 key_on 指定的 GeoJSON 属性字段匹配。如果名称不一致会导致部分区域无法着色。


22.9 标记与图标

22.9.1 自定义图标 (Icon)

Folium 支持多种图标样式来美化地图标记。

import folium

m = folium.Map(location=[39.9, 116.4], zoom_start=12)

# 使用默认图标(蓝色标记)
folium.Marker(
    location=[39.9042, 116.4074],
    popup='默认图标',
    icon=folium.Icon()
).add_to(m)

# 使用 Font Awesome 图标
folium.Marker(
    location=[39.9163, 116.3972],
    popup='故宫',
    icon=folium.Icon(
        color='red',
        icon='university',
        prefix='fa'           # 使用 Font Awesome 图标库
    )
).add_to(m)

# 使用 Glyphicon 图标
folium.Marker(
    location=[39.9929, 116.3966],
    popup='鸟巢体育场',
    icon=folium.Icon(
        color='green',
        icon='flag',
        prefix='glyphicon'    # 使用 Glyphicon 图标库
    )
).add_to(m)

# 自定义颜色的图标
folium.Marker(
    location=[39.8822, 116.4066],
    popup='天坛',
    icon=folium.Icon(
        color='purple',
        icon_color='white',
        icon='star',
        prefix='fa'
    )
).add_to(m)

m
# 可用的图标颜色
icon_colors = [
    'red', 'blue', 'green', 'purple', 'orange',
    'darkred', 'lightred', 'beige', 'darkblue', 'darkgreen',
    'cadetblue', 'darkpurple', 'white', 'pink', 'lightblue',
    'lightgreen', 'gray', 'black', 'lightgray'
]
print(f"可用图标颜色(共 {len(icon_colors)} 种):")
print(', '.join(icon_colors))

输出:

可用图标颜色(共 19 种):
red, blue, green, purple, orange, darkred, lightred, beige, darkblue, darkgreen, cadetblue, darkpurple, white, pink, lightblue, lightgreen, gray, black, lightgray

22.9.2 标记聚类 (MarkerCluster)

当地图上有大量标记时,使用 MarkerCluster 可以自动将邻近标记聚类显示。

import folium
from folium.plugins import MarkerCluster
import random

m = folium.Map(location=[35, 105], zoom_start=4)

# 创建标记聚类对象
marker_cluster = MarkerCluster(name='城市标记').add_to(m)

# 模拟大量城市数据
cities_data = [
    ('北京', 39.90, 116.41), ('上海', 31.23, 121.47),
    ('广州', 23.13, 113.26), ('深圳', 22.54, 114.06),
    ('成都', 30.57, 104.07), ('杭州', 30.27, 120.15),
    ('武汉', 30.59, 114.31), ('西安', 34.26, 108.94),
    ('南京', 32.06, 118.80), ('重庆', 29.56, 106.55),
    ('天津', 39.13, 117.20), ('苏州', 31.30, 120.62),
    ('郑州', 34.75, 113.65), ('长沙', 28.23, 112.94),
    ('青岛', 36.07, 120.38), ('大连', 38.91, 121.60),
    ('厦门', 24.48, 118.09), ('昆明', 25.04, 102.71),
    ('哈尔滨', 45.75, 126.65), ('沈阳', 41.80, 123.43),
]

for name, lat, lon in cities_data:
    folium.Marker(
        location=[lat, lon],
        popup=name,
        tooltip=name,
        icon=folium.Icon(color='blue', icon='info-sign')
    ).add_to(marker_cluster)  # 添加到聚类对象而非地图

folium.LayerControl().add_to(m)
m
# 使用 FastMarkerCluster 处理更大量的数据
from folium.plugins import FastMarkerCluster
import numpy as np

m = folium.Map(location=[35, 105], zoom_start=4)

# 生成大量随机点(模拟海量数据)
np.random.seed(42)
n_points = 1000
lats = np.random.uniform(20, 50, n_points)
lons = np.random.uniform(80, 130, n_points)

# FastMarkerCluster 接受坐标列表,性能更优
callback = """
function (row) {
    var marker = L.marker(new L.LatLng(row[0], row[1]));
    marker.bindPopup('位置: ' + row[0].toFixed(2) + ', ' + row[1].toFixed(2));
    return marker;
}
"""

FastMarkerCluster(
    data=list(zip(lats, lons)),
    callback=callback,
    name='随机点聚类'
).add_to(m)

folium.LayerControl().add_to(m)
m

22.10 导出与嵌入

22.10.1 保存为 HTML

交互式地图可以保存为独立的 HTML 文件,在任何浏览器中打开。

import folium
import geopandas as gpd

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))

# 创建交互式地图
m = world.explore(
    column='continent',
    cmap='Set2',
    legend=True,
    tooltip=['name', 'continent'],
    tiles='CartoDB positron'
)

# 保存为 HTML 文件
m.save('world_interactive_map.html')
print("地图已保存为 world_interactive_map.html")

输出:

地图已保存为 world_interactive_map.html
# 获取 HTML 字符串(用于动态生成)
html_string = m._repr_html_()
print(f"HTML 字符串长度: {len(html_string)} 字符")
print(f"前 200 个字符:\n{html_string[:200]}...")

输出:

HTML 字符串长度: 158432 字符
前 200 个字符:
<div style="width:100%;"><div style="position:relative;width:100%;height:0;padding-bottom:60%;"><span style="color:#565656">Make this Notebook Trusted to load map: File -> Trust No...

22.10.2 在 Jupyter 中显示

在 Jupyter Notebook/Lab 中,交互式地图会自动渲染。

import geopandas as gpd

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))

# 方式 1:在 Cell 末尾直接引用变量
m = world.explore(tiles='CartoDB positron')
m  # 自动渲染地图

# 方式 2:使用 display() 函数
from IPython.display import display
display(m)

# 方式 3:使用 HTML 组件
from IPython.display import HTML
HTML(m._repr_html_())
# 控制地图在 Notebook 中的尺寸
m = world.explore(
    tiles='CartoDB positron',
    width='100%',       # 宽度占满容器
    height='400px'      # 固定高度 400 像素
)
m
# 在 Jupyter Lab 中使用 IFrame 嵌入保存的 HTML 文件
from IPython.display import IFrame

# 先保存为文件
m.save('my_map.html')

# 再用 IFrame 嵌入
IFrame('my_map.html', width=800, height=500)

22.10.3 嵌入网页

将 Folium 地图嵌入到现有网页中有多种方式。

# 方式 1:使用 iframe 嵌入
import geopandas as gpd

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
m = world.explore(tiles='CartoDB positron', tooltip='name')
m.save('map_embed.html')

# 在 HTML 页面中使用 iframe 引用
iframe_code = """
<!-- 在你的网页中添加以下代码 -->
<iframe
    src="map_embed.html"
    width="100%"
    height="600"
    style="border: 1px solid #ccc; border-radius: 4px;"
    loading="lazy"
></iframe>
"""
print(iframe_code)

输出:

<!-- 在你的网页中添加以下代码 -->
<iframe
    src="map_embed.html"
    width="100%"
    height="600"
    style="border: 1px solid #ccc; border-radius: 4px;"
    loading="lazy"
></iframe>
# 方式 2:直接嵌入 HTML 代码
html_content = m.get_root().render()

# 提取地图部分的 HTML(去掉完整页面包装)
map_div = m._repr_html_()

page_template = f"""
<!DOCTYPE html>
<html>
<head>
    <title>我的交互式地图</title>
    <style>
        body 
        h1 
        .map-container 
    </style>
</head>
<body>
    <h1>世界地图展示</h1>
    <p>以下是使用 GeoPandas 和 Folium 生成的交互式地图:</p>
    <div class="map-container">
        {map_div}
    </div>
    <p>数据来源:Natural Earth</p>
</body>
</html>
"""

with open('full_page_map.html', 'w', encoding='utf-8') as f:
    f.write(page_template)

print("完整网页已保存为 full_page_map.html")

输出:

完整网页已保存为 full_page_map.html

注意: 嵌入地图时请注意文件大小。如果 GeoDataFrame 包含大量几何数据,生成的 HTML 文件可能很大。建议在嵌入前简化几何(使用 simplify() 方法)或筛选必要的字段。


22.11 本章小结

本章介绍了使用 GeoPandas 和 Folium 创建交互式 Web 地图的方法。从 explore() 快速生成地图到直接使用 Folium API 进行精细控制,涵盖了交互式地图开发的各个方面。

主题 方法/参数 关键要点
explore() 基础 gdf.explore() 一行代码创建交互式地图,返回 folium.Map 对象
底图选择 tiles='CartoDB positron' 内置多种底图,支持自定义瓦片 URL
按属性着色 column, cmap, scheme 支持分类着色和连续着色,需要 mapclassify
弹出与提示 tooltip, popup tooltip 悬停显示,popup 点击弹出
多图层叠加 m=existing_map 将多个 GeoDataFrame 叠加到同一张地图
图层控制 folium.LayerControl() 允许用户切换图层显示/隐藏
Folium Map folium.Map() 直接创建地图对象,精细控制参数
标记 Marker, CircleMarker Marker 固定图标,CircleMarker 可缩放
GeoJson folium.GeoJson() 自定义样式函数、高亮和提示
Choropleth folium.Choropleth() 快速创建分级统计地图
自定义图标 folium.Icon() 支持 Font Awesome 和 Glyphicon 图标
标记聚类 MarkerCluster 大量标记自动聚类,提升性能
导出 HTML m.save('map.html') 独立 HTML 文件,可在浏览器打开
网页嵌入 <iframe> / 内联 HTML 通过 iframe 或直接嵌入代码集成到网页

核心要诀:

  • 快速探索用 explore():GeoPandas 内置的 explore() 方法是最快捷的交互式地图创建方式
  • 精细控制用 Folium API:需要自定义标记、图标、弹出框时直接使用 Folium
  • 多图层叠加靠 m 参数:通过传递已有地图对象实现图层叠加
  • 大数据集注意性能:使用 simplify() 简化几何、FastMarkerCluster 处理海量点
  • 底图选择影响效果:浅色底图适合数据可视化,深色底图适合亮色数据展示
  • 导出与分享用 HTMLsave() 方法生成独立 HTML 文件,便于分享和嵌入

下一章我们将学习 第23章 的内容,继续深入探索 GeoPandas 的高级功能。