Folioevanzhou.org
← 返回 Tessera
R2 张图2026-04-13

treemap

Nested rectangles that divide a total among its categories by area, with an optional grouping level drawn as containers.

示例数据
economy_countries
配色
babelwalter_white2
语言
R
成图预览另有 1 张在配方里
treemap

Introduction

treemap 回答的是部分与整体:总量由哪些类别构成,每一块的面积就是它占的份额;矩形还能嵌套,于是可以再表达一层分组。

它拿精度换了紧凑。人判断长度远比判断面积准,所以"第一名比第二名多多少"该用排序条形图;treemap 的位置是把十几个类别连同它们的上层分组一起塞进一个矩形,一眼看出谁占大头。

读的时候按这个顺序:

  1. 先找最大的几块。 它们通常已经解释了大部分总量。
  2. 只比较明显的面积差。 两块接近时读标签里的数值,不要靠眼睛硬判。
  3. 再看颜色和分组边界。 扁平版每个国家一个颜色;分层版同一地区统一颜色,细外框补充空间层级。
  4. 最后看小块。 标签消失不是数据消失,只是那块空间放不下可读的文字。

Example Data

treemap 要的输入是:1 个分类变量 + 1 个非负、可相加的数值变量,要分层再加 1 个分组变量。面积是把总量切开分给各类别,所以数值必须能相加——比率、均值和可能为负的数切不出面积。

economy_countries 是 217 个经济体的宏观指标表,一行一个经济体。这里取 2023 年 GDP 最高的 10 个:Top 10 能给每个标签留下足够空间,再多塞两块,小矩形的名字就开始掉。

library(dplyr)
library(ggplot2)
library(treemapify)
library(biopalette)

# 公开地址,和数据集页上「下载 CSV」给的是同一个 —— 不写仓库相对路径:
# 那个目录不进仓库,读者 clone 下来也没有这个文件,这段代码就跑不了
economy <- read.csv(
  "https://assets.evanzhou.org/tessera/csv/economy_countries.csv",
  check.names = FALSE
)

plot_data <- economy |>
  # gdp_usd > 0 不是防御性代码:负数和 0 在面积编码里没有对应的矩形
  filter(year == 2023, !is.na(gdp_usd), gdp_usd > 0) |>
  slice_max(gdp_usd, n = 10, with_ties = FALSE) |>
  mutate(
    # 世行的正式名太长,塞进矩形会被折成三行或者干脆消失
    country_label = recode(
      country,
      "Russian Federation" = "Russia",
      "Korea" = "South Korea",
      .default = country
    ),
    # 万亿美元,一位小数:原始数是 13 位整数,写在块里没人读
    label = paste0(country_label, "\n", sprintf("$%.1fT", gdp_usd / 1e12))
  )

# 显式固定地区顺序:treemapify 按因子水平安排分组容器的位置,
# 不写就跟着数据里的出现顺序走,换一年数据版面就重排了
region_levels <- c(
  "North America",
  "East Asia & Pacific",
  "Europe & Central Asia",
  "South Asia",
  "Latin America & Caribbean"
)

plot_data <- plot_data |>
  mutate(region = factor(region, levels = region_levels)) |>
  arrange(desc(gdp_usd))

Palettes

两张图的阅读单位不同,因此不强行共用一种颜色逻辑。

  • 扁平版把国家当阅读单位,用能容纳较多类别的 babel,十个国家各一色。
  • 分层版把地区当阅读单位,改用 walter_white2;同一地区统一颜色,再由细外框强化层级。

两套填充都降了透明度,让颜色与纸白背景混合,避免大矩形过于刺激。国家名已经标在块内,所以不放十项图例;分层版的地区名也直接写在区域角落。

颜色在这里不编码大小——面积已经表示 GDP。颜色只负责保持身份:扁平版是国家的身份,分层版是地区的身份。

# 命名映射,不是按位置取色:后面还要 arrange() 和分组重排,
# 按位置给色的话同一个国家在两张图里会换颜色
country_colors <- setNames(
  # babel 有 20 色,挑出的这 10 个两两之间色相拉得最开
  get_palette("babel", type = "qualitative")[c(1, 2, 3, 4, 8, 9, 11, 12, 13, 20)],
  plot_data$country_label
)

region_colors <- setNames(
  get_palette("walter_white2", type = "qualitative"),
  region_levels
)

PAPER <- "#F4F3EE"
INK   <- "#353531"

Recipe

No. Method Input Data Palettes
1 treemapify plot_data country_colors
2 treemapify plot_data region_colors

1 · 扁平 treemap

这一版不建立空间层级:十个国家在同一层比较,各用一种颜色。

国家名和数值都写在矩形里,所以不放图例——十项图例要读者在色块和列表之间来回找,而标签就在块上。

