本指南通过完整示例介绍 R 的 Plotly 包,所有执行示例都使用 R 或 ggplot2 自带的数据,并直接在 R Markdown 中生成互动图表。最终 HTML 是自包含文件:代码、数据、Plotly JavaScript 运行库与图形规格都封装在同一个文件中。
即使没有服务器,Plotly 图表也支持多种浏览器端操作:
只有在互动能回答真实问题时,Plotly 才最有价值:这个点是谁?这个时间窗口发生了什么?这些组有何差异? 互动应该揭示信息,而不是补救本身就不清楚的图表。
一个 Plotly figure 有四个关键部分:
Trace
是一组具有相同类型、样式规则、图例项和悬停行为的图形标记。一张图可以包含多个
trace。x = ~wt 这样的 R 公式告诉 Plotly 在指定
data 数据框中寻找 wt 列。
model_demo <- plot_ly(
cars,
x = ~wt,
y = ~mpg,
type = "scatter",
mode = "markers"
)
built_demo <- plotly_build(model_demo)
names(built_demo$x)
#> [1] "visdat" "cur_data" "attrs" "layout" "source" "config"
#> [7] "data" "highlight" "shinyEvents" "base_url"
length(built_demo$x$data)
#> [1] 1plotly_build() 可以检查公式、映射、默认值和 trace
拆分全部解析后的最终结构。需要排查传给 Plotly.js 的确切 JSON 时,可使用
plotly_json()。
plot_ly() 图表教学和生产代码中,建议显式写出 type 与
mode。这里 type = "scatter" 选择散点 trace
系列,mode = "markers" 表示绘制点而不是连线。
p_first <- plot_ly(
cars,
x = ~wt,
y = ~mpg,
text = ~model,
type = "scatter",
mode = "markers",
marker = list(size = 10, color = "#0b6b66", opacity = 0.76),
hovertemplate = paste(
"<b>%{text}</b>",
"重量:%{x:.2f}(千磅)",
"燃油效率:%{y:.1f} mpg",
"<extra></extra>",
sep = "<br>"
)
) |>
plotly::layout(
title = list(text = "较轻的汽车通常更省油"),
xaxis = c(list(title = "重量(千磅)"), axis_clean),
yaxis = c(list(title = "燃油效率(mpg)"), axis_clean),
hovermode = "closest"
) |>
finish_plotly()
p_first最后的空标签 <extra></extra>
会移除悬停框中额外的 trace
名称区域。即使存在悬停提示,标题和轴标签依然不可省略,因为读者应该在操作图表前理解它。
color = ~Species、symbol = ~Species、size = ~hp
与 split = ~group 会把变量映射到视觉属性,并经常按变量拆分
trace。用 I()
包裹字面值,表示固定视觉属性,而不是数据映射。
p_mapping <- plot_ly(
iris,
x = ~Sepal.Length,
y = ~Petal.Length,
color = ~Species,
symbol = ~Species,
colors = c("#0b6b66", "#a4422f", "#3d7085"),
symbols = c("circle", "diamond", "square"),
type = "scatter",
mode = "markers",
marker = list(size = 9, opacity = 0.72),
hovertemplate = paste(
"物种:%{fullData.name}",
"萼片长度:%{x:.1f}",
"花瓣长度:%{y:.1f}",
"<extra></extra>",
sep = "<br>"
)
) |>
plotly::layout(
title = list(text = "颜色与符号双重编码让物种更易区分"),
xaxis = c(list(title = "萼片长度(cm)"), axis_clean),
yaxis = c(list(title = "花瓣长度(cm)"), axis_clean),
legend = list(orientation = "h", x = 0, y = -0.2)
) |>
finish_plotly()
p_mapping面积比位置更难精确比较。气泡大小适合表达近似的第三变量;应限制合理的大小范围,并在悬停提示中保留精确值。
p_bubble <- plot_ly(
cars,
x = ~wt,
y = ~mpg,
size = ~hp,
sizes = c(9, 38),
customdata = ~hp,
color = ~factor(cyl),
colors = c("#3d7085", "#8a5a10", "#a4422f"),
text = ~model,
type = "scatter",
mode = "markers",
marker = list(opacity = 0.66, line = list(color = "white", width = 1)),
hovertemplate = paste(
"<b>%{text}</b>",
"重量:%{x:.2f}",
"MPG:%{y:.1f}",
"马力:%{customdata:.0f}",
"<extra></extra>",
sep = "<br>"
)
) |>
plotly::layout(
title = list(text = "气泡大小表示马力"),
xaxis = c(list(title = "重量(千磅)"), axis_clean),
yaxis = c(list(title = "燃油效率(mpg)"), axis_clean),
legend = list(title = list(text = "汽缸数"))
) |>
finish_plotly()
p_bubble固定颜色可在高层映射参数中使用 color =
I(“#0b6b66”),或使用 marker = list(color =
“#0b6b66”)。直接写 color = “#0b6b66”
可能被当作标度输入,而不是预期的常量。
add_*() 叠加 traceadd_trace()
是通用接口;add_markers()、add_lines()、add_bars()、add_segments()
与 add_text()
等辅助函数能更清楚地表达意图。每层既可以继承初始数据和映射,也可以使用不同数据,或设
inherit = FALSE 后完整指定自身参数。
fit <- lm(mpg ~ wt, data = cars)
fit_line <- data.frame(wt = seq(min(cars$wt), max(cars$wt), length.out = 100))
fit_line$mpg <- predict(fit, newdata = fit_line)
p_layered <- plot_ly() |>
add_markers(
data = cars,
x = ~wt,
y = ~mpg,
text = ~model,
name = "汽车",
marker = list(size = 9, color = "#3d7085", opacity = 0.68),
hovertemplate = "<b>%{text}</b><br>重量:%{x:.2f}<br>MPG:%{y:.1f}<extra></extra>",
inherit = FALSE
) |>
add_lines(
data = fit_line,
x = ~wt,
y = ~mpg,
name = "线性拟合",
line = list(color = "#a4422f", width = 3),
hovertemplate = "重量:%{x:.2f}<br>拟合 MPG:%{y:.1f}<extra></extra>",
inherit = FALSE
) |>
plotly::layout(
title = list(text = "不同 trace 可以使用不同数据"),
xaxis = c(list(title = "重量(千磅)"), axis_clean),
yaxis = c(list(title = "燃油效率(mpg)"), axis_clean)
) |>
finish_plotly()
p_layeredwave <- data.frame(x = seq(0, 2 * pi, length.out = 240))
wave$sin_x <- sin(wave$x)
wave$cos_x <- cos(wave$x)
p_wave <- plot_ly(wave, x = ~x) |>
add_lines(y = ~sin_x, name = "sin(x)", line = list(color = "#0b6b66", width = 3)) |>
add_lines(y = ~cos_x, name = "cos(x)", line = list(color = "#a4422f", width = 3, dash = "dot")) |>
plotly::layout(
title = list(text = "图例项对应 trace"),
xaxis = c(list(title = "x"), axis_clean),
yaxis = c(list(title = "函数值", range = c(-1.1, 1.1)), axis_clean),
legend = list(orientation = "h", x = 0, y = -0.2)
) |>
finish_plotly()
p_wave线图编码有顺序的路径。绘图前应在每个组内按 x 排序。按 x
顺序连接的序列使用
add_lines();只有当数据行顺序本身表示轨迹时才使用
add_paths()。
air <- data.frame(
date = seq(as.Date("1949-01-01"), by = "month", length.out = length(AirPassengers)),
passengers = as.numeric(AirPassengers)
)
p_air <- plot_ly(
air,
x = ~date,
y = ~passengers,
type = "scatter",
mode = "lines",
line = list(color = "#0b6b66", width = 2),
hovertemplate = "%{x|%Y 年 %m 月}<br>%{y:,.0f} 千人<extra></extra>"
) |>
plotly::layout(
title = list(text = "1949–1960 年月度航空旅客数"),
xaxis = c(
list(
title = NULL,
rangeslider = list(visible = TRUE, thickness = 0.09),
rangeselector = list(
buttons = list(
list(count = 12, label = "1 年", step = "month", stepmode = "backward"),
list(count = 36, label = "3 年", step = "month", stepmode = "backward"),
list(step = "all", label = "全部")
)
)
),
axis_clean
),
yaxis = c(list(title = "旅客数(千人)"), axis_clean),
margin = list(t = 70, b = 70)
) |>
finish_plotly()
p_airsplit 绘制多组序列stock_matrix <- as.matrix(EuStockMarkets)
stocks <- data.frame(
observation = rep(seq_len(nrow(stock_matrix)), times = ncol(stock_matrix)),
value = as.vector(stock_matrix),
market = rep(colnames(stock_matrix), each = nrow(stock_matrix))
)
p_stocks <- plot_ly(
stocks,
x = ~observation,
y = ~value,
split = ~market,
color = ~market,
colors = c("#0b6b66", "#a4422f", "#3d7085", "#76618c"),
type = "scatter",
mode = "lines",
line = list(width = 1.4),
hovertemplate = "%{fullData.name}<br>观察序号:%{x}<br>指数:%{y:,.1f}<extra></extra>"
) |>
plotly::layout(
title = list(text = "split 为每个市场建立一个 trace"),
xaxis = c(list(title = "交易日观察序号"), axis_clean),
yaxis = c(list(title = "指数水平"), axis_clean),
hovermode = "x unified"
) |>
finish_plotly()
p_stocksPlotly 的柱状 trace 直接绘制提供的数值。与 ggplot2 的
geom_bar() 不同,add_bars()
不会自动计数。应先汇总数据,再选择与问题一致的柱状排列方式。
gear_cyl <- as.data.frame(table(
Cylinders = factor(cars$cyl),
Gears = factor(cars$gear)
))
p_grouped_bar <- plot_ly(
gear_cyl,
x = ~Cylinders,
y = ~Freq,
color = ~Gears,
colors = c("#0b6b66", "#8a5a10", "#a4422f"),
type = "bar",
hovertemplate = "汽缸数:%{x}<br>汽车数:%{y}<extra>%{fullData.name} 挡</extra>"
) |>
plotly::layout(
title = list(text = "并列柱状图比较绝对数量"),
xaxis = list(title = "汽缸数"),
yaxis = c(list(title = "汽车数量", rangemode = "tozero"), axis_clean),
barmode = "group",
bargap = 0.18,
legend = list(title = list(text = "挡位"))
) |>
finish_plotly()
p_grouped_bartitanic_counts <- aggregate(
Freq ~ Class + Survived,
data = as.data.frame(Titanic),
FUN = sum
)
p_stacked_bar <- plot_ly(
titanic_counts,
x = ~Class,
y = ~Freq,
color = ~Survived,
colors = c("#a4422f", "#0b6b66"),
type = "bar",
hovertemplate = "%{x}<br>乘客数:%{y}<extra>生还:%{fullData.name}</extra>"
) |>
plotly::layout(
title = list(text = "堆叠柱同时展示总量与构成"),
xaxis = list(title = "乘客舱级"),
yaxis = c(list(title = "乘客数量"), axis_clean),
barmode = "stack",
legend = list(title = list(text = "生还"))
) |>
finish_plotly()
p_stacked_barbarmode = "relative" 可让正负柱向相反方向延伸。Plotly
不会自动把堆叠柱变成 100% 柱;需要先计算比例再绘图。
p_histogram <- plot_ly(
faithful,
x = ~waiting,
type = "histogram",
nbinsx = 20,
marker = list(color = "#3d7085", line = list(color = "white", width = 1)),
hovertemplate = "等待时间箱:%{x}<br>观察数:%{y}<extra></extra>"
) |>
plotly::layout(
title = list(text = "老忠实泉喷发等待时间"),
xaxis = c(list(title = "等待时间(分钟)"), axis_clean),
yaxis = c(list(title = "频数"), axis_clean),
bargap = 0.04
) |>
finish_plotly()
p_histogramp_box <- plot_ly(
iris,
x = ~Species,
y = ~Sepal.Width,
color = ~Species,
colors = c("#0b6b66", "#a4422f", "#3d7085"),
type = "box",
boxpoints = "outliers",
jitter = 0.25,
pointpos = 0,
hovertemplate = "%{fullData.name}<br>萼片宽度:%{y:.1f} cm<extra></extra>"
) |>
plotly::layout(
title = list(text = "箱线图汇总各组分布"),
xaxis = list(title = NULL),
yaxis = c(list(title = "萼片宽度(cm)"), axis_clean),
showlegend = FALSE
) |>
finish_plotly()
p_boxp_violin <- plot_ly(
iris,
x = ~Species,
y = ~Petal.Length,
color = ~Species,
colors = c("#0b6b66", "#a4422f", "#3d7085"),
type = "violin",
box = list(visible = TRUE),
meanline = list(visible = TRUE),
points = FALSE,
hoveron = "violins+points",
hovertemplate = "%{fullData.name}<br>花瓣长度:%{y:.1f} cm<extra></extra>"
) |>
plotly::layout(
title = list(text = "小提琴展示形状,箱线展示稳健摘要"),
xaxis = list(title = NULL),
yaxis = c(list(title = "花瓣长度(cm)"), axis_clean),
showlegend = FALSE
) |>
finish_plotly()
p_violin矩阵类 trace 接受按行列排列的 z
值。热图通常比三维表面更适合精确比较;只有当旋转和形状探索确实增加信息时才应使用三维图。
p_heatmap <- plot_ly(
x = seq_len(ncol(volcano)),
y = seq_len(nrow(volcano)),
z = volcano,
type = "heatmap",
colors = colorRamp(c("#e9f5f1", "#3d7085", "#8a5a10", "#a4422f")),
colorbar = list(title = "高程"),
hovertemplate = "列:%{x}<br>行:%{y}<br>高程:%{z}<extra></extra>"
) |>
plotly::layout(
title = list(text = "volcano 矩阵热图"),
xaxis = list(title = "网格列", constrain = "domain"),
yaxis = list(title = "网格行", scaleanchor = "x")
) |>
finish_plotly()
p_heatmapp_surface <- plot_ly(
z = volcano,
type = "surface",
colors = colorRamp(c("#e9f5f1", "#3d7085", "#8a5a10", "#a4422f")),
colorbar = list(title = "高程"),
hovertemplate = "x:%{x}<br>y:%{y}<br>z:%{z}<extra></extra>"
) |>
plotly::layout(
title = list(text = "旋转三维表面探索地形"),
scene = list(
xaxis = list(title = "网格 x"),
yaxis = list(title = "网格 y"),
zaxis = list(title = "高程"),
aspectmode = "data",
camera = list(eye = list(x = 1.45, y = 1.35, z = 0.85))
),
margin = list(l = 10, r = 10, b = 10, t = 70)
) |>
finish_plotly()
p_surface透视和遮挡会让三维数值难以比较。精确比较很重要时,请同时提供热图、等高线图或数据表。
ggplotly() 让 ggplot2 互动化ggplotly() 会把现有 ggplot 对象转换为 Plotly
trace。如果已经有一张完善的静态
ggplot,这是最快的起点。但转换不是逐像素复制:不支持的
geom、主题细节、注释和图例行为可能发生变化。
g_scatter <- ggplot(
cars,
aes(
wt,
mpg,
color = factor(cyl),
text = paste0(
"<b>", model, "</b>",
"<br>重量:", format(wt, digits = 3),
"<br>MPG:", mpg,
"<br>马力:", hp
)
)
) +
geom_point(size = 3, alpha = 0.72) +
scale_color_manual(values = c("4" = "#3d7085", "6" = "#8a5a10", "8" = "#a4422f")) +
labs(
title = "ggplot 可以转换为互动图",
x = "重量(千磅)",
y = "燃油效率(mpg)",
color = "汽缸数"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom", panel.grid.minor = element_blank())
p_ggplotly <- ggplotly(g_scatter, tooltip = "text") |>
finish_plotly()
p_ggplotlyg_facet <- ggplot(
iris,
aes(
Sepal.Length,
Petal.Length,
text = paste0(
"物种:", Species,
"<br>萼片长度:", Sepal.Length,
"<br>花瓣长度:", Petal.Length
)
)
) +
geom_point(color = "#0b6b66", alpha = 0.7) +
facet_wrap(~Species, nrow = 1) +
labs(
title = "分面会转换成 Plotly 子图",
x = "萼片长度(cm)",
y = "花瓣长度(cm)"
) +
theme_minimal(base_size = 11) +
theme(panel.grid.minor = element_blank())
p_ggfacet <- ggplotly(g_facet, tooltip = "text") |>
plotly::layout(hovermode = "closest") |>
finish_plotly()
p_ggfacetlayout() 接收与 Plotly.js
属性对应的嵌套列表,用于控制分析图形的呈现;config()
控制工具栏与浏览器行为。区分二者能让可复用函数更容易理解。
peak_row <- air[which.max(air$passengers), ]
p_layout <- plot_ly(
air,
x = ~date,
y = ~passengers,
type = "scatter",
mode = "lines+markers",
line = list(color = "#3d7085", width = 2),
marker = list(size = 4),
hovertemplate = "%{x|%Y 年 %m 月}<br>%{y:,.0f} 千人<extra></extra>"
) |>
plotly::layout(
title = list(
text = "航空旅行增长的同时,季节波动也在扩大<br><sup>月度国际航空旅客数</sup>",
x = 0,
xanchor = "left"
),
xaxis = c(list(title = NULL, tickformat = "%Y"), axis_clean),
yaxis = c(list(title = "旅客数(千人)", rangemode = "tozero"), axis_clean),
hovermode = "x unified",
annotations = list(list(
x = peak_row$date,
y = peak_row$passengers,
text = "序列最大值",
showarrow = TRUE,
arrowcolor = "#a4422f",
ax = -55,
ay = -45
)),
shapes = list(list(
type = "line",
x0 = min(air$date),
x1 = max(air$date),
y0 = mean(air$passengers),
y1 = mean(air$passengers),
line = list(color = "#a4422f", dash = "dash", width = 1.5)
)),
margin = list(l = 70, r = 25, b = 55, t = 90)
) |>
plotly::config(
displaylogo = FALSE,
responsive = TRUE,
scrollZoom = FALSE,
modeBarButtonsToRemove = c("lasso2d")
)
p_layout常用布局模式包括:
| 任务 | 属性 |
|---|---|
| 使用对数轴 | yaxis = list(type = "log") |
| 固定可见范围 | xaxis = list(range = c(min, max)) |
| 保持 x/y 单位相同 | yaxis = list(scaleanchor = "x") |
| 使用水平图例 | legend = list(orientation = "h") |
| 在一个 x 位置比较 trace | hovermode = "x unified" |
| 绘制参考区域 | shapes = list(list(type = "rect", ...)) |
hovertemplate 与 customdata 精确控制悬停hovertemplate 可以精确控制内容与格式。Plotly 变量使用
%{...} 占位符和 D3 风格格式。额外信息通过 text
或 customdata 提供;不要把所有列都塞进每个提示框。
hover_matrix <- cbind(cars$hp, cars$qsec)
p_hover <- plot_ly(
cars,
x = ~wt,
y = ~mpg,
text = ~model,
customdata = hover_matrix,
type = "scatter",
mode = "markers",
marker = list(size = 10, color = "#0b6b66", opacity = 0.75),
hovertemplate = paste(
"<b>%{text}</b>",
"重量:%{x:.2f}(千磅)",
"燃油效率:%{y:.1f} mpg",
"马力:%{customdata[0]:.0f}",
"四分之一英里:%{customdata[1]:.2f} 秒",
"<extra></extra>",
sep = "<br>"
)
) |>
plotly::layout(
title = list(text = "customdata 将元数据附着到每个点"),
xaxis = c(list(title = "重量(千磅)"), axis_clean),
yaxis = c(list(title = "燃油效率(mpg)"), axis_clean)
) |>
finish_plotly()
p_hover如果 customdata 混合不同 R 类型,cbind()
可能把所有内容转成字符。需要数值格式时,应保持自定义数据矩阵为数值型,或提前构造明确的数据结构。
subplot() 与 Crosstalk 协调视图air$change <- c(NA, diff(air$passengers))
air_change <- air[!is.na(air$change), ]
p_level <- plot_ly(
air,
x = ~date,
y = ~passengers,
type = "scatter",
mode = "lines",
line = list(color = "#0b6b66", width = 2),
name = "水平",
hovertemplate = "%{x|%Y 年 %m 月}<br>水平:%{y:,.0f}<extra></extra>"
)
p_change <- plot_ly(
air_change,
x = ~date,
y = ~change,
type = "bar",
marker = list(color = ifelse(air_change$change >= 0, "#3d7085", "#a4422f")),
name = "月度变化",
hovertemplate = "%{x|%Y 年 %m 月}<br>变化:%{y:+,.0f}<extra></extra>"
)
p_subplot <- subplot(
p_level,
p_change,
nrows = 2,
heights = c(0.64, 0.36),
shareX = TRUE,
titleX = FALSE,
margin = 0.055
) |>
plotly::layout(
title = list(text = "序列水平与环比变化"),
showlegend = FALSE,
hovermode = "x unified"
) |>
finish_plotly()
p_subplotsubplot() 可设置
widths、heights、nrows、shareX
与
shareY。较小边距能节省空间,但必须为刻度和标题留足位置。组合时不同布局会被合并,完成后应检查结果。
Crosstalk 可以在静态 HTML 中联动 Plotly
视图。highlight_key() 附加稳定键,highlight()
定义浏览器选择后如何淡化和突出观察。该联动不需要 Shiny 服务器。
shared_cars <- highlight_key(cars, ~model, group = "cars-link")
p_weight <- plot_ly(
shared_cars,
x = ~wt,
y = ~mpg,
type = "scatter",
mode = "markers",
marker = list(size = 9, color = "#3d7085", opacity = 0.75),
text = ~model,
hovertemplate = "%{text}<br>重量:%{x:.2f}<br>MPG:%{y:.1f}<extra></extra>"
)
p_power <- plot_ly(
shared_cars,
x = ~hp,
y = ~qsec,
type = "scatter",
mode = "markers",
marker = list(size = 9, color = "#8a5a10", opacity = 0.75),
text = ~model,
hovertemplate = "%{text}<br>马力:%{x}<br>四分之一英里:%{y:.2f} 秒<extra></extra>"
)
p_linked <- subplot(p_weight, p_power, margin = 0.08, titleX = TRUE, titleY = TRUE) |>
plotly::layout(
title = list(text = "在任一面板拖动框选"),
dragmode = "select",
showlegend = FALSE
) |>
highlight(
on = "plotly_selected",
persistent = TRUE,
dynamic = FALSE,
color = "#a4422f",
opacityDim = 0.18
) |>
finish_plotly()
p_linkedframe 与稳定 ids 创建动画动画应该展示有顺序状态中的有意义变化。稳定的字符串 ids
告诉 Plotly 哪个对象在不同 frame
中持续存在,从而获得更平滑的过渡并避免对象身份交换。
animation_data <- do.call(
rbind,
lapply(seq_len(10), function(frame_no) {
id <- seq_len(8)
angle <- 2 * pi * (id - 1) / 8 + (frame_no - 1) * pi / 20
data.frame(
id = paste0("point-", id),
frame = sprintf("%02d", frame_no),
x = cos(angle),
y = sin(angle),
value = id
)
})
)
p_animation <- plot_ly(
animation_data,
x = ~x,
y = ~y,
frame = ~frame,
ids = ~id,
color = ~factor(value),
colors = colorRamp(c("#0b6b66", "#3d7085", "#a4422f")),
type = "scatter",
mode = "markers+text",
text = ~value,
textposition = "middle center",
marker = list(size = 34, line = list(color = "white", width = 1)),
hovertemplate = "对象 %{text}<extra></extra>",
showlegend = FALSE
) |>
animation_opts(frame = 650, transition = 320, easing = "cubic-in-out", redraw = FALSE) |>
animation_slider(currentvalue = list(prefix = "帧:")) |>
animation_button(label = "播放") |>
plotly::layout(
title = list(text = "稳定 ID 在各帧中保留对象身份"),
xaxis = list(title = NULL, range = c(-1.35, 1.35), visible = FALSE),
yaxis = list(title = NULL, range = c(-1.35, 1.35), visible = FALSE, scaleanchor = "x"),
margin = list(l = 20, r = 20, b = 80, t = 70)
) |>
finish_plotly()
p_animation即使动画削弱比较,它仍可能吸引注意。当读者需要精确比较状态时,小多图或静态前后对照通常更好。
plot_geo() 创建地理图plot_geo() 无需 Mapbox token 即可创建地理
trace。下例使用 R 自带的州缩写,offline = TRUE 则让配套包
plotlyGeoAssets
提供本地地图资源;完整渲染本教程需要安装该包。
if (!requireNamespace("plotlyGeoAssets", quietly = TRUE)) {
stop("请安装 plotlyGeoAssets 后再渲染离线地图示例。")
}
states <- data.frame(
state = state.name,
code = state.abb,
income = state.x77[, "Income"],
population = state.x77[, "Population"]
)
p_geo <- plot_geo(states, offline = TRUE) |>
add_trace(
z = ~income,
locations = ~code,
locationmode = "USA-states",
type = "choropleth",
text = ~state,
customdata = ~population,
colors = "Blues",
colorbar = list(title = "人均收入"),
marker = list(line = list(color = "white", width = 0.6)),
hovertemplate = paste(
"<b>%{text}</b>",
"人均收入(1974 年美元):%{z:,.0f}",
"人口(千人):%{customdata:,.0f}",
"<extra></extra>",
sep = "<br>"
)
) |>
plotly::layout(
title = list(text = "1974 年美国各州人均收入"),
geo = list(
scope = "usa",
projection = list(type = "albers usa"),
showlakes = TRUE,
lakecolor = "white",
bgcolor = "rgba(0,0,0,0)"
),
margin = list(l = 0, r = 0, b = 0, t = 70)
) |>
finish_plotly()
p_geo分级着色地图会强调面积较大的区域,而不只是数值。暴露人口不同的情况下应使用率而不是原始计数,并选择合理分级或连续色标;排名很重要时还应提供非地图对照。
静态 HTML 可以在浏览器中缩放、悬停、选择、播放动画、切换 trace 和执行 Crosstalk 联动,但发布后不能运行新的 R 计算。服务器端事件处理需要 Shiny 或其他应用运行环境。
关键组件包括:
source:为需要读取事件的 widget 命名;key:保留有意义的观察标识;event_data():在响应式上下文中读取单击、选择、悬停或布局事件;plotlyProxy():无需重建全部界面即可更新现有
widget。library(shiny)
library(plotly)
cars <- data.frame(model = rownames(mtcars), mtcars, row.names = NULL)
ui <- fluidPage(
plotlyOutput("cars_plot"),
verbatimTextOutput("clicked")
)
server <- function(input, output, session) {
output$cars_plot <- renderPlotly({
plot_ly(
cars,
x = ~wt,
y = ~mpg,
key = ~model,
source = "cars",
type = "scatter",
mode = "markers"
)
})
output$clicked <- renderPrint({
event_data("plotly_click", source = "cars")
})
}
shinyApp(ui, server)常见事件名包括
plotly_click、plotly_hover、plotly_selected、plotly_brushing、plotly_relayout
和 plotly_legendclick。较少见事件可按需使用
event_register() 注册。
在普通静态 Rmd 中调用 event_data() 并不会让发布页面具备
R 响应能力;它必须在 Shiny
响应式上下文中运行。静态页面需要联动时,应使用 Crosstalk
支持的浏览器端功能。
htmlwidgets::saveWidget() 可以脱离 R Markdown 保存
Plotly widget。其参数拼写为 selfcontained,而 R Markdown
YAML 使用 self_contained。
htmlwidgets::saveWidget(
p_first,
file = "car_scatter.html",
selfcontained = TRUE,
title = "互动汽车散点图"
)如果使用 selfcontained = FALSE,HTML 会依赖
car_scatter_files/
一类伴随目录;发布时必须一起上传并保持相对路径。
partial_bundle() 可以为单个 widget 用较小模块替换完整
Plotly.js:
small_widget <- partial_bundle(p_first, type = "auto", local = TRUE)
htmlwidgets::saveWidget(small_widget, "car_scatter_small.html", selfcontained = TRUE)不要在同一页混用互不兼容的 partial bundle:先加载的 Plotly.js 可能被后续 widget 复用,却缺少后者需要的 trace 模块。本指南同时包含 scatter、bar、violin、heatmap、surface、animation 和 geo,因此有意使用完整包。
对于适量数据,SVG 散点 trace 有很好的浏览器交互。达到数万点时,可使用
type = "scattergl" 或
toWebGL(),减少悬停内容,并先考虑抽样或分箱,而不是只增加硬件负担。
large_points <- data.frame(
x = rnorm(20000),
y = 0.65 * rnorm(20000) + rnorm(20000, sd = 0.55),
group = sample(c("A", "B"), 20000, replace = TRUE)
)
p_webgl <- plot_ly(
large_points,
x = ~x,
y = ~y,
color = ~group,
colors = c("#3d7085", "#a4422f"),
type = "scattergl",
mode = "markers",
marker = list(size = 4, opacity = 0.32),
hoverinfo = "skip"
) |>
plotly::layout(
title = list(text = "WebGL 让两万个点保持流畅"),
xaxis = c(list(title = "x"), axis_clean),
yaxis = c(list(title = "y"), axis_clean),
legend = list(title = list(text = "组"))
) |>
finish_plotly()
p_webgl| 症状 | 可能原因 | 检查或修复方法 |
|---|---|---|
| 找不到数据列 | 忘记公式语法 | 指定 data 后使用
x = ~column |
| 固定颜色产生奇怪标度 | 混淆了常量与映射 | 使用 I("#hex") 或嵌套 marker/line
列表 |
| 折线向后跳 | 行顺序错误 | 在每组内按 x 排序后再绘图 |
| 图例重复 | 多个 trace 表示相同语义 | 对部分 trace 设 showlegend = FALSE,或共用
legendgroup |
| 悬停信息过多 | 默认提示包含所有映射 | 使用 hovertemplate 或
ggplotly(..., tooltip = ...) |
| 数值悬停格式失效 | customdata 被转成字符 |
保持自定义数据矩阵为数值型 |
| 转换后的 ggplot 不同 | 并非每个 geom/主题细节都支持转换 | 检查 ggplotly(),必要时原生构建 |
| 动画对象互换身份 | 缺少稳定字符串 ID | 指定 ids = ~stable_id |
GitHub Pages 上 event_data() 没反应 |
静态 HTML 没有 R 服务器 | 放入 Shiny,或用 Crosstalk 做浏览器端联动 |
| 发布后 HTML 损坏 | 漏传 _files 依赖目录 |
使用自包含 HTML,或上传伴随目录 |
| 打包后后续 trace 消失 | 同页 partial bundle 不兼容 | 使用一个兼容包,或完整 Plotly.js |
| 浏览器交互缓慢 | SVG 标记或悬停内容过多 | 使用 scattergl、抽样、分箱或汇总 |
遇到复杂问题时,先检查图形结构:
cat("R:", R.version.string, "\n")
#> R: R version 4.6.1 (2026-06-24)
cat("plotly:", as.character(packageVersion("plotly")), "\n")
#> plotly: 4.12.1
cat("ggplot2:", as.character(packageVersion("ggplot2")), "\n")
#> ggplot2: 4.0.3
cat("htmlwidgets:", as.character(packageVersion("htmlwidgets")), "\n")
#> htmlwidgets: 1.6.4
cat("crosstalk:", as.character(packageVersion("crosstalk")), "\n")
#> crosstalk: 1.2.2
cat("knitr:", as.character(packageVersion("knitr")), "\n")
#> knitr: 1.51
cat("rmarkdown:", as.character(packageVersion("rmarkdown")), "\n")
#> rmarkdown: 2.31至此,你已经完成了从第一个 plot_ly() 散点图到 trace
叠加、ggplot 转换、精确悬停、组合图、联动选择、动画、地图、发布、Shiny
事件与 WebGL
性能优化的完整学习路径。最好的互动图,依然是在读者动手之前就已经把问题和比较任务讲清楚的图。