什么时候用它
饼图用角度和面积表达一个整体的构成,适合回答“总量里大约有多少属于每一类”。它最适合类别少、差异明显、各部分相加严格等于整体的场景;本例把 592 名学生按眼睛颜色汇总为四个互斥类别。
饼图不是精确比较工具。人眼对共同基线上的长度更敏感,对扇区角度的细小差异不敏感;Brown 与 Blue 这种相近比例,只能读成“接近”,不能靠图判断谁多 1 个百分点。需要精确排序或比较相近数值时,改用柱状图。
它是怎么画出来的
ggplot2 没有单独的 geom_pie()。饼图本质上是把一根 100% 堆叠柱卷成圆:
geom_col() 按计数画出一根堆叠柱。
coord_polar(theta = "y") 把柱高方向映射为圆周角度。
- 每个扇区的角度等于该组计数占总数的比例。
这与 evanverse::plot_pie() 的核心实现一致;Recipe 把汇总、因子顺序和标注计算显式写出,方便检查每一步的数据含义。
怎么读
- 先确认分母。 标题或图注应说明“整体”究竟是谁,以及总样本量
n。
- 先看大块,再看小块。 饼图可靠表达的是明显的构成差异,而不是小数点后的精确排名。
- 用标签读取数值。 角度给整体印象,百分比标签承担相对精确的读数。
- 检查是否完整。 所有扇区必须互斥,并且合计为 100%;缺失类别不能被悄悄排除。
常见陷阱
- 类别太多。 超过五六块后,颜色、图例与小扇区会一起失去可读性;合并有意义的次要类别或改用柱图。
- 用饼图比较相近值。 例如 37% 与 36% 几乎无法靠角度可靠区分。
- 同时使用三维和透视。 透视会让靠近观察者的扇区显得更大,直接扭曲面积判断。
- 随意拉出某个扇区。 exploded pie 会把视觉注意力当成数据差异;只有事先定义的重点才有理由强调。
- 按大小重排有自然顺序的类别。 流程阶段、剂量等级等应保留原始顺序;本例为名义类别,显式固定顺序是为了跨图一致。
- 在小扇区里塞长标签。 标签放不下时使用图例或外部标注,不要缩到无法阅读。
- 颜色循环使用。 色板不足时不能从头重复颜色;应减少类别或把其余类别明确处理为中性上下文。
配色
眼睛颜色是四个无序类别,因此使用定性色板 walter_white2 的前四种颜色。相邻扇区之间加入细窄的纸白色边线,让大面积色块保持清楚边界;边线只负责分隔,不能替代本身可区分的颜色。
扇区面积比散点更容易暴露浅色与深色的文字对比问题。标签版为每个扇区预先指定深色或白色文字,正式换色板时也应重新检查标签对比,而不是固定使用一种文字颜色。
配方
数据与公共样式
hair_eye_color 已经是 Hair × Eye × Sex × Freq 的汇总长表。这里再次按 Eye 合并发色与性别,并检查总数与比例之和。
library(dplyr)
library(ggplot2)
library(scales)
library(ggforce)
library(biopalette)
d <- read.csv("content/tessera/data/csv/hair_eye_color.csv")
eye_levels <- c("Brown", "Blue", "Hazel", "Green")
eye_total <- d |>
summarise(count = sum(Freq), .by = Eye) |>
mutate(Eye = factor(Eye, levels = eye_levels)) |>
arrange(Eye) |>
mutate(
share = count / sum(count),
label = percent(share, accuracy = 1)
)
if (sum(eye_total$count) != 592 || abs(sum(eye_total$share) - 1) > 1e-12) {
stop("眼睛颜色汇总未覆盖完整样本")
}
eye_colors <- setNames(
get_palette("walter_white2", type = "qualitative")[1:4],
eye_levels
)
pie_theme <- theme_void(base_size = 13) +
theme(
plot.title = element_text(face = "bold", size = 15, hjust = 0),
plot.subtitle = element_text(color = "grey35", hjust = 0),
legend.title = element_text(face = "bold"),
legend.text = element_text(size = 10.5),
plot.margin = margin(14, 22, 14, 22)
)
A · 基础饼图
先让扇区只承担构成关系,精确计数由图例与配套文字提供。position_stack(reverse = TRUE) 与图例采用同一类别顺序,第一类从十二点方向开始,阅读顺序稳定。
#| fig: basic
#| fig-width: 7.2
#| fig-height: 5.4
ggplot(eye_total, aes(x = "", y = count, fill = Eye)) +
geom_col(
width = 1,
color = "#F4F3EE",
linewidth = 0.9,
position = position_stack(reverse = TRUE)
) +
coord_polar(theta = "y", start = -pi / 2, direction = 1) +
scale_fill_manual(values = eye_colors, breaks = eye_levels) +
labs(
title = "Eye color composition",
subtitle = "592 students · HairEyeColor",
fill = "Eye color"
) +
pie_theme
pie-basic
B · 在扇区内标百分比
百分比比原始人数更贴合饼图的任务。标签仍沿用与扇区完全相同的反向堆叠位置;Brown 与 Blue 使用白字,其余浅色扇区使用深字,避免只在某一块上失去对比。
#| fig: labeled
#| fig-width: 7.2
#| fig-height: 5.4
eye_total <- eye_total |>
mutate(label_color = ifelse(Eye %in% c("Brown", "Blue"), "#FFFFFF", "#262622"))
ggplot(eye_total, aes(x = "", y = count, fill = Eye)) +
geom_col(
width = 1,
color = "#F4F3EE",
linewidth = 0.9,
position = position_stack(reverse = TRUE)
) +
geom_text(
aes(label = label, color = label_color, group = Eye),
position = position_stack(vjust = 0.5, reverse = TRUE),
size = 4,
fontface = "bold",
show.legend = FALSE
) +
coord_polar(theta = "y", start = -pi / 2, direction = 1) +
scale_fill_manual(values = eye_colors, breaks = eye_levels) +
scale_color_identity() +
labs(
title = "Eye color composition",
subtitle = "Percent of 592 students",
fill = "Eye color"
) +
pie_theme
pie-labeled
C · 圆环变体
圆环没有增加新的统计信息,只是用中心留白换取更轻的视觉重量。x = 2 把堆叠柱放到外圈,xlim() 决定中央孔径;孔越大,扇区面积越小,小类别也越难辨认。
#| fig: donut
#| fig-width: 7.2
#| fig-height: 5.4
ggplot(eye_total, aes(x = 2, y = count, fill = Eye)) +
geom_col(
width = 0.72,
color = "#F4F3EE",
linewidth = 0.9,
position = position_stack(reverse = TRUE)
) +
geom_text(
aes(label = label, color = label_color, group = Eye),
position = position_stack(vjust = 0.5, reverse = TRUE),
size = 3.8,
fontface = "bold",
show.legend = FALSE
) +
coord_polar(theta = "y", start = -pi / 2, direction = 1) +
xlim(0.65, 2.45) +
scale_fill_manual(values = eye_colors, breaks = eye_levels) +
scale_color_identity() +
labs(
title = "Eye color composition",
subtitle = "The same data as a donut chart",
fill = "Eye color"
) +
pie_theme
pie-donut
D · 抽出一个重点扇区
Exploded pie 通过平移扇区强调一个预先确定的类别。这里仅示范把 Green 移出;偏移量是版式参数,不编码额外数值。不要按结果临时挑一个扇区抽出,也不要同时拉开多块——那会破坏整体轮廓,却没有清楚的重点。
ggforce::geom_arc_bar() 允许给每个扇区独立的圆心。起止角仍严格来自累计比例,只有 Green 的 x0 / y0 沿扇区中线移动。
#| fig: exploded
#| fig-width: 7.2
#| fig-height: 5.4
exploded_data <- eye_total |>
mutate(
end = cumsum(share) * 2 * pi,
start = lag(end, default = 0),
middle = (start + end) / 2,
offset = ifelse(Eye == "Green", 0.12, 0),
x0 = sin(middle) * offset,
y0 = cos(middle) * offset
)
ggplot(exploded_data) +
geom_arc_bar(
aes(
x0 = x0,
y0 = y0,
r0 = 0,
r = 1,
start = start,
end = end,
fill = Eye
),
color = "#F4F3EE",
linewidth = 0.9
) +
coord_fixed(xlim = c(-1.28, 1.28), ylim = c(-1.18, 1.18), clip = "off") +
scale_fill_manual(values = eye_colors, breaks = eye_levels) +
labs(
title = "Eye color composition",
subtitle = "Green highlighted with a single exploded slice",
fill = "Eye color"
) +
pie_theme
pie-exploded
四种画法怎么选
- 图很小,或正文已经给出数字 → A,保留最干净的整体构成。
- 读者需要直接读取比例 → B;这是本例最完整、最推荐的版本。
- 版面需要更轻的块面,或中心需要留白 → C;它是视觉变体,不比普通饼图更准确。
- 有一个事先定义的重点类别 → D;只抽出这一块,并在标题或图注明确说明原因。
如果类别相近、需要排序,或者读者必须比较百分点差异,不在这些饼图之间纠结,直接改用从零开始的柱状图。