#| fig: basic
#| fig-width: 8
#| fig-height: 6
ggplot(plot_data, aes(area = gdp_usd, fill = country_label, label = label)) +
  geom_treemap(
    colour = PAPER,      # 用纸白当缝,块与块之间靠底色分开,不画深色描边
    linewidth = 1.1,
    alpha = 0.62,
    start = "topleft"    # 从左上角开始铺,最大的块落在左上,符合读图起点
  ) +
  geom_treemap_text(
    colour = INK,
    place = "centre",
    grow = FALSE,        # 不让字随块大小缩放,否则字号本身变成第二套面积编码
    reflow = TRUE,
    size = 9,
    min.size = 5.5,      # 小于这个字号就不画:宁可没有标签,也不要读不出的标签
    padding.x = grid::unit(2.4, "mm"),
    padding.y = grid::unit(2.4, "mm"),
    start = "topleft"
  ) +
  scale_fill_manual(values = country_colors, guide = "none") +
  labs(
    title = "The world's largest economies",
    subtitle = "Area shows 2023 GDP; each country keeps its own color",
    caption = "Source: World Bank via Tessera - current US$"
  ) +
  theme_void(base_size = 12) +
  theme(
    plot.title = element_text(
      colour = INK, size = 17, face = "bold", hjust = 0,
      margin = margin(b = 4)
    ),
    plot.subtitle = element_text(
      colour = INK, size = 10.5, hjust = 0,
      margin = margin(b = 12)
    ),
    plot.caption = element_text(colour = INK, size = 8.5, hjust = 1),
    plot.margin = margin(16, 18, 12, 18),
    plot.background = element_rect(fill = PAPER, colour = NA),
    panel.background = element_rect(fill = PAPER, colour = NA)
  )
treemap — basic
treemap-basic

2 · 分层 treemap

地区同时映射到 fill 和 subgroup:同地区的国家先被装进一个大矩形,并共享 walter_white2 里的一种颜色。

层级只由细灰框和角落的小标题表达,压不过面积和国家标签——地区名一旦按旧式 treemap 的做法长到塞满整组,叶节点的名字就没地方站了。

#| fig: grouped
#| fig-width: 8
#| fig-height: 6
ggplot(
  plot_data,
  aes(
    area = gdp_usd,
    fill = region,
    label = label,
    subgroup = region   # 同一个变量喂两处:fill 给颜色,subgroup 给空间容器
  )
) +
  geom_treemap(
    colour = PAPER,
    linewidth = 0.8,    # 比扁平版细:块之间的缝要弱于下面那圈分组边界
    alpha = 0.72,
    start = "topleft"
  ) +
  geom_treemap_subgroup_border(
    colour = "#77776F", # 中性灰,不用任何一个地区的色,免得被当成第六个分组
    size = 0.35,
    start = "topleft"
  ) +
  geom_treemap_text(
    # 加拿大那块窄而高,横排的名字会被 min.size 直接吃掉,转 90° 才放得下
    aes(angle = if_else(country_label == "Canada", 90, 0)),
    colour = INK,
    place = "centre",
    grow = FALSE,
    reflow = TRUE,
    size = 8.4,
    min.size = 5.2,
    padding.x = grid::unit(1.1, "mm"),
    padding.y = grid::unit(2.4, "mm"),
    start = "topleft"
  ) +
  geom_treemap_subgroup_text(
    aes(label = region),
    colour = INK,
    place = "topleft",
    grow = FALSE,
    reflow = TRUE,
    size = 6,           # 明显小于国家名:地区是背景信息,不是阅读的第一层
    min.size = 4.8,
    fontface = "bold",
    padding.x = grid::unit(2.2, "mm"),
    padding.y = grid::unit(1.8, "mm"),
    start = "topleft"
  ) +
  scale_fill_manual(values = region_colors, guide = "none") +
  labs(
    title = "The same economies, nested by region",
    subtitle = "Fine outlines group countries by World Bank region",
    caption = "Source: World Bank via Tessera - current US$"
  ) +
  theme_void(base_size = 12) +
  theme(
    plot.title = element_text(
      colour = INK, size = 15, face = "bold", hjust = 0,
      margin = margin(b = 4)
    ),
    plot.subtitle = element_text(
      colour = INK, size = 10.5, hjust = 0,
      margin = margin(b = 12)
    ),
    plot.caption = element_text(colour = INK, size = 8.5, hjust = 1),
    plot.margin = margin(16, 18, 12, 18),
    plot.background = element_rect(fill = PAPER, colour = NA),
    panel.background = element_rect(fill = PAPER, colour = NA)
  )
treemap — grouped
treemap-grouped

Constraints

  • 面积不适合精确比较。 这是视觉编码本身的限制,换布局算法也解决不了。需要精确排序或比较接近的两个值,改用排序条形图。
  • 数值必须非负且可相加。 比率、均值和负数切不出面积;这里用 GDP 总量,不是人均 GDP。
  • 矩形的位置不是编码。 数据、排序或画布比例一变,布局就重排;正文里不能写"左上角那块"。
  • 标签取决于最终画布尺寸。 min.size 以 pt 计,导出尺寸一缩,小块的标签会被自动省略——图还是完整的,只是名字没了。

两个 recipe 怎么比

  1. 要快速比较国家份额 → recipe 1。只保留一层面积关系,空间最干净。
  2. 地区总量本身也是结论 → recipe 2。先形成地区容器,再在容器内切国家,层级才是数据结构而不是装饰。
  3. 需要精确排序或比较接近值 → 两个都别用,换排序条形图。
运行环境5 个包 · 2026-09-02T12:59:24.924+0800

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

  • biopalette 0.2.2
  • dplyr 1.2.1
  • ggplot2 4.0.3
  • grid 4.5.1
  • treemapify 2.5.6