R交互式图表:用 plotly 与 leaflet 做可探索的图
静态图把结论固定下来,交互图把探索的过程交出去。鼠标悬停能读出每个点的具体数值,缩放能放大重叠的区域,点击图例能临时关掉一组——这些在探索数据时省下的时间,比画图本身多得多。
代价也要先讲清楚:交互图不能进期刊 PDF,分享要靠 HTML 文件,点数上万时浏览器会卡,而且读者看到的画面取决于他自己怎么缩放——截图传出去的图,可能和你当初想说的结论不一样。
ggplotly():一行转换
Section titled “ggplotly():一行转换”已经用 ggplot2 画好的图,加一行就能变成交互图:
library(ggplot2)library(plotly)
p <- ggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl))) + geom_point(size = 2)
ggplotly(p)图形输出:在 RStudio 的 Viewer 面板(或浏览器)里出现一个可交互的散点图,点的位置和配色与静态图完全一致。鼠标悬停时弹出提示框,显示该点的wt、mpg 和 factor(cyl) 三个值;右上角工具栏提供缩放、平移、框选缩放、重置视图、导出 PNG 五个按钮;点击图例里的某一组可以隐藏或显示该组。控制台不打印任何内容。这就是 plotly 最实用的入口:把 ggplotly() 包在已有的 ggplot 对象外面,图上的一切(分面、主题、坐标轴设置)都会带过来。转换后返回的是一个 plotly 对象,可以继续用 %>% 接 layout() 改标题、改坐标轴,写法跟 ggplot2 完全不同——它对应的是 plotly.js 的属性结构。
复杂图层转换后可能走样:自定义统计图层、patchwork 拼好的组合图、部分扩展包提供的 geom,转完都要自己看一眼再发出去。
控制悬停内容
Section titled “控制悬停内容”默认提示框把该点上所有映射值都列出来。想精简,用 tooltip 指定要显示哪几项:
ggplotly(p, tooltip = c("x", "y", "colour"))更实用的是自己拼提示文字。把 text 放进 aes(),提示框就完全由你决定内容:
p2 <- ggplot(mtcars, aes(x = wt, y = mpg, text = rownames(mtcars))) + geom_point(size = 2)
ggplotly(p2, tooltip = "text")图形输出:悬停任意一个点时,提示框只显示一行车名,例如 "Mazda RX4"、"Datsun 710"、"Cadillac Fleetwood",不再显示 wt / mpg 的数值和字段名。rownames(mtcars) 正好是车型名,用它做提示比看数字直观得多。多行提示可以用 paste() 拼,加上 <br> 换行:
aes(text = paste("车型:", rownames(mtcars), "<br>车重:", wt, "千磅"))提示框里的 HTML 标签会被解析,所以 <br>、<b> 这类标签都能用。做完图记得把 tooltip 的属性留全——只显示车名而藏掉数值,读者想核对具体数据时反而要回到 R 里查。
直接用 plot_ly()
Section titled “直接用 plot_ly()”不用 ggplot2 也能画。plot_ly() 的接口更接近 plotly.js,公式写法 ~wt 表示「取 data 里的这一列」:
plot_ly( data = mtcars, x = ~wt, y = ~mpg, color = ~factor(cyl), colors = c("#0072B2", "#D55E00", "#009E73"), type = "scatter", mode = "markers", marker = list(size = 9, opacity = 0.8), hoverinfo = "text", text = ~paste("车型:", rownames(mtcars), "<br>车重:", wt, "<br>油耗:", mpg)) %>% layout( title = "车重与油耗", xaxis = list(title = "车重 (千磅)"), yaxis = list(title = "油耗 (mpg)") )图形输出:与 ggplotly() 版本相近的散点图,点按气缸数分三色,颜色取自 Okabe-Ito 配色。悬停显示「车型 / 车重 / 油耗」三行文字。左上角有标题「车重与油耗」,x、y 轴标题按 layout() 里的设置显示。plot_ly() 默认的绘图区没有网格线,背景为白色,和 ggplot2 的默认灰色背景不同。type 和 mode 决定了画什么:type = "scatter" 配 mode = "markers" 是散点图,配 mode = "lines" 是折线图,mode = "lines+markers" 两者兼有。layout() 里可改的项很多,查文档时对照 plotly.js 的参考页会比看 R 帮助更快。
plot_ly() 适合快速探索,ggplot2 适合需要精细控制、要复现的图。两者不是二选一:用 ggplot2 画好再用 ggplotly() 转换,通常是投入产出比最高的路径。
leaflet:带标记的在线地图
Section titled “leaflet:带标记的在线地图”地图是交互式可视化的另一个刚需场景。leaflet 包把地图瓦片、缩放平移、标记弹出框都封好了:
library(leaflet)
cities <- data.frame( city = c("北京", "上海", "广州", "成都", "兰州"), lng = c(116.4074, 121.4737, 113.2644, 104.0665, 103.8343), lat = c(39.9042, 31.2304, 23.1291, 30.5728, 36.0611))
cities city lng lat1 北京 116.4074 39.90422 上海 121.4737 31.23043 广州 113.2644 23.12914 成都 104.0665 30.57285 兰州 103.8343 36.0611leaflet(cities) %>% addTiles() %>% setView(lng = 108, lat = 34, zoom = 4) %>% addMarkers(lng = ~lng, lat = ~lat, popup = ~city)图形输出:一张可拖动缩放的 OpenStreetMap 底图,视野以 (108, 34) 为中心、缩放级别 4,大致覆盖中国全境。5 个蓝色水滴状标记落在北京、上海、广州、成都、兰州,点击任一标记弹出对应城市名。右下角有 OpenStreetMap 的版权署名。三个函数撑起了全部骨架。addTiles() 加默认底图——瓦片是实时从 OpenStreetMap 服务器下载的,所以必须联网:断网或内网环境下地图区域是灰的,标记还在。setView() 定初始视野,lng 是经度、lat 是纬度,顺序别写反,leaflet 沿用 GeoJSON 的习惯,经度在前。很多数据源(GPS 记录、部分 API 的返回值)给的是 lat/lng,直接扔进去会把点画到南极洲附近。如果标记全部落在海里,先检查这两个字段有没有对调。
底图样式可以换。做数据叠加时浅色底图更好用,标记和图上的信息不会被路网盖住:
addProviderTiles(providers$CartoDB.Positron)addProviderTiles(providers$Esri.WorldImagery)providers$ 后面的名字在 leaflet 的帮助页里有完整列表。注意 Stamen 系列瓦片已经迁移到 Stadia Maps,现在需要申请 API key,网上直接抄来的老代码会加载不出底图。
点多了用 addCircleMarkers() 代替 addMarkers():矢量圆圈的数量承载能力比水滴图标强得多,还可以加 clusterOptions = markerClusterOptions() 做聚合,缩放时自动把邻近的点合并成一个带数字的圆。上万个点直接画水滴图标,浏览器基本就动不了了。
交互图的分享方式是 HTML 文件。htmlwidgets::saveWidget() 把图和数据打包成一个自包含的文件:
w <- ggplotly(p)htmlwidgets::saveWidget(w, file = "scatter.html", selfcontained = TRUE)工作目录下生成 scatter.html。文件是自包含的:plotly 的 JavaScript 库和绘图数据全部内联在文件里,通常 2–4 MB,双击用浏览器打开就能交互,可以直接当附件发给同门。selfcontained = TRUE 需要 pandoc(RStudio 自带,纯命令行环境要自己装)。不想用 pandoc 就去掉这个参数,但那样会额外生成一个文件夹存放依赖,移动文件时必须整个文件夹一起走。
两个提醒。第一,HTML 文件里包含完整的原始数据,把数据发给别人之前想清楚这件事——未发表的数据、含个人信息的数据不要这样分享。第二,在 R Markdown 或 Quarto 里写分析报告时,只要代码块返回 plotly 对象,HTML 输出会自动嵌入交互图;但如果输出目标是 PDF 或 Word,knitr 会退化成静态截图,交互效果全部丢失。这也是为什么论文正文里的图还是得用静态图,把交互版放进补充材料或者线上附录。
什么时候用,什么时候不用
Section titled “什么时候用,什么时候不用”适合交互图的场景:自己探索数据(看离群点、看重叠区域、看某个点到底是什么);网页版的补充材料;组会演示时现场回答「这一组具体是多少」。
不适合的场景:期刊正文图(要矢量、要可复现、要能归档);需要读者精确比较数值的图;数据量大到浏览器卡顿的分析;以及任何会被打印出来的场合。
静态图的主题、配色和排版规范在 /r/visualization/themes/ 里讲过一遍——ggplotly() 会沿用这些设置,先把静态图调好再转交互图,比在 plotly 里重新做一遍省事得多。如果你也用 Python,静态图的构造方式可以对照 /python/visualization/matplotlib-basics/,交互图那边则是 Plotly 的 Python 版本,plotly.express 的接口和这里的 plot_ly() 同源。