Folioevanzhou.org
← 返回 Tessera
R4 张图2026-08-11

multi_line

Several series traced along one ordered axis, each keeping its colour so a reader can follow it end to end.

示例数据
orange
配色
walter_white2gene_red
语言
R
成图预览另有 3 张在配方里
multi_line

Introduction

折线图读的是沿一个有序轴的变化:时间、年龄、剂量、距离或步骤。横轴必须有真实顺序——如果 x 只是几个没有先后的类别,把点连起来会凭空制造出一个"过程"。

单条线看变化的方向和速度;多条线则看各组是否一起动、哪一组更快、何时开始分离,以及轨迹有没有交叉。

读的时候按这个顺序:

  1. 先沿横轴追方向。 上升、下降、平台或拐点出现在哪里。
  2. 再比较斜率。 同一段时间内哪条线变化更快。
  3. 看间距。 线之间逐渐拉开说明组间差异随时间扩大,逐渐靠拢则相反。
  4. 找交叉。 两条线交换高低顺序时,单独比较起点或终点都会漏掉信息。
  5. 确认点的位置。 线段之间没有真实观测;折线只是连接相邻点,不代表中间过程真的线性。

Example Data

折线图要的输入是:时间或有序位置 + 连续数值,多序列时再加 1 个分组变量。x 必须能排序——Date、数值或有序 factor 都行,字符串不行。

orange 是五棵橘树在同样的七个年龄点测得的树干周长,标准的多序列纵向数据。

library(ggplot2)
library(ggpubr)
library(biopalette)

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

# Tree 是分组不是数值,转成 factor 才不会被当成连续量做渐变色标
d$Tree <- factor(d$Tree, levels = sort(unique(d$Tree)))

Palettes

五棵树是五个无序类别,完整分组版用五色定性色板 walter_white2。颜色沿整条线保持不变,读者才能从左到右持续追踪同一棵树。

色板少于五色时,不插值、不循环,也不删掉其余轨迹:所有线先用浅灰画成上下文,再把与色板容量相同的重点序列覆盖在上面。浅灰表示"仍在图中,但当前不由色板编码",不是第六个类别。

一条线只用颜色区分就够。颜色、点形、虚实一起变化会让图例和图面同时过载——常规彩色图先只用颜色,黑白输出再考虑加第二个通道。

tree_colors <- setNames(
  get_palette("walter_white2", type = "qualitative"),
  levels(d$Tree)
)

# 四张图共用同一套坐标范围和版式:轴范围一致,四张图才能横着比
line_scales <- list(
  scale_x_continuous(
    breaks = c(0, 400, 800, 1200, 1600),
    limits = c(0, 1650),
    expand = expansion(mult = c(0, 0.01))
  ),
  scale_y_continuous(
    breaks = seq(25, 225, 50),
    limits = c(20, 225),
    expand = expansion(mult = c(0, 0))
  )
)

line_theme <- theme_pubr(base_size = 13, legend = "right") +
  theme(
    panel.grid.major = element_line(color = "#E5E3DC", linewidth = 0.35),
    panel.grid.minor = element_blank(),
    axis.line = element_line(color = "#333330", linewidth = 0.45),
    axis.ticks = element_line(color = "#333330", linewidth = 0.4),
    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.key.height = grid::unit(5, "mm"),
    plot.margin = margin(14, 16, 12, 12)
  )

Recipe

No. Method Input Data Palettes
1 ggplot2 one_tree —
2 ggplot2 d tree_colors
3 ggplot2 d focus_colors
4 ggplot2 d tree_colors

1 · 单条序列

线负责连接相邻年龄,点负责标出七次真实测量。

只画线的话,读者会把整条线当成连续测量。观测点不密时叠一层小圆点,才算诚实地告诉人数据在哪里。

#| fig: single
#| fig-width: 7.4
#| fig-height: 5.1
one_tree <- subset(d, Tree == "4")

ggplot(one_tree, aes(age, circumference)) +
  geom_line(linewidth = 1.05, color = "#5A5A56") +
  # 点比散点图小,也和线同色 —— 它只负责标位置,太抢会把线切成一串珠子
  geom_point(size = 2.4, color = "#5A5A56") +
  line_scales +
  labs(
    title = "One tree, seven observations",
    x = "Age (days)",
    y = "Trunk circumference (mm)"
  ) +
  line_theme
multi_line — single
multi_line-single

2 · 五条序列全部着色

#| fig: grouped
#| fig-width: 7.8
#| fig-height: 5.2
# color 和 group 都指向 Tree:group 漏了,ggplot 会把不同树的点按 age
# 连成一条来回折返的线,而且不报错
ggplot(d, aes(age, circumference, color = Tree, group = Tree)) +
  geom_line(linewidth = 1.05, alpha = 0.92) +
  geom_point(size = 2.35, alpha = 0.95) +   # 保持同一种圆形,不再加 shape
  scale_color_manual(values = tree_colors) +
  line_scales +
  labs(
    title = "Orange tree growth",
    subtitle = "Five trees measured at the same seven ages",
    x = "Age (days)",
    y = "Trunk circumference (mm)",
    color = "Tree"
  ) +
  line_theme
