# 6  交互图形

## 6.1 基础元素

### 6.1.1 图层

plotly 包封装了许多图层函数，可以绘制各种各样的统计图形，见下 表格 6.1

# https://plotly.com/r/reference/scatter/
plotly::plot_ly(data = quakes, x = ~long, y = ~lat) |>
plotly::add_markers()

plotly::plot_ly(data = quakes, x = ~long, y = ~lat) |>
plotly::add_trace(type = "scatter", mode = "markers")

plotly 包的函数 plot_ly() 又与 ggplot2 包中函数 qplot() 类似，可以将大部分设置塞进去。

plotly::plot_ly(
data = quakes, x = ~long, y = ~lat,
type = "scatter", mode = "markers"
)

### 6.1.2 配色

plotly::plot_ly(data = quakes, x = ~long, y = ~lat) |>
plotly::add_markers(color = ~mag)

### 6.1.3 刻度

plotly::plot_ly(data = quakes, x = ~long, y = ~lat) |>
plotly::layout(
xaxis = list(title = "经度", ticksuffix = 'E'),
yaxis = list(title = "纬度", ticksuffix = 'S')
)

### 6.1.4 标签

plotly::plot_ly(
data = quakes, x = ~long, y = ~lat,
marker = list(
color = ~mag,
colorscale = "Viridis",
colorbar = list(title = list(text = "震级"))
)
) |>
plotly::layout(
xaxis = list(title = "经度"),
yaxis = list(title = "纬度"),
title = "斐济及其周边地区的地震活动"
)

### 6.1.5 主题

plotly 内置了一些主题风格

plotly::plot_ly(
data = quakes, x = ~long, y = ~lat,
marker = list(
color = ~mag,
colorscale = "Viridis",
colorbar = list(title = list(text = "震级"))
)
) |>
plotly::layout(
xaxis = list(title = "经度"),
yaxis = list(title = "纬度"),
title = "斐济及其周边地区的地震活动"
)

## 6.2 常用图形

### 6.2.1 散点图

plotly 包支持绘制许多常见的散点图，从直角坐标系 scatter 到极坐标系 scatterpolar 和地理坐标系 scattergeo，从二维平面 scatter 到三维空间 scatter3d，借助 WebGL 可以渲染大规模的数据点 scattergl

plotly::plot_ly(
data = quakes, x = ~long, y = ~lat,
type = "scatter", mode = "markers"
) |>
plotly::layout(
xaxis = list(title = "经度"),
yaxis = list(title = "纬度")
)

### 6.2.2 柱形图

# https://plotly.com/r/reference/bar/
plotly::plot_ly(
data = trunk_year, x = ~year, y = ~revision, type = "bar"
) |>
plotly::layout(
xaxis = list(title = "年份"),
yaxis = list(title = "代码提交量")
)

### 6.2.3 曲线图

plotly::plot_ly(
data = trunk_year, x = ~year, y = ~revision, type = "scatter",
mode = "markers+lines", line = list(shape = "spline")
) |>
plotly::layout(
xaxis = list(title = "年份"),
yaxis = list(title = "代码提交量")
)

### 6.2.4 直方图

# https://plotly.com/r/reference/histogram/
plotly::plot_ly(quakes, x = ~mag, type = "histogram") |>
plotly::layout(
xaxis = list(title = "震级"),
yaxis = list(title = "次数")
)

plotly::plot_ly(
data = quakes, x = ~mag, type = "histogram",
histnorm = "probability",
marker = list(
color = "lightblue",
line = list(color = "white", width = 2)
)
) |>
plotly::layout(
xaxis = list(title = "震级"),
yaxis = list(title = "频率")
)

histnorm = "probability" 意味着纵轴表示频率，即每个窗宽下地震次数占总地震次数的比例。地震常常发生在地下，不同的深度对应着不同的地质构造、不同的地震成因，下 图 6.11 展示海平面下不同深度的地震震级分布。

quakes$depth_bin <- cut(quakes$depth, breaks = 150 * 0:5)
plotly::plot_ly(quakes,
x = ~mag, colors = "viridis",
color = ~depth_bin, type = "histogram"
) |>
plotly::layout(
xaxis = list(title = "震级"),
yaxis = list(title = "次数")
)

### 6.2.5 箱线图

plotly::plot_ly(quakes,
x = ~depth_bin, y = ~mag, colors = "viridis",
color = ~depth_bin, type = "box"
) |>
plotly::layout(
xaxis = list(title = "深度"),
yaxis = list(title = "震级")
)
plotly::plot_ly(quakes,
x = ~depth_bin, y = ~mag, split = ~depth_bin,
type = "violin", color = ~depth_bin, colors = "viridis",
box = list(visible = TRUE),
meanline = list(visible = TRUE)
) |>
plotly::layout(
xaxis = list(title = "深度"),
yaxis = list(title = "震级")
)

