← 返回 Tessera
构成4 张图hair_eye_color(Tessera Toy,32 行长表)2026-08-11

pie

一个整体由少数几个类别组成时,各部分大约占多少?

需要的输入
1 个分类变量;或 1 个分类变量 + 1 个非负计数变量
示例数据
hair_eye_color(Tessera Toy,32 行长表)
依赖
ggplot2 · dplyr · scales · ggforce · biopalette
配色
walter_white2
成图预览另有 3 张在配方里
pie

什么时候用它

饼图用角度和面积表达一个整体的构成,适合回答“总量里大约有多少属于每一类”。它最适合类别少、差异明显、各部分相加严格等于整体的场景;本例把 592 名学生按眼睛颜色汇总为四个互斥类别。

饼图不是精确比较工具。人眼对共同基线上的长度更敏感,对扇区角度的细小差异不敏感;Brown 与 Blue 这种相近比例,只能读成“接近”,不能靠图判断谁多 1 个百分点。需要精确排序或比较相近数值时,改用柱状图。

它是怎么画出来的

ggplot2 没有单独的 geom_pie()。饼图本质上是把一根 100% 堆叠柱卷成圆:

  1. geom_col() 按计数画出一根堆叠柱。
  2. coord_polar(theta = "y") 把柱高方向映射为圆周角度。
  3. 每个扇区的角度等于该组计数占总数的比例。

这与 evanverse::plot_pie() 的核心实现一致;Recipe 把汇总、因子顺序和标注计算显式写出,方便检查每一步的数据含义。

怎么读

  1. 先确认分母。 标题或图注应说明“整体”究竟是谁,以及总样本量 n
  2. 先看大块,再看小块。 饼图可靠表达的是明显的构成差异,而不是小数点后的精确排名。
  3. 用标签读取数值。 角度给整体印象,百分比标签承担相对精确的读数。
  4. 检查是否完整。 所有扇区必须互斥,并且合计为 100%;缺失类别不能被悄悄排除。

常见陷阱

  • 类别太多。 超过五六块后,颜色、图例与小扇区会一起失去可读性;合并有意义的次要类别或改用柱图。
  • 用饼图比较相近值。 例如 37% 与 36% 几乎无法靠角度可靠区分。
  • 同时使用三维和透视。 透视会让靠近观察者的扇区显得更大,直接扭曲面积判断。
  • 随意拉出某个扇区。 exploded pie 会把视觉注意力当成数据差异;只有事先定义的重点才有理由强调。
  • 按大小重排有自然顺序的类别。 流程阶段、剂量等级等应保留原始顺序;本例为名义类别,显式固定顺序是为了跨图一致。
  • 在小扇区里塞长标签。 标签放不下时使用图例或外部标注,不要缩到无法阅读。
  • 颜色循环使用。 色板不足时不能从头重复颜色;应减少类别或把其余类别明确处理为中性上下文。

配色

眼睛颜色是四个无序类别,因此使用定性色板 walter_white2 的前四种颜色。相邻扇区之间加入细窄的纸白色边线,让大面积色块保持清楚边界;边线只负责分隔,不能替代本身可区分的颜色。

扇区面积比散点更容易暴露浅色与深色的文字对比问题。标签版为每个扇区预先指定深色或白色文字,正式换色板时也应重新检查标签对比,而不是固定使用一种文字颜色。

配方

方法绘图系统什么时候选它
Ageom_col() + coord_polar()ggplot2类别很少,只需展示整体构成与大致比例
Bposition_stack()ggplot2在扇区中央标出百分比,并保持标签与扇区使用相同堆叠顺序
Cxlim()ggplot2需要圆环变体,为中央说明或版式留出空间
Dgeom_arc_bar()ggplot2(ggforce 扩展)只把一个事先确定的重点扇区移出,其余扇区保持原位

数据与公共样式

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
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
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
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
pie-exploded

四种画法怎么选

  1. 图很小,或正文已经给出数字 → A,保留最干净的整体构成。
  2. 读者需要直接读取比例 → B;这是本例最完整、最推荐的版本。
  3. 版面需要更轻的块面,或中心需要留白 → C;它是视觉变体,不比普通饼图更准确。
  4. 有一个事先定义的重点类别 → D;只抽出这一块,并在标题或图注明确说明原因。

如果类别相近、需要排序,或者读者必须比较百分点差异,不在这些饼图之间纠结,直接改用从零开始的柱状图。

运行环境5 个包 · 2026-08-11T20:20:26.127+0800

R version 4.5.1 (2025-06-13 ucrt) · x86_64-w64-mingw32

  • biopalette 0.1.0
  • dplyr 1.2.1
  • ggforce 0.5.0
  • ggplot2 4.0.3
  • scales 1.4.0