
饼图大概是R绘图里最容易上手、也最容易画丑的图形之一。你只需要一个数值向量调用pie()函数几秒钟就能出一张图但如果你想把这张图做得规范、清晰、能直接放进报告里事情就远没有这么简单了。标签怎么放不重叠、顺序怎么排才符合直觉、百分比要不要显示、颜色怎么选才不辣眼睛更不用说如何用ggplot2实现更灵活的自定义效果这些细节我踩过的坑加起来能写满一整页。这篇博文就围绕“R绘图-饼图”这个主题从基础画法讲到进阶改造再聊到饼图到底该怎么用才算合格的数据可视化希望能帮到正在学R绘图、或者想在报告里把饼图做得更专业的读者。1. 基础饼图到底怎么画base R的pie()其实够用刚接触R绘图的人很容易被网上各种花哨的案例吓退觉得一定要上ggplot2才算正经。但如果你只是临时看一眼数据分布或者做一张快速汇报用的示意图R自带的pie()函数完全够用没必要一上来就引一堆包。1.1 一张最小可用的饼图代码假设你手头有一份分类销售数据想看看各品类占比我一般会先跑这样一段sales - c(电器 45, 服饰 28, 食品 18, 图书 9) pie(sales)就这么一行R会把sales里的数值加起来算比例然后按顺时针方向从12点钟位置开始画扇区。注意pie()默认从x轴正上方开始方向是顺时针这跟很多人的习惯一致不需要额外调。如果嫌默认的灰色大饼太单调可以加col参数指定颜色main参数加标题pie(sales, col c(#4E79A7, #F28E2B, #59A14F, #B07AA1), main 某季度各品类销售额占比)col的向量长度如果比数据少R会自动循环填充所以你要是只给了两个颜色四个扇区就会交替出现两种颜色。这个机制偶尔也会被用来做特殊效果但大多数时候我会建议颜色数量跟扇区数量一一对应否则容易误导看图的人。1.2 标签和百分比怎么放才不出错默认的pie()画出来的标签只是“分类名百分比”的朴素组合但实际使用中有一个很常见的坑当某个扇区的占比太小比如低于5%时标签文本会和相邻扇区的标签挤在一起甚至在图形边界外被截断。一个简单的改善办法是先算好百分比再用labels参数手动拼接让标签更可控pct - round(sales / sum(sales) * 100, 1) lbl - paste0(names(sales), , pct, %) pie(sales, labels lbl, col cols, cex 0.8)cex控制标签字号一般0.8到0.9比较合适太大了小扇区的标签很容易叠。如果数据里确实有一个特别小的占比我会顺手处理一下把小于某阈值的分类合并成“其他”这样不仅标签不再重叠图也更干净。合并不意味着丢掉信息至少你可以把“其他”的明细放到图注或者表格里。1.3 中文字体才是base R绘图的第一大坑用pie()画中文标签很多人第一次跑出来就是一堆方块或乱码。这不是饼图本身的问题而是R默认的图形设备对中文字体支持不完善。在Windows系统上我一般会在画图之前先设一下par的family参数par(family Microsoft YaHei) pie(sales, labels lbl)如果没有Microsoft YaHei这个字体名改成SimHei或者STHeiti也行。Mac上则通常是PingFang SC或STHeiti。最稳妥的办法是用quartzFonts或者showtext包把字体载入R的图形系统library(showtext) font_add(heiti, msyh.ttc) showtext_begin() pie(sales, labels lbl) showtext_end()showtext的好处是不管你在哪个平台也不管RStudio里预览还是导出PDF、PNG字体效果都能保持一致。说实话字体问题如果一开始不解决等你把图放进PPT里再发现乱码返工成本就高了。2. 用ggplot2画饼图先理解“饼图就是卷起来的条形图”很多人第一次看到ggplot2画饼图的代码会懵代码里没有pie字样而是用geom_bar()加coord_polar()。搞懂这个底层逻辑你才算真正掌握了ggplot2的饼图画法而不是只会抄代码。2.1 从条形图到饼图的坐标变换ggplot2里画饼图的核心思想是先把分类数据用geom_bar()画成一个堆叠条形图再用coord_polar()把条形图从笛卡尔坐标系“卷”成极坐标系。条形图的每一段高度就变成了饼图里每一个扇区的弧长。基础代码如下library(ggplot2) df - data.frame( category c(电器, 服饰, 食品, 图书), value c(45, 28, 18, 9) ) ggplot(df, aes(x , y value, fill category)) geom_bar(stat identity, width 1) coord_polar(theta y, start 0)注意coord_polar()里的theta y这告诉ggplot2把y轴数据映射成角度。如果你写成theta x卷起来的就是另一个方向的效果大多数饼图需求用不到。start 0表示从12点钟方向开始。如果需要调整起始角度比如想让某一类从正右侧开始可以设置start pi / 2之类的弧度值。弧度制在这个地方对新手不太友好但记住pi对应180度就够了。2.2 扇区宽度、边框线和主题清理ggplot2默认画出来的饼图扇区之间会有白色分割线但有时分割线太粗反而让图形显得碎。通过geom_bar里的color和linewidth参数可以控制ggplot(df, aes(x , y value, fill category)) geom_bar(stat identity, width 1, color white, linewidth 0.3) coord_polar(theta y, start 0) theme_void()theme_void()会清掉坐标轴、网格线和背景这是饼图排版里很重要的一步。如果你不写这个图里会残留x轴、y轴的刻度线和坐标标签看起来特别业余。width 1也很关键。在ggplot2里geom_bar的宽度是相对分组区间而言的饼图场景下x只有一个分类所以宽度设为1才能让条形图完整占满整个“圆筒”卷起来后没有缺口。如果小于1饼图中间会出现一个缺口反而变成类似环形图的效果——这有时候是故意的但如果没弄明白原因就别乱调。2.3 从饼图到环形图中心留白的实用技巧环形图Donut Chart在商务报告里出镜率相当高原因很简单中间那块空白可以放总数、核心指标或者品牌logo信息密度更高。跟很多人的直觉相反ggplot2里画环形图的方法比画普通饼图更简单只要在geom_bar里加一个xlim限制即可ggplot(df, aes(x , y value, fill category)) geom_bar(stat identity, width 1, color white, linewidth 0.3) coord_polar(theta y, start 0) xlim(c(0.5, 1.5)) theme_void()xlim(c(0.5, 1.5))相当于把原来的圆从中心挖掉一块只保留外环。这个数字可以根据想要的环厚薄来调范围越大环越薄。我一般习惯用xlim(0.5, 1.5)环不会太宽也不会太细。图做好之后再用annotate()往中心加文本annotate(text, x 0.5, y 0, label 总销售额\n100万, size 5)x要跟xlim的下界保持一致文字才会落在环中心的位置。2.4 标签位置计算用累计百分比定位扇区ggplot2画饼图最麻烦的不是画扇区而是放标签。扇区标签如果直接放在每个扇区的正中央就需要先算出每个扇区的“角度中心位置”。这个位置不能靠猜要用累积百分比来算。比如我们的数据df$pct - df$value / sum(df$value) * 100 df$ymax - cumsum(df$pct) df$ymin - c(0, head(df$ymax, -1)) df$label_pos - (df$ymax df$ymin) / 2 df$label_text - paste0(df$category, \n, round(df$pct, 1), %)ymin和ymax是每个扇区在y轴上的起止位置label_pos取两者的中间点正是扇区的角度中心。然后画图时用geom_text把标签放到这个位置ggplot(df, aes(x , y value, fill category)) geom_bar(stat identity, width 1, color white, linewidth 0.3) geom_text(aes(y label_pos, label label_text), size 4, nudge_x 0.2) coord_polar(theta y, start 0) theme_void()nudge_x 0.2是把标签沿着径向往外推一点让文字不要紧贴中心。对占比特别小的扇区哪怕位置算对了文字一样容易和相邻扇区重叠。这种情况要么把标签统一放在饼图外面再加引线指示要么干脆在下面放一个明细表。交互式图表另说静态报告里“小而碎”的扇区不适合硬塞标签。3. 进阶需求多组饼图、交互式饼图和导出细节实际工作中你不会永远只画一张饼图。我经常遇到的场景是数据分了好几个类别同时又要按年份或者按地区对比这时候就需要把多个饼图拼接在一起甚至做成交互式的HTML页面方便汇报时点击查看明细。3.1 用facet_wrap()拼多组饼图的正确姿势很多人会想ggplot2有facet_wrap()那直接按年份分组自动拼一排饼图不就行了理论上可以但实际效果往往很丑因为facet_wrap()会把每个面板的坐标轴都保留饼图会被强制压缩成椭圆比例失调。我的做法是先把数据按分组变量处理好然后利用ggplot2的facet_grid加上coord_polar同时设置clip off来避免标签被裁切。但更省事的方式是直接用gridExtra或者patchwork把多张已经画好的饼图拼在一起library(patchwork) p1 - ggplot(df_2023, aes(x , y value, fill category)) geom_bar(stat identity, width 1) coord_polar(theta y) theme_void() ggtitle(2023年) p2 - ggplot(df_2024, aes(x , y value, fill category)) geom_bar(stat identity, width 1) coord_polar(theta y) theme_void() ggtitle(2024年) p1 | p2patchwork用|和/控制拼图方向横排竖排都很直观。拼完之后如果需要统一图例可以在一张图里用guides提取图例或者让每张图都保留图例后再微调。这里有个经验多图对比时让所有图的扇区顺序保持一致否则看图的人很难快速抓住差异。3.2 用plotly把静态饼图变成可交互的R里面做交互式饼图最省事的方案是plotly::ggplotly()它能把ggplot2对象直接转换成plotly的HTML交互图鼠标悬停就能看到数值和百分比library(plotly) p - ggplot(df, aes(x , y value, fill category)) geom_bar(stat identity, width 1) coord_polar(theta y) theme_void() ggplotly(p, tooltip c(fill, y))tooltip参数控制悬停时显示的字段。默认情况下plotly显示的y值可能不带单位所以我通常会在数据框里先构造一个文本列比如45万元然后让text参数指向它这样悬停提示更友好。ggplotly转出来的交互图有一个需要注意的点它会把coord_polar的饼图转成真正的极坐标扇形。如果你的原图里用了geom_text放标签标签的位置在转换后偶尔会偏移需要自己在数据里多试几次偏移量。3.3 导出图片的尺寸与分辨率设置饼图的导出参数其实比很多人想象的更重要。同一张图分辨率不对放到PPT里就是糊的尺寸比例不对圆饼会被拉成椭圆。我用ggsave()导出时的一般设置是ggsave(pie_chart.png, width 8, height 6, dpi 300, bg white)宽高比保持4:3或者1:1比较稳妥。如果图里有长分类名我会把宽度稍微加大到9或10防止标签被截断。导出PDF给印刷用就把dpi设为300以上文件格式换成矢量最好。另外建议把bg设置为white有些情况下默认的透明背景在PPT深色模板里会显得突兀提前设置好免得返工。4. 饼图可视化规范和常见误区饼图在数据可视化圈子里争议很大但吐槽归吐槽饼图依然有它适合的场景。重点是你要清楚什么时候该用什么时候不该用以及用了之后怎么让它更专业。4.1 为什么饼图经常被批评数据可视化领域对饼图的批评主要有三点。一是人眼对角度和面积的感知不如对长度的感知精确两张饼图放在一起各位差异小于5%时基本分辨不出来二是扇区数量一多整个图就会变得像一个大转盘信息获取效率低三是当各类别占比接近时饼图难以一眼看出排序关系而条形图只要扫一眼长度就一目了然。这不是说饼图一无所有而是说它更适合展示“部分与整体”的关系尤其在占比差异明显、类别数量不多的情况下。比如“某个市场中三家头部品牌占了85%份额”这种信息用饼图一画出来就很直观但如果是“某个产品线里12个SKU的销售占比”饼图就会变成灾难。4.2 饼图可用的场景和评分标准我自己的判断标准很简单分类不超过6个、没有“各占比都很接近”的情况、且核心目的是突出某一个占绝对优势的类别用饼图是合适的。超过6个分类就合并成“其他”或者直接改用条形图。画饼图时还需要注意几个细节规范扇区从大到小排列最大的从12点钟方向开始这样视觉顺序最自然。百分比至少保留一位小数太小了干脆不标。类别名不要横七竖八地转角度尽量保持水平或者统一放在图外。用色尽量选色盲友好的调色板比如RColorBrewer的Set3、Paired等方案。关于排序ggplot2默认的因子顺序是按字母排的这在中文标签下没有明确规则所以我会提前用factor()指定顺序df$category - factor(df$category, levels c(电器, 服饰, 食品, 图书))这样扇区的顺序完全受控不会因为绘图环境不同而改变。4.3 什么时候应该放弃饼图改用条形图或其它图如果你发现数据里有十来个基础分类或者好几个分类占比都在10%到15%之间甚至还有零值、极小值那直接用水平条形图往往更清晰。条形图在R里画起来也简单ggplot(df_sorted, aes(x reorder(category, value), y value, fill category)) geom_col() coord_flip() theme_minimal() labs(x NULL, y 销售额)reorder(category, value)会让条形图自动按数值从大到小排列一眼就能看出哪个品类排在前面。读者不需要转动头部去对比角度视线沿着条形长度扫过去就能秒懂。既然目标是用最少的认知成本传达信息那就没必要死守饼图这一种形式。5. 常见问题与排查技巧实录无论用base R还是ggplot2画饼图总会遇到一些固定的问题。我把自己踩过的坑整理成了一张速查表按“症状-原因-解决”的方式列出来方便你在遇到同样问题时快速定位。5.1 饼图问题速查表问题的表现可能的原因解决方案中文标签显示为方块图形设备不支持中文字体设置par(family Microsoft YaHei)或用showtext载入字体标签互相重叠扇区太小或标签字号过大合并小分类为“其他”、减小cex或size、把标签移到图外画出来的不是圆而是椭圆图形窗口宽高比例不合适用ggsave()指定width和height保证比例接近1:1饼图中间出现白色空隙geom_bar()的width小于1将width设为1或检查xlim的范围是否是故意的环图效果ggplotly()转换后标签错位plotly对极坐标文本支持有限改用plot_ly()原生绘制或接受悬停交互、移除图中标签扇区顺序不符合预期因子水平没有指定顺序提前用factor(level ...)设置好顺序负值或零值导致图形异常饼图不适用于负值数据检查数据负值应改用其他图形零值直接剔除或单独说明图例和扇区颜色对不上填充向量顺序和数据顺序不一致确保scale_fill_manual(values ...)的颜色顺序与因子水平一致这个表里最容易被忽略的是最后一行的颜色顺序问题。很多人写scale_fill_manual(values c(red, blue, green))但数据里因子水平的顺序其实是“电器、服饰、食品”颜色就会错位。排查时可以先levels(df$category)确认顺序再对照调色板。5.2 三个值得一试的独家技巧第一个技巧是用geom_text配合position_stack(vjust 0.5)自动居中标签。ggplot2里除了自己算label_pos还可以直接让position_stack处理堆叠条形图的居中逻辑代码会简洁一些ggplot(df, aes(x , y value, fill category)) geom_bar(stat identity, width 1) geom_text(aes(label label_text), position position_stack(vjust 0.5)) coord_polar(theta y)这个做法在扇区比较多时尤其省事不用维护一堆位置变量。第二个技巧是做“带引导线的外置标签”。当扇区多、标签长时把标签全部放在饼图外侧用小线段指向对应扇区图的可读性会大幅提升。静态R图实现引导线稍显繁琐如果项目周期紧我通常会把外置标签的坐标算好后用geom_segment()和geom_text()组合完成如果时间充裕就直接上ggrepel包library(ggrepel) ggplot(df, aes(x , y value, fill category)) geom_bar(stat identity, width 1) geom_text_repel(aes(y label_pos, label label_text), nudge_x 0.5) coord_polar(theta y) xlim(c(0, 1.5)) theme_void()xlim(0, 1.5)给外侧标签留出空间ggrepel会自动推开重叠的标签。效果比硬贴标签好看很多尤其在分类多的时候。第三个技巧是给饼图加“中心总数值”。不管画的是饼图还是环形图中间放一个总数能让信息一步到位而不需要读者自己心算求和。用环形图加annotate完全可控也方便加副标题说明比如“总计100万元某品类贡献38%”这种画龙点睛的效果。写在最后饼图之外的一些体会说实话这几年我自己用饼图的频率已经少了很多大多数对比场景都改用条形图或者棒棒糖图了因为传信息确实更高效。但碰到“整体中各部分占比”或者“某个类别占绝对主导”的场景饼图依然是无可替代的表达方式。R本身给饼图提供的原生支持不算丰富但理解了base R的pie()、ggplot2的coord_polar()、以及标签位置的底层逻辑之后你完全可以组合出适合自己业务场景的成品图。最后分享一个小技巧画饼图前先把数据按数值从大到小排好再用factor()固定因子水平。这一个动作能省掉后面一半的后悔操作。至于那些让人头疼的容错问题照着上面的速查表检查一遍大部分都能当场解决。希望这篇经验之谈能帮你在R绘图的路上少踩几个坑做出真正能拿得出手的饼图。