### 6.2.6 热力图

plotly 整合了开源的 Mapbox GL JS，可以使用 Mapbox 提供的瓦片地图服务（Mapbox Tile Maps），对空间点数据做核密度估计，展示热力分布，如 图 6.14 所示。图左上角为所罗门群岛（Solomon Islands）、瓦努阿图（Vanuatu）和新喀里多尼亚（New Caledonia），图下方为新西兰北部的威灵顿（Wellington）和奥克兰（Auckland），图中部为斐济（Fiji）。

plotly::plot_ly(
data = quakes, lat = ~lat, lon = ~long, radius = 10,
type = "densitymapbox", coloraxis = "coloraxis"
) |>
plotly::layout(
mapbox = list(
style = "stamen-terrain", zoom = 3,
center = list(lon = 180, lat = -25)
),
coloraxis = list(colorscale = "Viridis")
)

### 6.2.7 面量图

# https://plotly.com/r/reference/choropleth/
dat <- data.frame(state.x77,
stats = rownames(state.x77),
stats_abbr = state.abb
)
# 绘制图形
plotly::plot_ly(
data = dat,
type = "choropleth",
locations = ~stats_abbr,
locationmode = "USA-states",
colorscale = "Viridis",
colorbar = list(title = list(text = "人均收入")),
z = ~Income
) |>
plotly::layout(
geo = list(scope = "usa"),
title = "1974年美国各州的人均收入"
)

### 6.2.8 动态图

# https://plotly.com/r/animations/
trunk_year_author <- aggregate(data = svn_trunk_log, revision ~ year + author, FUN = length)
# https://plotly.com/r/cumulative-animations/
accumulate_by <- function(dat, var) {
var <- lazyeval::f_eval(f = var, data = dat)
lvls <- plotly:::getLevels(var)
dats <- lapply(seq_along(lvls), function(x) {
cbind(dat[var %in% lvls[seq(1, x)], ], frame = lvls[[x]])
})
dplyr::bind_rows(dats)
}

subset(trunk_year_author, year >= 1999 & author %in% c("ripley", "maechler")) |>
accumulate_by(~year) |>
plotly::plot_ly(
x = ~year, y = ~revision, split = ~author,
frame = ~frame, type = "scatter", mode = "lines",
line = list(simplyfy = F)
) |>
plotly::layout(
xaxis = list(title = "年份"),
yaxis = list(title = "代码提交量")
) |>
plotly::animation_opts(
frame = 100, transition = 0, redraw = FALSE
) |>
plotly::animation_button(
visible = TRUE, # 显示播放按钮
label = "播放", # 按钮文本
font = list(color = "gray")# 文本颜色
) |>
plotly::animation_slider(
currentvalue = list(
prefix = "年份 ",
xanchor = "right",
font = list(color = "gray", size = 30)
)
)

lazyeval 的非标准计算采用 Base R 实现，目前，已经可以被 rlang 替代。

## 6.3 常用技巧

### 6.3.1 数学公式

\begin{aligned} & f(x;\mu,\sigma^2) = \frac{1}{\sqrt{2\pi}\sigma}\exp\{-\frac{(x -\mu)^2}{2\sigma^2}\} \end{aligned}

x <- seq(from = -4, to = 8, length.out = 193)
y1 <- dnorm(x, mean = 3, sd = 1)
y2 <- dnorm(x, mean = 2, sd = 1.5)

plotly::plot_ly(
x = x, y = y1, type = "scatter", mode = "lines",
fill = "tozeroy", fillcolor = "rgba(0, 204, 102, 0.2)",
text = ~ paste0(
"x：", x, "<br>",
"y：", round(y1, 3), "<br>"
),
hoverinfo = "text",
name = plotly::TeX("\\mathcal{N}(3,1^2)"),
line = list(shape = "spline", color = "#009B95")
) |>
x = x, y = y2, type = "scatter", mode = "lines",
fill = "tozeroy", fillcolor = "rgba(51, 102, 204, 0.2)",
text = ~ paste0(
"x：", x, "<br>",
"y：", round(y2, 3), "<br>"
),
hoverinfo = "text",
name = plotly::TeX("\\mathcal{N}(2, 1.5^2)"),
line = list(shape = "spline", color = "#403173")
) |>
plotly::layout(
xaxis = list(showgrid = F, title = plotly::TeX("x")),
yaxis = list(showgrid = F, title = plotly::TeX("f(x)")),
legend = list(x = 0.8, y = 1, orientation = "v")
) |>
plotly::config(mathjax = "cdn", displayModeBar = FALSE)

