大概没有一个做数据分析的人能绕过 ggplot2。如果你在用 R 做图柱状图基本上是你最早接触、也最常画的图形。但越是常用的东西越容易在细节上踩坑同样的数据有人画出来是计数柱状图有人画出来是数值柱状图有人觉得 ggplot2 的柱状图排序很反直觉还有人第一次遇到中文标签乱码时直接抓狂。这篇我把自己用 ggplot2 画柱状图的思路整理了一遍从最基础的原理讲起配上可以直接复制的代码把常见坑也尽量提前说出来。这篇文章适合刚接触 R 可视化的同学也适合已经画过不少图、但想系统整理柱状图用法的朋友。你可以把它当成一份工具笔记用到哪个知识点直接翻到对应章节抄代码。柱状图本身并不复杂但 ggplot2 的图层语法决定了你理解得越透后面画折线图、散点图、地图才会越顺手。所以我不会只丢一堆模板代码而是把每个参数背后的逻辑讲清楚这样你遇到不完全一样的场景时也能自己调整。全文我都基于 R 4.x 和 ggplot2 3.5 环境实测过代码可以直接运行。1. 画柱状图前先搞懂 ggplot2 在做什么1.1 ggplot2 的图层语法数据、映射、几何对象ggplot2 的核心思想是把数据和图形属性分开管理。一条完整的绘图命令一般长这样ggplot(data df, aes(x 分类, y 数值)) geom_bar(stat identity)ggplot()负责声明数据框和映射关系aes()告诉系统“分类”对应 x 轴“数值”对应 y 轴。geom_bar()则负责画柱子。很多人一开始不理解为什么有时候geom_bar()不需要写y也能画图有时候必须写y这就是后面要说的统计变换问题。刚接触的时候把aes()里写的变量想象成“图纸上的通道”x、y、fill、color、group 都是通道。geom_*()的作用则是根据这些通道渲染出实际图形。这个思路贯穿 ggplot2 所有图形柱状图只是第一个例子。1.2 geom_bar 和 geom_col两个函数到底该选谁这两个函数画出来的图形几乎一样但默认行为完全不同新手往往在这里栽跟头。geom_bar()默认对 x 变量做计数统计也就是自动统计每个类别出现了多少行不需要你提供 y 值。geom_col()直接把aes()里的y当作柱子高度不会做任何统计适合“一行数据就是一个柱子”的现成汇总表。我个人的习惯是如果是原始明细数据想快速看各分类的出现次数用geom_bar()如果我已经用dplyr::count()或group_by() summarise()汇总好了数值就用geom_col()。你可以把geom_bar(stat identity)看作geom_col()的等价写法只是我更喜欢直接写geom_col()语义更清楚。举个例子df - data.frame( 水果 c(苹果, 香蕉, 苹果, 橘子, 苹果, 香蕉), 数量 c(3, 5, 2, 4, 6, 1) ) # 计数柱状图统计每种水果出现几行 ggplot(df, aes(x 水果)) geom_bar() # 数值柱状图一行一个柱子使用数量作为高度 ggplot(df, aes(x 水果, y 数量)) geom_col()如果你把geom_col()和geom_bar()混用很容易出现“明明数据没问题图却不对”的情况。解决方法就是认清当前数据是明细数据还是汇总数据。1.3 position 参数堆叠、并列、填充到底怎么选当柱状图存在第二维分组时position参数决定柱子怎么摆放。常见的有三个值position效果适用场景position_stack()堆叠多组柱子上下叠放总高度是各组之和看总量和各部分占比position_dodge()并列多组柱子并排显示组间数值对比position_fill()百分比堆叠每根柱子总高度一致为 100%看各组内部比例变化这三个参数在geom_bar()和geom_col()里都可以用。position dodge是简写默认的position_dodge(width NULL)会自动计算但如果你在分组柱状图上加了误差线或标签就需要显式设置宽度比如position_dodge(width 0.9)。这个问题后面会详细讲。很多初学者把“堆叠”和“百分比堆叠”搞混。堆叠柱状图的每个柱子高度代表总数不同部分的高度代表绝对数值百分比堆叠的柱子高度一样看的是相对比例。选哪一个取决于你想强调绝对数还是占比。2. 数据准备用什么结构的数据画柱状图最省心2.1 什么样的数据适合直接画柱状图画柱状图最理想的数据结构是“长表”一列是分类变量一列是数值变量。分类变量可以是产品名、地区、季度、实验组别数值变量可以是销量、评分、频率、金额。如果是明细数据比如每笔订单一行那么往往需要用geom_bar()对某个分类计数如果已经是对比汇总表比如每个月的平均销量那就直接用geom_col()。我见过不少同学把数据整理成 Excel 的“宽表”每一列是一个季度每一行是一个产品。这种结构方便人眼看但 ggplot2 画分组柱状图时通常需要先转换成长表。别嫌麻烦这一步做对了后面的绘图代码会非常简洁。2.2 从宽表变长表tidyr 的 pivot_longer假设你手上的数据是这样的wide_data - data.frame( 产品 c(A, B, C), Q1 c(120, 200, 150), Q2 c(180, 160, 210), Q3 c(140, 220, 190) )这种数据要画“产品 x 季度”的分组柱状图需要先把季度列合并成一列library(tidyr) long_data - wide_data %% pivot_longer(cols Q1:Q3, names_to 季度, values_to 销量) head(long_data)转换后每一行代表“某个产品在某个季度的销量”这就是 ggplot2 最喜欢的结构。cols Q1:Q3指定哪些列需要转换names_to是原来的列名变成的变量名values_to是原来的数值变成的变量名。这个函数几乎每天都会用到。2.3 构造一组示例数据后面都用它为了让文章里的代码可以完整复现我造了一份模拟数据不涉及任何真实业务只是用来演示柱状图的各种画法library(ggplot2) library(dplyr) library(tidyr) set.seed(1234) sales_data - data.frame( 产品 rep(c(产品A, 产品B, 产品C, 产品D), each 4), 季度 rep(c(Q1, Q2, Q3, Q4), times 4), 销售额 round(runif(16, 80, 300), 0), 占比 round(runif(16, 0.3, 0.9), 2) ) head(sales_data)这份数据共 16 行每个产品有 4 个季度的销售额。后面所有图都基于这个sales_data。你也可以换成自己的数据只要保证有“分类数值”两列就够。2.4 很容易被忽略的 factor 顺序问题ggplot2 画柱状图时x 轴分类的顺序默认按照字母顺序或数据框中的原始顺序。如果你发现柱子的排列不符合预期大部分原因是没有把 x 轴变量转成因子并指定顺序。sales_data$季度 - factor(sales_data$季度, levels c(Q1, Q2, Q3, Q4))一旦设定了levelsggplot2 就会严格按照这个顺序画柱子。文字型的季度如果不做这一步可能会被排成 Q1、Q2、Q3、Q4 还好但如果是“高、中、低”这种自定义顺序不转 factor 就一定会乱。所有涉及排序的需求请第一时间想到factor()。3. 基础柱状图一步步画出能用的图3.1 最简单的计数柱状图先看一个典型的场景你手上有一份订单明细想知道每个产品被下单了多少次。# 假设这是我的原始订单数据 orders - data.frame( 订单号 1:12, 产品 c(产品A, 产品B, 产品A, 产品C, 产品A, 产品B, 产品C, 产品D, 产品B, 产品A, 产品C, 产品D) ) ggplot(orders, aes(x 产品)) geom_bar(fill #4C72B0)这里geom_bar()默认统计每个产品出现的行数所以不需要y。柱子的高度表示频数。如果你想改变柱子颜色用fill如果只想改变边框颜色则用color。要注意fill和color是两个不同通道前者填充内部后者画边框。很多刚入门的人问“为什么我改了 fill 却看不到效果”多半是因为写成了aes(fill 产品)这种映射方式而不是直接传参数。直接传参写在geom_bar(fill red)里即可如果写在aes(fill red)里会被当成一个新的离散分组变量。3.2 用现成的数值画柱状图geom_col如果数据已经汇总好了比如每个产品的总销售额total_sales - sales_data %% group_by(产品) %% summarise(总销售额 sum(销售额)) ggplot(total_sales, aes(x 产品, y 总销售额)) geom_col(fill #55A868)geom_col()需要手动指定y。如果你把这段代码中的geom_col()换成geom_bar()会得到错误或意想不到的结果因为geom_bar()默认会对“产品”计数而不是画销售额。这类问题在论坛里反复出现建议直接记住geom_bar()无 ygeom_col()必须有 y。3.3 填充颜色、透明度、边框让图更耐看默认的灰色柱子虽然不出错但在实际项目里根本不够用。调整几个参数就能瞬间改善观感ggplot(total_sales, aes(x 产品, y 总销售额)) geom_col(fill #3B9AB2, color white, linewidth 1.2, alpha 0.85)fill柱子内部颜色。color柱子边框颜色。linewidth边框线宽。旧版本中叫sizeggplot2 3.4 之后推荐使用linewidth如果你还在用旧参数会看到提示但功能还在。alpha透明度0 到 1 之间。当柱子重叠时可以透出底下的内容平时也用得上。我经常看到有人为了“好看”给每根柱子用完全不同的颜色比如aes(fill 产品)。这样做一个很直接的问题柱子的颜色代表了一个类别但如果这个图本身就只是展示不同产品的销售额完全可以用同一颜色把注意力集中在高度差异上而不是颜色差异。颜色分组要留给真正需要传达“分组”的场景。3.4 柱宽和间距怎么调width 参数实际画图时柱子太宽会显得笨重太窄又显得稀疏。width参数直接控制柱子占分类区间的比例默认是 0.9ggplot(total_sales, aes(x 产品, y 总销售额)) geom_col(width 0.6, fill #E69F00)width 0.6表示柱子宽度占 x 轴上该分类可用宽度的 60%剩下 40% 是间隔。如果只有一个分类这个比例就是整个绘图区域的宽度比例。想调间距直接改这个值就行。注意width不是像素宽度而是相对比例这点和很多 Excel 用户的理解不太一样。4. 分组、堆叠、百分比、水平柱状图的常见进阶画法4.1 分组柱状图不同季度并排对比当你想同时比较“产品”和“季度”两个维度时分组柱状图是最直接的选择。这里用position_dodge()ggplot(sales_data, aes(x 产品, y 销售额, fill 季度)) geom_col(position position_dodge(width 0.8), width 0.7) scale_fill_manual(values c(#E41A1C, #377EB8, #4DAF4A, #984EA3))这段代码里有一点容易踩坑position_dodge(width 0.8)控制的是多个柱子在同一个分类内的“错开幅度”geom_col(width 0.7)控制的是柱子本身的宽度。如果 dodge 宽度小于柱子宽度柱子会重叠如果 dodge 宽度太大柱子之间会分离得很远。通常我在资料里看到的稳妥组合是width 0.7配position_dodge(width 0.8)或width 0.8配position_dodge(width 0.9)这样既不会重叠也不会间隔太远。scale_fill_manual(values ...)的作用是手动指定填充颜色。ggplot2 默认的颜色方案虽然能用但在同一个图里出现超过四五个类别时默认色的辨识度并不理想。我习惯在项目的开始阶段就把常用颜色统一写成常量方便复用。4.2 堆叠柱状图看清总量和组成部分堆叠柱状图适合展示“每个分类的总量以及总量内部的结构”。比如我想看每个产品全年销售额里四个季度分别贡献了多少ggplot(sales_data, aes(x 产品, y 销售额, fill 季度)) geom_col(position position_stack()) scale_fill_manual(values c(#E41A1C, #377EB8, #4DAF4A, #984EA3))这里不用width也行默认每个柱子占满分类区间。position_stack()会按照aes(fill)中因子水平的顺序从下往上堆叠。如果你希望某个季度在最下面需要提前调整季度的因子水平顺序。堆叠柱状图有一个常见陷阱如果不同产品的总销售额差距很大那么较小的产品内部结构很难看清楚。比如产品 A 全年销售额 1200产品 C 只有 300那么 C 的四个季度堆叠后就非常矮。这时候可以考虑用分面facet_wrap(~产品)把每个产品单独画一个小图或者换成百分比堆叠图。4.3 百分比堆叠柱状图看结构比例百分比堆叠用position_fill()它会把每根柱子的总高度拉伸到相同高度然后展示各组成部分的比例。这个图特别适合看“不同产品在四个季度的销售结构是否有差异”。ggplot(sales_data, aes(x 产品, y 销售额, fill 季度)) geom_col(position position_fill()) scale_y_continuous(labels scales::percent_format()) scale_fill_manual(values c(#E41A1C, #377EB8, #4DAF4A, #984EA3)) labs(y 季度销售额占比)scale_y_continuous(labels scales::percent_format())把 y 轴刻度从 0~1 显示成 0%~100%需要先安装并加载scales包或用library(scales)。如果你不想额外加载包也可以把labels写成function(x) paste0(x * 100, %)但我更推荐直接用scales因为后面还有很多格式化函数要用。这种图不适合比较具体数值大小只适合比较比例。如果观众需要知道具体销售额建议在旁边同时提供原始数值或者把数字标签加到图上。4.4 水平柱状图coord_flip 让标签更好读当分类名称很长或者分类数量很多时垂直柱子上的文字很容易重叠。最简单的方式是把图翻转 90 度让柱子横着放ggplot(total_sales, aes(x 产品, y 总销售额)) geom_col(fill #56B4E9) coord_flip()coord_flip()会交换 x 和 y 轴。翻转之后原来的 x 轴分类变成了 y 轴并且默认从下往上排列。如果你的分类顺序想反过来需要在数据框里把因子水平反过来设置比如total_sales$产品 - factor(total_sales$产品, levels c(产品D, 产品C, 产品B, 产品A))如果不设因子顺序coord_flip()后的柱子顺序可能不符合预期。这里再次体现了 factor 的重要性。另一种方式是使用geom_col()aes(y 产品, x 总销售额)直接交换映射效果类似但控制方式略有不同。我个人的习惯是保持原来的 x/y 映射最后加coord_flip()这样更容易维护。4.5 在柱子上加数值标签geom_text 的正确姿势柱子上的数字标签几乎是每次汇报都要加的。最简单的做法ggplot(total_sales, aes(x 产品, y 总销售额)) geom_col(fill #CC79A7) geom_text(aes(y 总销售额, label 总销售额), vjust -0.5, size 4)vjust -0.5表示文字放在柱顶上方偏外。如果柱顶太靠近绘图区边界文字可能被裁掉此时可以用expand_limits(y max(total_sales$总销售额) * 1.1)或调整scale_y_continuous(expand expansion(mult c(0, 0.1)))来留白。分组柱状图加标签需要额外注意要让标签跟着柱子走必须在geom_text()里设定与geom_col()相同的position_dodge()参数否则全部标签会落在同一个 x 位置重叠在一起。ggplot(sales_data, aes(x 产品, y 销售额, fill 季度)) geom_col(position position_dodge(width 0.8), width 0.7) geom_text( aes(label 销售额), position position_dodge(width 0.8), size 3, vjust -0.3 )堆叠柱状图的标签要放在每个分段的中间通常用position_stack(vjust 0.5)ggplot(sales_data, aes(x 产品, y 销售额, fill 季度)) geom_col(position position_stack()) geom_text( aes(label 销售额), position position_stack(vjust 0.5), size 3 )vjust 0.5在堆叠场景下表示文字位于每个堆叠段的垂直中央。这里的关键是position_stack(vjust 0.5)不是给你随便试的数字而是 ggplot2 内置的位置调整方式。如果忘记设置position标签会默认堆在当前 x 位置的底部根本对不上柱子。4.6 排序让柱子按数值大小排列很多场景下我们希望柱子从高到低排列一眼看出谁是第一谁是倒数。这需要借助forcats::fct_reorder()或者手动指定因子水平。library(forcats) total_sales$产品 - fct_reorder(total_sales$产品, total_sales$总销售额, .desc TRUE) ggplot(total_sales, aes(x 产品, y 总销售额)) geom_col(fill #44AA99) geom_text(aes(y 总销售额, label 总销售额), vjust -0.5)fct_reorder(产品, 销售额)会把产品按照销售额的平均值重新排序。.desc TRUE表示降序柱子最高的排在最左边。这个函数在画水平柱状图时尤其好用因为水平柱状图通常希望从大到小从上往下排。如果你不想额外引入forcats也可以直接用base方法total_sales$产品 - factor(total_sales$产品, levels total_sales$产品[order(total_sales$总销售额, decreasing TRUE)])两种写法效果一样但fct_reorder更简洁我推荐使用它。5. 项目实战从原始表到可发布图表的完整流程5.1 场景描述与模拟数据假设你拿到了一份销售明细表字段包括产品类别、销售区域、订单金额、日期。你的需求是画一张“各产品类别在各区域的销售额对比图”一眼能看出哪个区域最强、哪个产品最弱。我先构造一份模拟数据set.seed(4321) raw_sales - data.frame( 产品 sample(c(手机, 笔记本, 平板, 耳机), 200, replace TRUE), 区域 sample(c(华北, 华东, 华南, 西南), 200, replace TRUE), 金额 round(runif(200, 500, 8000), 0), 日期 sample(seq(as.Date(2024-01-01), as.Date(2024-12-31), by day), 200, replace TRUE) )这种结构就是典型的明细数据不可能直接把每一行画成柱子你需要先汇总。5.2 数据处理dplyr 汇总并转成长表library(dplyr) summary_data - raw_sales %% group_by(产品, 区域) %% summarise(销售额 sum(金额), .groups drop) head(summary_data)这里group_by(产品, 区域)表示按两个维度分组summarise(销售额 sum(金额))计算每个组合的总金额。.groups drop是为了让结果不再保留分组属性避免后续步骤出现意外。如果你画分组柱状图这种“产品一行区域一行”的长表已经可以直接用了ggplot(summary_data, aes(x 产品, y 销售额, fill 区域)) geom_col(position position_dodge(width 0.8), width 0.7)5.3 完整的可发布图表主题、标签、颜色一步到位实际项目中我不会直接把默认图丢出去至少会调整这几个部分轴标题、图例标题、主题、配色、数值格式。下面这段代码比较接近我日常工作流的最终版本library(scales) final_plot - ggplot(summary_data, aes(x 产品, y 销售额, fill 区域)) geom_col(position position_dodge(width 0.8), width 0.7) geom_text( aes(label comma(销售额)), position position_dodge(width 0.8), size 3, vjust -0.2 ) scale_fill_manual(values c( 华北 #D55E00, 华东 #0072B2, 华南 #009E73, 西南 #CC79A7 )) scale_y_continuous(labels comma, expand expansion(mult c(0, 0.1))) labs( title 各产品在不同区域的销售额对比, x 产品类别, y 销售额元, fill 销售区域 ) theme_minimal() theme( plot.title element_text(face bold, size 16, hjust 0.5), axis.text element_text(size 11), legend.position top ) final_plot这段代码你可以直接抄走改数据。几个关键点comma(销售额)来自scales包把 1234 格式化显示成 1,234标签不会变成很长的数字串。expand expansion(mult c(0, 0.1))让柱子紧贴 x 轴底部不留空白同时给柱子顶部上方留出 10% 的余量避免标签被绘图区边缘切掉。theme_minimal()是我常用的基础主题它去掉了默认的灰色背景和网格线比较干净。legend.position top把图例放到图的上方读者先看图例再读数据比放在右侧更自然。如果你把这张图放到报告里还需要统一字体。中文字体在不同操作系统上的默认显示不一致我一般会在theme()里加一个text element_text(family Noto Sans CJK SC)或PingFang SC具体取决于当前环境。最稳妥的方法是先用windowsFonts()或extrafont注册好字体再传给theme()。5.4 保存图片ggsave 的参数细节画好的图需要输出成文件常见格式是 PDF、PNG、TIFF。我用得最多的是ggsave()ggsave( filename sales_bar.png, plot final_plot, width 10, height 6, dpi 300 )width和height默认单位是英寸。10 x 6 英寸在屏幕上看起来大约是 3000 x 1800 像素足够清晰。dpi 300适合打印如果只是为了放在网页上dpi 150也可以文件会更小。很多人保存图片后发现中文乱码尤其是保存 PDF 时。解决思路是在ggsave()中指定device cairo_pdf或者提前用showtext包加载系统字体。PNG 格式一般不会乱码PDF 格式更容易出现。如果你需要矢量图建议用ggsave(..., device cairo_pdf)并确保字体可用。保存图片前最好调用一下dev.off()关闭默认图形设备否则有时会弹出空白的图片文件。这个问题在 RStudio 里不是太常见但在脚本批处理时会遇到。6. 常见问题与排查技巧实录6.1 报错stat_count() can only have an x or y aesthetic这是柱状图最常见的报错之一。你可能会写出这样的代码ggplot(total_sales, aes(x 产品, y 总销售额)) geom_bar()错误提示提到stat_count()原因就是geom_bar()默认统计计数但它发现你还提供了y。它不理解你想画的是“现成的数值柱子”。解决方法是改成geom_col()或者geom_bar(stat identity)。我建议直接改成geom_col()。还有一种情况是y不是数值而是字符ggplot2 会尝试按计数处理也会报类似的错。最简单的排查思路是看一眼数据里y这一列是不是 numeric不是就先转换。6.2 中文乱码字体和输出设备中文标签在 Windows 上经常显示为方块在 Mac 和 Linux 上也可能因为缺少中文字体而乱码。我的处理步骤如下确认系统已安装中文字体例如 Windows 的“微软雅黑”macOS 的“苹方”Linux 的“Noto Sans CJK SC”。在 R 里注册字体。推荐使用showtext包library(showtext) showtext_auto()然后正常绘图geom_text()里的中文和坐标轴中文都会自动显示。这个包会自动处理设备的字体渲染比extrafont简单。如果只是偶尔画一次图也可以用windowsFonts()搭配theme(text element_text(family Microsoft YaHei))。但这种方法跨平台不一致我自己在脚本里尽量用showtext。6.3 分组柱状图上的标签位置错乱如果你发现分组柱状图中三根柱子的标签全部堆积在同一根柱子上方多半是geom_text()缺少position_dodge()。geom_col()设置了position position_dodge(width 0.8)geom_text()也必须设置同样的position_dodge(width 0.8)两个图层才能对齐。另一种情况是标签确实错开了但还是不对齐。这通常是因为position_dodge(width 0.8)中的宽度和geom_col()的width 0.7不一致尤其在柱状图旁边还有误差线时。解决办法是让所有图层使用同样的position_dodge(width 0.8)并且不要随便改。如果发现标签和柱子有偏移优先检查这两个地方。6.4 图例顺序和名称调整图例顺序默认跟随因子水平。如果你想改变图例项目的顺序直接改因子的levelssummary_data$区域 - factor(summary_data$区域, levels c(华东, 华北, 华南, 西南))图例名称可以通过scale_fill_manual(name 区域名称, values ...)修改也可以直接在labs(fill 区域名称)里写。如果你的 fill 映射用的不是区域而是其他变量就对应改成labs(color ...)或labs(fill ...)。我还经常用guides()控制图例的显示细节。比如不想显示某个图例guides(fill none)这在某些只需要颜色突出但不需要图例的宽幅图中很实用。6.5 柱子不按数值大小排列或排序失效这个问题我在 4.6 里提过。再补充一个容易忽略的点如果你用了coord_flip()那么用fct_reorder()时要注意方向。默认fct_reorder(产品, 销售额)是按从小到大排序翻转后最大的柱子会排在底部。如果你希望最大的柱子排在最上面用.desc TRUE或者手动反向因子水平。我踩过的坑是先对数据arrange(desc(销售额))然后直接画图发现顺序还是乱的。因为 ggplot2 读取数据顺序不等于绘图顺序绘图顺序完全由因子水平决定。所以一旦遇到排序问题第一反应应该是“检查 x 变量的因子水平”而不是反复调数据框的行顺序。6.6 柱子底部悬空或者高度被截断默认情况下ggplot2 会在坐标轴两端自动留出 5% 的扩展空白所以柱子底部通常不会贴住 x 轴。有些主题下柱子看起来“悬浮”在半空中其实是因为扩展空白过大。想要柱子贴紧 x 轴可以scale_y_continuous(expand expansion(mult c(0, 0.05)))expansion(mult c(0, 0.05))的意思是 x 轴和柱子底端之间留 0 的额外比例上方留 5%。如果你不设置默认可能是 0.05 和 0.05柱子下方就悬空了一段。很多期刊图表要求柱子必须从零开始并且底部不空白这个参数就是用来解决这类需求的。还有一个危险的用法是coord_cartesian(ylim c(0, 1500))它只会放大坐标轴区域不会裁剪数据。如果你用scale_y_continuous(limits c(0, 1500))超出范围的柱子会被直接裁掉。两者的区别在画分组柱状图时尤其重要。如果只是想看清局部用coord_cartesian()如果确实只想显示某一范围并排除离群值才用scale_limits。6.7 图例和柱子颜色对不上这种情况通常是因为在aes()外直接传了fill颜色同时又在图例中设置了其他变量。比如ggplot(df, aes(x 产品, y 销售额)) geom_col(aes(fill 区域), color black) scale_fill_manual(values c(...))如果你在geom_col()里既用了aes(fill 区域)又想在scale_fill_manual中指定颜色请确保颜色向量里的数量与区域类别数量一致。如果数量不一致颜色会循环或报错。图例名称和 fill 映射必须一致否则无法对应。另一个新手常犯的错误是geom_col(fill 区域) # 错误举例fill 应是颜色不是变量如果fill不是颜色常量而是变量名必须放进aes()里面。这个规则适用于所有图形属性变量映射写在aes()里常量参数直接写在geom_*()里。6.8 标签重叠、文字太挤怎么办柱子太窄或者数值太大导致标签重叠时有几个简单办法把size改小比如从 4 降到 2.5。旋转标签角度angle 90但会显得不够直观。用check_overlap TRUE这个参数在geom_text()里只能对同一层文本起作用能避免重复文本重叠但会删掉一些标签。改用geom_label()给标签加白色背景这样被压住的文字依然可读。如果分类很多我一般直接改画水平柱状图这是最简单也最耐看的选择。最后再分享一点个人体会柱状图画了这么多年我最常用的组合是geom_col()position_dodge()coord_flip()。原因很实际竖向柱状图在分类名称长的时候总是要处理重叠问题翻转成水平后一眼就能看清所有标签分组柱状图能同时传达两个维度的信息。我踩过的坑里面90% 都出在factor顺序和position对齐上尤其是分组柱状图加数值标签时两边参数不一致图就彻底乱了。如果你准备把 ggplot2 当成常用工具建议一开始就养成写小函数的习惯。比如把统一配色、主题、字体封装成一个函数每次画图直接调用后面所有图表的风格会非常一致。这个方法帮我省了大量调整图表的时间。这个系列我后续还会整理折线图、箱线图、热力图这些常用图形的画法如果你有想看的主题也可以顺着这个思路先自己动手试一遍。画图这件事代码抄得再多也不如自己跑一遍记得牢。