2.5 地图 · Web Maps with leaflet
2.5 地图 · Web Maps with leaflet
本章目标
完成本章后,你能够:
- model 用「点 + 经纬度(lon/lat)」的空间心智模型组织数据,杜绝坐标颠倒
- construct 用
addTiles()铺底图,用addMarkers()/addCircleMarkers()叠加标记层 - differentiate label(悬停轻提示)与 popup(点击详情卡)两种信息 affordance
- encode 用
colorNumeric()/colorFactor()+addLegend()把数据映射为颜色 - build 用 sf +
addPolygons()做面上填色(choropleth-lite),并知道何时要除基数 - orchestrate 图层控制、
setView()/fitBounds()取景,并保存可分享的地图部件
前置自测(≤5 分钟)
知道数据框「行 = 观测、列 = 变量」、会用 filter() 再继续;否则先回补 tidyverse 入门要求:
quakes是 R 自带数据(斐济附近 1000 次地震):运行head(quakes),说出哪列是经度、哪列是纬度。- 经度 178°E、纬度 20°S 在地球上是哪片海域?(答对恭喜,这就是本章主场。)
1. 空间心智模型:点,以及经纬度
网页地图的最小数据单位是点:一对坐标(经度 longitude = lng,纬度 latitude = lat)。
| 概念 | 常见写法 | 方向 | 直觉 |
|---|---|---|---|
| 经度 | lng / lon / long | 东西 | 相当于 x;负数 = 西经 |
| 纬度 | lat | 南北 | 相当于 y;负数 = 南纬 |
head(quakes)
# long ≈ 166–188(在斐济附近横跨国际日期变更线),lat ≈ -38 到 -10(南纬)→ 本章 1000 个地震点主场addMarkers(lng = ~lat, lat = ~long) 不报错,只是把点画到完全错误的地方——§3 的 Check In 会让你亲手犯一次,终身免疫。
leaflet 说的是 WGS84(EPSG:4326) 经纬度。GIS 数据若在其他坐标系(投影),先 sf::st_transform(x, 4326) 再进地图(§5 会用到)。
2. 底图:addTiles()
地图 = 底图(瓦片,tiles:服务器上的小图片拼成的画布)+ 你的数据层。先铺底图:
library(leaflet)
leaflet() |> # 最小地图:默认 OpenStreetMap 瓦片
addTiles() |>
setView(lng = 178, lat = -20, zoom = 4)
# 正式交付常换更「安静」的底图,好让数据当主角:
leaflet() |>
addProviderTiles(providers$CartoDB.Positron) |> # 浅色低干扰
setView(lng = 178, lat = -20, zoom = 4)右下角的 attribution(版权行)随瓦片自动带上——正式交付别裁掉它。瓦片来自网络服务器:离线或内网受限环境下地图只剩灰底(§7 再回到这点)。
3. 标记层:addMarkers / addCircleMarkers
leaflet(data = quakes[1:200, ]) |>
addTiles() |>
addMarkers(lng = ~long, lat = ~lat,
label = ~as.character(mag), # 悬停:一个数
popup = ~paste0("震级 ", mag, " · 深度 ", depth,
" km · 台站 ", stations)) # 点击:详情卡| affordance | 触发 | 适合放什么 |
|---|---|---|
| label | 悬停 | 一个词、一个数——快速扫读 |
| popup | 点击 | 多字段结构化详情 |
图钉标记(markers)适合少量带名字的点;数据一多、还想让大小/颜色随数据变化,换矢量圆 addCircleMarkers():
leaflet(data = quakes) |>
addTiles() |>
addCircleMarkers(lng = ~long, lat = ~lat,
radius = ~mag / 2, # 半径随震级
stroke = FALSE, fillOpacity = 0.5)点数上千时,图钉标记会拖慢浏览器;可开聚合:addMarkers(clusterOptions = markerClusterOptions()),缩放时自动聚散。
先渲 setView(lng = -122.4, lat = 37.8, zoom = 10)(旧金山)确认坐标感;再故意交换成 setView(lng = 37.8, lat = -122.4),观察视角飞去了哪里。截图留档——这个 bug 值得见一次症状,胜过十次说教。
4. 用数据上色:colorNumeric / colorFactor + addLegend
连续数值 → 连续色阶(colorNumeric);类别 → 离散色(colorFactor)。套路是先建「翻译器」(色板函数),再喂给颜色映射:
pal <- colorNumeric(palette = "viridis", domain = quakes$mag)
leaflet(quakes) |>
addProviderTiles(providers$CartoDB.Positron) |>
addCircleMarkers(lng = ~long, lat = ~lat, radius = 3,
color = ~pal(mag), stroke = FALSE, fillOpacity = 0.7,
popup = ~paste0("震级 ", mag)) |>
addLegend(pal = pal, values = ~mag, opacity = 1, title = "震级")
# 类别上色:先把连续变量分箱,再映射离散色(colorFactor)
quakes$depth_band <- cut(quakes$depth, breaks = c(0, 200, 400, 700),
labels = c("浅层", "中层", "深层"))
pal_band <- colorFactor("Set1", domain = quakes$depth_band)
# 之后 color = ~pal_band(depth_band) + addLegend(pal = pal_band, values = ~depth_band)选口诀:数值连续→colorNumeric;类别→colorFactor;等宽分箱还有 colorBin。addLegend() 不是可选装饰——没有图例的颜色就是哑谜。
5. 面上填色:sf + addPolygons(choropleth-lite)
点之外的第二种空间单位是面(polygon):行政区、流域、网格。sf 包内置的 nc 数据(北卡罗来纳 100 县,公共教学数据)是标准练习场:
library(sf)
nc <- st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE) |>
st_transform(4326) # leaflet 只认 WGS84
pal_nc <- colorNumeric("YlOrRd", domain = nc$SID79)
leaflet(nc) |>
addProviderTiles(providers$CartoDB.Positron) |>
addPolygons(weight = 1, color = "white",
fillColor = ~pal_nc(SID79), fillOpacity = 0.7,
highlightOptions = highlightOptions(weight = 3, bringToFront = TRUE),
popup = ~paste0(NAME, " 县:SIDS ", SID79, " 例"))SID79 是计数:人多的县天然更多。想让读者看到风险而非人口规模,就除以基数(rate = SID79 / BIR79 * 1000,每千活产)再上色,原始数放进 popup。
choropleth 的默认谎言是总量填色:你以为在讲疾病,其实在讲人口。动手前先自问「这块颜色代表强度还是规模」;想讲故事就归一,想保细节就把原始数交给 popup。底图同理——它从来不是中性背景:地名、边界、配色都是某个版本的地理叙事,正式交付前多换一个 provider 看一眼。
6. 图层控制与视角:addLayersControl / setView / fitBounds
图层(group)是地图的「目录」:底图单选、数据层多选,读者自己开关;视角(view)决定读者第一眼看到哪里:
m <- leaflet(quakes) |>
addTiles(group = "OSM 默认") |>
addProviderTiles(providers$CartoDB.Positron, group = "浅色底图") |>
addCircleMarkers(lng = ~long, lat = ~lat, radius = ~mag / 2,
stroke = FALSE, fillOpacity = 0.5,
color = ~pal(mag), group = "地震点") |>
addLayersControl(baseGroups = c("OSM 默认", "浅色底图"),
overlayGroups = c("地震点"),
options = layersControlOptions(collapsed = FALSE))
m |> setView(lng = 178, lat = -20, zoom = 5) # 定点定焦
m |> fitBounds(lng1 = 165, lat1 = -38, lng2 = 190, lat2 = -10) # 恰好框住数据baseGroups 互斥单选,overlayGroups 可多选叠加;collapsed = FALSE 让控件常开(省读者一次点击)。取景即叙事:fitBounds() 交代全局,setView() 把镜头对准你要讲的那块。
7. 保存地图部件
leaflet 地图与 2.1 章的 plotly 部件同属 htmlwidgets,保存方式一致:
htmlwidgets::saveWidget(m, "quakes-map.html") # 自包含单文件(权衡见 2.1 §5)部件里你的数据是内嵌的,但瓦片来自网络:断网打开只剩数据层和灰底。交付时要么声明「需联网」,要么走自托管瓦片方案(超出本章,见 leaflet 文档)。Quarto html 里 leaflet 输出会自动内嵌,无需另存。
复跑 §2 底图 + §3 前 200 个震点的 addMarkers() 图;再用 setView() 把视角移到你所在城市(自查经纬度)zoom = 10,与默认视角截图对比。
给 §4 的 colorNumeric 图加三件事:① radius = ~mag / 2 映射;② §4 的深度分箱图层(colorFactor)作为第二图层;③ addLayersControl() 切换两个数据层与两种底图,最后 fitBounds() 框住全部震点。
第一轮(禁用 AI):把 §5 的 nc 地图改成「每千活产 SIDS 率」(SID79 / BIR79 * 1000)上色,popup 同时给出率与原始数,配 addLegend()。 第二轮(开放 AI):只问 Posit Assistant 一个问题:「我的配色对色盲读者友好吗?给一个可验证的改法。」记录建议并实测修改前后你自己对色阶的分辨体验。
Capstone · 压轴项目
任务:「一页地图简报」。点数据(quakes 换一列变量,或自备 CSV:城市/门店/站点)或面数据(nc 换变量,或自备 shapefile)二选一,交付:① leaflet 地图:底图切换 + 数据图层 + 图例 + popup/label;② 视角控制:setView() 或 fitBounds(),并用一句话说明为什么这样取景;③ saveWidget() 独立 html;④ Quarto 简报一页:为什么这组信息值得用地图讲、色阶与半径的选择理由、坐标系与底图归因说明。
| 维度 | 达到 | 良好 | 卓越 |
|---|---|---|---|
| 空间心智 | 经纬度无颠倒 | 说明数据坐标系与 4326 的关系 | 取景(view/zoom)有明确叙事意图 |
| 视觉编码 | 颜色映射数值且有图例 | 色阶/半径选择有理由(连续 vs 分类) | 声明色盲友好性与(choropleth)基数归一 |
| 交互设计 | popup 或 label 可用 | 图层控制 + 底图切换就位 | 交互不过载:每个控件服务一个问题 |
| 发布边界 | saveWidget 能独立打开 | 说明离线时底图失效的风险 | 提供静态截图 fallback 与归因说明 |
SOURCES · 来源映射
| 讲义节 | 素材 | 性质 |
|---|---|---|
| 全章结构、示例、练习、capstone、rubric | 本项目 | 原创 |
addTiles / addMarkers / addCircleMarkers / colorNumeric / addPolygons / addLayersControl 等用法 |
Leaflet for R 官方文档 https://rstudio.github.io/leaflet/ | 引用 |
| nc.shp 示例数据 | sf 包内置北卡县界数据(公共教学数据) | 数据 |
本章以 CC-BY-SA 4.0 发布。