### 6.3.2 动静转化

library(ggplot2)
p <- ggplot(data = quakes, aes(x = long, y = lat)) +
geom_point()
p

ggplot2 包绘制的散点图转化为交互式的散点图，只需调用 plotly 包的函数 ggplotly()

plotly::ggplotly(p)

plotly::ggplotly(p) |>
plotly::config(staticPlot = TRUE)

plotly::ggplotly(p, dynamicTicks = "y") |>
plotly::style(hoveron = "points", hoverinfo = "x+y+text",
hoverlabel = list(bgcolor = "white"))

orca (Open-source Report Creator App) 软件针对 plotly.js 库渲染的图形具有很强的导出功能，安装 orca 后，plotly::orca() 函数可以将基于 htmlwidgetsplotly 图形对象导出为 PNG、PDF 和 SVG 等格式的高质量静态图片。

# orca
plotly::orca(p, "plotly-quakes.svg")
# kaleido
plotly::save_image(p, "plotly-quakes.svg")

### 6.3.3 坐标系统

quakes 是一个包含空间位置的数据集，plotly 的 scattergeo 图层 针对空间数据提供多边形矢量边界地图数据，支持设定坐标参考系。下 图 6.19 增加了地震震级维度，在空间坐标参考系下绘制散点。

plotly::plot_ly(
data = quakes,
lon = ~long, lat = ~lat,
type = "scattergeo", mode = "markers",
text = ~ paste0(
"站点：", stations, "<br>",
"震级：", mag
),
marker = list(
color = ~mag, colorscale = "Viridis",
size = 10, opacity = 0.8,
line = list(color = "white", width = 1)
)
) |>
plotly::layout(geo = list(
showland = TRUE,
landcolor = plotly::toRGB("gray95"),
countrycolor = plotly::toRGB("gray85"),
subunitcolor = plotly::toRGB("gray85"),
countrywidth = 0.5,
subunitwidth = 0.5,
lonaxis = list(
showgrid = TRUE,
gridwidth = 0.5,
range = c(160, 190),
dtick = 5
),
lataxis = list(
showgrid = TRUE,
gridwidth = 0.5,
range = c(-40, -10),
dtick = 5
)
))

### 6.3.4 添加水印

plotly::plot_ly(quakes,
x = ~long, y = ~lat, color = ~mag,
type = "scatter", mode = "markers"
) |>
plotly::config(staticPlot = TRUE) |>
plotly::layout(
images = list( # 水印图片
source = "https://images.plot.ly/language-icons/api-home/r-logo.png",
xref = "paper", # 页面参考
yref = "paper",
x = 0.90, # 横坐标
y = 0.20, # 纵坐标
sizex = 0.2, # 长度
sizey = 0.2, # 宽度
opacity = 0.5 # 透明度
)
)

### 6.3.5 多图布局

p1 <- plotly::plot_ly(
data = trunk_year, x = ~year, y = ~revision, type = "bar"
) |>
plotly::layout(
xaxis = list(title = "年份"),
yaxis = list(title = "代码提交量")
)

p2 <- plotly::plot_ly(
data = trunk_year, x = ~year, y = ~revision, type = "scatter",
mode = "markers+lines", line = list(shape = "spline")
) |>
plotly::layout(
xaxis = list(title = "年份"),
yaxis = list(title = "代码提交量")
)

htmltools::tagList(p1, p2)

plotly 包提供的函数 subplot() 专门用于布局排列，下图的上下子图共享 x 轴。

plotly::subplot(plotly::style(p1, showlegend = FALSE),
plotly::style(p2, showlegend = FALSE),
nrows = 2, margin = 0.05, shareX = TRUE, titleY = TRUE)

p11 <- plotly::subplot(plotly::style(p1, showlegend = FALSE),
plotly::style(p2, showlegend = FALSE),
nrows = 1, margin = 0.05, shareY = TRUE, titleX = TRUE
)

plotly::subplot(p11,
plotly::style(p2, showlegend = FALSE),
nrows = 2, margin = 0.05, shareY = FALSE, titleX = FALSE
)

### 6.3.6 图表联动

crosstalk 包可将 plotly 包绘制的图形和 DT 包制作的表格联动起来。plotly 绘制交互图形，在图形上用套索工具筛选出来的数据显示在表格中。

library(crosstalk)
# quakes 数据变成可共享的
quakes_sd <- SharedData\$new(quakes)
# 绘制交互图形
p <- plotly::plot_ly(quakes_sd, x = ~long, y = ~lat) |>
bscols(list(p, d))