You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决R ggplot中多类别X轴标签显示及保存消失问题

问题描述

尝试用ggplot绘制包含多类别的柱状图,修复X轴标签时遇到困难。曾采用隐藏原生X轴标签、通过annotate叠加自定义标签的方法,但保存为jpeg格式时X轴标签会消失。怀疑是代码中隐藏X轴标签后,叠加的内容因绘图区域裁剪或保存逻辑问题无法被ggsave捕获,现寻求替代解决方案。

原代码如下:

dodge <- position_dodge(width = 0.9)
g1 <- ggplot(data = df, aes(x = interaction(gender, nexp), y = perc, fill = factor(positive))) +
  geom_bar(stat = "identity", position = position_dodge())  +
  coord_cartesian(ylim = c(0, 65)) +
  annotate("text", x = c(1.5, 3.5, 5.5, 7.5, 9.5), y = -10,
           label = rep(c("0", "1", "2", "3", "4"), 1)) +
  annotate("text", x= c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), y = - 15, 
           label = rep(c("Man", "Woman","Man", "Woman","Man", "Woman","Man", "Woman","Man", "Woman"))) +
  theme_classic() +
  theme(plot.margin = unit(c(1, 1, 4, 1), "lines"),
        axis.title.x = element_blank(),
        axis.text.x = element_blank()) +
  geom_text(aes(label=count), 
                position = position_dodge(width = 1), 
                vjust=-.5, size = 4) +
  labs(title = "Percentage of positive and negative outcomes by number of experiencial activity and gender") +
         theme(plot.title = element_text(hjust = .1),
               plot.title.position = "plot") +
  guides(fill = guide_legend(title.position="top", title ="Outcome") ) + 
  scale_fill_brewer(palette = "Set1", labels = c("Negative", "Positive")) +
  ylab("Percentage per number of experiences (%)")

g1

g2 <- ggplot_gtable(ggplot_build(g1))
g2$layout$clip[g2$layout$name == "panel"] <- "off"
grid.draw(g2)
ggsave("plot.jpeg", dpi = "retina")

使用的数据:

df <- structure(list(nexp = c(0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 
                             3, 3, 3, 4, 4, 4, 4), gender = structure(c(1L, 1L, 2L, 2L, 1L, 
                                                                        1L, 2L, 2L, 1L, 1L, 2L, 2L, 1L, 1L, 2L, 2L, 1L, 1L, 2L, 2L), levels = c("Man", 
                                                                                                                                                "Woman"), class = "factor"), positive = c(0, 1, 0, 1, 0, 1, 0, 
                                                                                                                                                                                          1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1), count = c(30L, 47L, 15L, 
                                                                                                                                                                                                                                            43L, 56L, 94L, 69L, 121L, 40L, 80L, 74L, 170L, 19L, 53L, 50L, 
                                                                                                                                                                                                                                            134L, 2L, 7L, 14L, 36L), total = c(135L, 135L, 135L, 135L, 340L, 
                                                                                                                                                                                                                                                                               340L, 340L, 340L, 364L, 364L, 364L, 364L, 256L, 256L, 256L, 256L, 
                                                                                                                                                                                                                                                                               59L, 59L, 59L, 59L), perc = c(22.2222222222222, 34.8148148148148, 
                                                                                                                                                                                                                                                                                                             11.1111111111111, 31.8518518518519, 16.4705882352941, 27.6470588235294, 
                                                                                                                                                                                                                                                                                                             20.2941176470588, 35.5882352941176, 10.989010989011, 21.978021978022, 
                                                                                                                                                                                                                                                                                                             20.3296703296703, 46.7032967032967, 7.421875, 20.703125, 19.53125, 
                                                                                                                                                                                                                                                                                                             52.34375, 3.38983050847458, 11.864406779661, 23.728813559322, 
                                                                                                                                                                                                                                                                                                             61.0169491525424)), row.names = c(NA, -20L), class = "data.frame")

替代解决方案

核心思路是抛弃手动annotate和绘图区域裁剪的方法,直接利用ggplot原生的轴标签格式化功能实现多层X轴效果,同时确保保存逻辑一致,避免标签丢失。

方案1:原生多层X轴标签

通过scale_x_discrete直接将分组标签拆分为两行,无需额外叠加文本:

library(ggplot2)

g <- ggplot(data = df, aes(x = interaction(gender, nexp), y = perc, fill = factor(positive))) +
  geom_bar(stat = "identity", position = position_dodge(width = 0.9)) +
  coord_cartesian(ylim = c(0, 65)) +
  # 自定义X轴标签:拆分交互分组为「体验次数+性别」两行显示
  scale_x_discrete(
    labels = function(x) {
      split_vals <- strsplit(as.character(x), "\\.")
      sapply(split_vals, function(v) paste(v[2], v[1], sep = "\n"))
    }
  ) +
  geom_text(aes(label = count), 
            position = position_dodge(width = 0.9), 
            vjust = -.5, size = 4) +
  labs(
    title = "不同体验活动次数与性别下,正负结果的占比",
    x = "体验次数 / 性别",
    y = "占比(%)",
    fill = "结果类型"
  ) +
  scale_fill_brewer(palette = "Set1", labels = c("负面", "正面")) +
  theme_classic() +
  theme(
    plot.title = element_text(hjust = 0.1, plot.title.position = "plot"),
    # 调整X轴标签行距,确保两行内容清晰
    axis.text.x = element_text(lineheight = 1.2),
    # 底部边距适配两行标签,避免截断
    plot.margin = unit(c(1, 1, 2, 1), "lines"),
    legend.title.position = "top"
  )

# 直接保存ggplot对象,无需额外处理gtable
ggsave("plot.jpeg", plot = g, dpi = "retina", width = 10, height = 6)

方案2:分面分组(更直观的分隔效果)

如果需要更清晰的分组边界,用facet_wrap按体验次数分组,自动生成顶部标签:

g_facet <- ggplot(data = df, aes(x = gender, y = perc, fill = factor(positive))) +
  geom_bar(stat = "identity", position = position_dodge(width = 0.9)) +
  coord_cartesian(ylim = c(0, 65)) +
  geom_text(aes(label = count), 
            position = position_dodge(width = 0.9), 
            vjust = -.5, size = 4) +
  # 按体验次数分面,横向排列
  facet_wrap(~nexp, nrow = 1, labeller = labeller(nexp = function(x) paste("体验次数:", x))) +
  labs(
    title = "不同体验活动次数与性别下,正负结果的占比",
    x = "性别",
    y = "占比(%)",
    fill = "结果类型"
  ) +
  scale_fill_brewer(palette = "Set1", labels = c("负面", "正面")) +
  theme_classic() +
  theme(
    plot.title = element_text(hjust = 0.1, plot.title.position = "plot"),
    # 移除分面背景,保持风格统一
    strip.background = element_blank(),
    plot.margin = unit(c(1, 1, 1, 1), "lines"),
    legend.title.position = "top"
  )

ggsave("plot_facet.jpeg", plot = g_facet, dpi = "retina", width = 12, height = 6)

方案优势

  1. 解决保存问题:原代码中修改g2后仅用grid.draw显示,但ggsave默认保存原始g1,导致标签丢失。改进方案直接操作ggplot对象,保存逻辑一致。
  2. 无需手动调整裁剪:原生轴标签自动适配绘图区域,不用设置clip = "off"。
  3. 可维护性更高:标签逻辑与数据绑定,无需手动计算坐标位置。

内容的提问来源于stack exchange,提问作者Victor Shin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 02:45:55