ggplot2 如何为离散样本按来源分组,并只显示一次组标签?
可以。要在同一条 x 轴上排列所有样本,并让每个来源只显示一次标签,可以先为样本分配数值位置,再把各组中心设为主刻度,在组间边界画竖线。如果可以接受每个来源一个面板,分面更简单。
原代码的问题在哪里?
你的 x 轴映射的是 Sample:
geom_point(aes(x = Sample), size = 2)
因此,scale_x_discrete() 的 breaks 应当对应 Sample 中的取值。传入 Mill 不会让 ggplot2 自动计算各来源的样本范围,也不会合并重复标签。
labels 负责显示刻度文字,不负责对样本分组。即使把多个样本刻度改成相同的来源名称,它们仍然是多个独立刻度。
直接把字符型 Sample 交给连续坐标轴也不会自动转换位置,需要先为每个样本指定数值位置。
在一条 x 轴上显示来源标签和组间分隔线
下面的代码使用你提供结构的 pH.Kond.Alk.1,假设 Mill 和 Sample 没有缺失值,且 pH 为数值型。如果实际绘图使用的是经过筛选的 pH.1,应改用该数据,并确认其中保留了这些列。
library(ggplot2)
d <- pH.Kond.Alk.1
# 保留已有的来源顺序,删除未使用的水平
# 如果 Mill 原来是字符型,factor() 会建立因子水平
d$Mill <- droplevels(factor(d$Mill))
# 每个来源中的每个样本只分配一个位置
key <- unique(d[c("Mill", "Sample")])
key <- key[order(key$Mill), , drop = FALSE]
key$x_pos <- seq_len(nrow(key))
# 将位置合并回所有观测记录
plot_data <- merge(
d,
key,
by = c("Mill", "Sample"),
sort = FALSE
)
# 计算来源中心,以及相邻来源之间的边界
positions <- split(key$x_pos, key$Mill, drop = TRUE)
centers <- vapply(
positions,
function(x) mean(range(x)),
numeric(1)
)
right_edges <- vapply(positions, max, numeric(1))
boundaries <- head(right_edges, -1) + 0.5
pH.plot <- ggplot(plot_data, aes(x = x_pos, y = pH)) +
geom_vline(
xintercept = boundaries,
colour = "grey50",
linetype = "dashed"
) +
geom_point(size = 2) +
scale_x_continuous(
breaks = unname(centers),
labels = names(centers),
minor_breaks = key$x_pos
) +
labs(x = NULL, y = NULL) +
theme(
legend.title = element_blank(),
axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.5),
panel.grid.major.x = element_blank(),
panel.grid.minor.x = element_line(colour = "grey90")
)
pH.plot
代码中的三个位置变量分别表示:
x_pos是每个样本的实际绘图位置,同一来源的样本会连续排列。centers是各来源覆盖区间的中心,用于显示一次来源名称。boundaries位于相邻来源之间,用于绘制分隔线。
minor_breaks 指定样本位置处的次网格线位置,是否显示取决于主题设置。它本身不会在轴线上绘制次刻度短线。这里的分隔线由 geom_vline() 绘制,不依赖次刻度功能。
位置表使用 Mill 和 Sample 共同匹配,因此不同来源即使有相同的样本名称,也不会被误合并为同一个位置。同一来源、同一样本的两条观测会落在相同的 x 位置,各自保留其 pH 值。
如果接受分面,可以减少位置计算
如果来源标签放在面板顶部也符合需求,可以直接按 Mill 分面:
library(ggplot2)
pH.plot <- ggplot(
pH.Kond.Alk.1,
aes(x = Sample, y = pH)
) +
geom_point(size = 2) +
facet_grid(
. ~ Mill,
scales = "free_x",
space = "free_x",
drop = TRUE
) +
labs(x = NULL, y = NULL) +
theme(
legend.title = element_blank(),
axis.text.x = element_blank(),
axis.ticks.x = element_blank()
)
pH.plot
每个来源会有一个面板和一个来源标签。scales = "free_x" 让各面板使用自己的样本集合,space = "free_x" 让面板宽度随离散 x 轴范围调整。各面板仍共用 y 轴尺度,便于比较 pH。
这里隐藏了样本名称。如果还需要显示它们,可以把 axis.text.x 改回 element_text(angle = 90, hjust = 1, vjust = 0.5),并移除隐藏 x 轴刻度的设置。
使用时需要确认什么?
来源顺序由 Mill 的因子水平决定。如果需要业务指定的顺序,应先设置 factor(..., levels = ...)。第一种方案中,同一来源内部的样本默认按它们在输入数据中首次出现的顺序排列。如果要按 Halfyear 或其他字段排序,应先明确排序规则,再建立位置表。
颜色和形状可以继续使用原有映射,两种布局都不需要占用它们。
原代码使用 pH.1,数据结构却来自 pH.Kond.Alk.1,两者是否经过不同筛选并不明确。绘图时应从同一份数据建立位置、刻度和图层,并在 aes() 中直接写列名,例如 aes(y = pH),避免使用外部数据框列向量导致对应关系错位。
目前只有 str() 输出,没有完整数据和图片,因此无法确认原图重复标签的具体表现,以上代码也未经原数据验证。
备注:内容仅供参考。