multi_line — grouped
multi_line-grouped

3 · 色板不足时突出部分序列

这里故意用只有两色的 gene_red。Tree 2 和 Tree 4 是当前重点,其余三棵仍以浅灰显示完整轨迹,但不进入颜色图例。

灰线应当刚好看得见:太深会和重点线竞争,太浅等于删掉数据。正式输出时要结合页面背景和最终缩放再检查一次。

#| fig: highlighted
#| fig-width: 7.8
#| fig-height: 5.2
focus_levels <- c("2", "4")

# 拆成两份数据分别画,而不是给五条线各自设色 ——
# 上下文那三条根本不该进 scale_color_manual(),否则图例会多出三项
context <- subset(d, !Tree %in% focus_levels)
focus <- subset(d, Tree %in% focus_levels)
focus$Tree <- factor(focus$Tree, levels = focus_levels)

focus_colors <- setNames(
  get_palette("gene_red", type = "qualitative"),
  focus_levels
)

ggplot() +
  # 先画上下文:后画的重点线才会压在上面
  geom_line(
    data = context,
    aes(age, circumference, group = Tree),
    color = "#D2D0C9",
    linewidth = 0.85
  ) +
  geom_point(
    data = context,
    aes(age, circumference),
    color = "#D2D0C9",
    size = 1.9
  ) +
  geom_line(
    data = focus,
    aes(age, circumference, color = Tree, group = Tree),
    linewidth = 1.2      # 比上下文粗一档,层次靠粗细和颜色一起给
  ) +
  geom_point(
    data = focus,
    aes(age, circumference, color = Tree),
    size = 2.55
  ) +
  scale_color_manual(values = focus_colors) +
  line_scales +
  labs(
    title = "Two highlighted, three kept as context",
    subtitle = "A two-color palette should encode two lines, not repeat across five",
    x = "Age (days)",
    y = "Trunk circumference (mm)",
    color = "Highlighted tree"
  ) +
  line_theme
multi_line — highlighted
multi_line-highlighted

4 · 平滑曲线,不显示观测点

geom_smooth() 在每棵树内部独立拟合 loess。这里关掉置信带,也不叠观测点,让画面只保留五条连续轨迹。

代价是读者看不出每棵树其实只有七次测量。loess 是局部估计,不经过每一个观测,还会在点与点之间给出看似连续的轨迹——它适合概括形态,不适合精确读数。要判断采样是否稀疏、某个拐点是不是由单个观测造成,回 recipe 2。

#| fig: smooth
#| fig-width: 7.8
#| fig-height: 5.2
ggplot(d, aes(age, circumference, color = Tree, group = Tree)) +
  geom_smooth(
    method = "loess",
    formula = y ~ x,
    se = FALSE,        # 每棵树只有七个点,置信带会宽到互相盖住
    span = 0.85,       # 越小越贴数据、越容易把噪声画成拐点
    linewidth = 1.15
  ) +
  scale_color_manual(values = tree_colors) +
  line_scales +
  labs(
    title = "Smoothed growth trajectories",
    subtitle = "Loess summarizes shape; the seven observation times are intentionally hidden",
    x = "Age (days)",
    y = "Trunk circumference (mm)",
    color = "Tree"
  ) +
  line_theme
multi_line — smooth
multi_line-smooth

Constraints

  • 横轴必须有真实顺序。 A / B / C 三组如果没有先后关系,换一下排列就会得到另一条"趋势"。那种数据该画点图或柱图。
  • geom_line() 按 x 排序,而字符串不是时间顺序。 日期写成 "2024-1-5"、月份写成 "Mar",排出来是字母序,线会来回穿。画之前先把 x 转成 Date、数值或有序 factor。
  • 漏了 group = 不会报错。 ggplot 会把不同序列的点按 x 连成一条来回折返的线——图照样画得出来,只是它什么也不表示。
  • 线段之间没有观测。 折线只是连接相邻点,中间那一段是画出来的,不是测出来的。
  • 细线在最终版面上会消失。 低饱和颜色缩进论文双栏后尤其明显。按导出尺寸检查线宽,不按预览窗口判断。

四个 recipe 怎么比

  1. 只有一条序列 → recipe 1。
  2. 序列数不超过定性色板容量 → recipe 2。
  3. 序列数超过可用颜色 → recipe 3。保留全部上下文,只突出当前真正需要比较的几条,不要循环用色。
  4. 只强调宏观形态、不要求读出真实测量位置 → recipe 4。
运行环境4 个包 · 2026-08-11T16:23:51.405+0800

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

  • biopalette 0.1.0
  • ggplot2 4.0.3
  • ggpubr 0.6.2
  • grid 4.5.1