Introduction
饼图用角度和面积表达一个整体的构成,回答的是"总量里大约有多少属于每一类"。它最适合类别少、差异明显、各部分相加严格等于整体的场景;这里把 592 名学生按眼睛颜色汇总成四个互斥类别。
ggplot2 没有 geom_pie()。饼图本质上是把一根 100% 堆叠柱卷成圆:geom_col() 按计数画出一根堆叠柱,coord_polar(theta = "y") 把柱高方向映射为圆周角度,于是每个扇区的角度等于该组计数占总数的比例。下面的 recipe 把汇总、因子顺序和标注计算都显式写出来,每一步的数据含义都能查。
读的时候按这个顺序:
- 先确认分母。 标题或图注应说明"整体"究竟是谁,以及总样本量
n。 - 先看大块,再看小块。 饼图可靠表达的是明显的构成差异,不是小数点后的精确排名。
- 用标签读取数值。 角度给整体印象,百分比标签承担相对精确的读数。
- 检查是否完整。 所有扇区必须互斥,并且合计为 100%;缺失类别不能被悄悄排除。
Example Data
饼图要的输入是:1 个分类变量,或 1 个分类变量 + 1 个非负计数变量。计数必须非负、可相加,而且这几个类别加起来就是整体——少一类,剩下的百分比全是错的。
hair_eye_color 已经是 Hair × Eye × Sex × Freq 的汇总长表。这里按 Eye 把发色和性别合并掉,再核对一次总数和比例之和。
library(dplyr)
library(ggplot2)
library(scales)
library(ggforce)
library(biopalette)
# 公开地址,和数据集页上「下载 CSV」给的是同一个 —— 不写仓库相对路径:
# 那个目录不进仓库,读者 clone 下来也没有这个文件,这段代码就跑不了
d <- read.csv("https://assets.evanzhou.org/tessera/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)
)
# 归一化按定义就凑得满 100%,所以"比例加起来是 1"永远成立、查不出漏类别。
# 真正要核对的是总数 —— 592 是这份数据的全样本
if (sum(eye_total$count) != 592 || abs(sum(eye_total$share) - 1) > 1e-12) {
stop("眼睛颜色汇总未覆盖完整样本")
}
Palettes
眼睛颜色是四个无序类别,用定性色板 walter_white2 的前四色。相邻扇区之间加一道细窄的纸白边线,让大面积色块保持清楚边界;边线只负责分隔,替代不了颜色本身的可区分。
色板不够时不能从头循环取色——两个不同类别拿到同一个颜色,图上看就是同一类。类别多到色板装不下,该做的是合并次要类别或换柱图。
扇区面积比散点更容易暴露文字对比问题:同一种文字颜色在深扇区上够黑、在浅扇区上就糊了。所以标签版给每个扇区单独指定深字或白字,换色板时这一步要重新检查。
eye_colors <- setNames(
get_palette("walter_white2", type = "qualitative")[1:4],
eye_levels
)
# 四张图共用一套版式,theme_void() 打底 —— 饼图不需要坐标轴和网格
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)
)
Recipe
| No. | Method | Input Data | Palettes |
|---|---|---|---|
| 1 | ggplot2 |
eye_total |
eye_colors |
| 2 | ggplot2 |
eye_total |
eye_colors |
| 3 | ggplot2 |
eye_total |
eye_colors |
| 4 | ggforce |
exploded_data |
eye_colors |
1 · 基础饼图
先让扇区只承担构成关系,精确计数交给图例和配套文字。
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)
) +
# theta = "y" 把柱高映射成圆周角;start = 0 让第一类从十二点开始
coord_polar(theta = "y", start = 0, 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


