8.ggplot2——图形注释

8 注释

在构建数据可视化时,通常需要对显示的数据进行注释。从概念上讲,注释为图提供元数据:也就是说,它提供关于所显示数据的附加信息。然而,从实际的角度来看,元数据只是数据的另一种形式。因此,ggplot2中的注释工具重用了用于创建其他绘图的相同geoms。然而,为了满足用户在注释图时通常具有的特定需求,ggplot2本身中有一些辅助函数,以ggplot2方式扩展的许多其他包以可能对您有帮助。

8.1 绘图和坐标轴标题

自定义绘图时,修改图形、轴和图例相关的标题通常是有用的。为了帮助完成这项任务,ggplot2 提供了labs()辅助函数,它允许您使用名称-值对来设置各种标题,例如title = My plot title",x = "X axis"fill = "fill legend"

ggplot(mpg, aes(displ, hwy)) + 
  geom_point(aes(colour = factor(cyl))) + 
  labs(
    x = "Engine displacement (litres)", 
    y = "Highway miles per gallon", 
    colour = "Number of cylinders",
    title = "Mileage by engine size and cylinders",
    subtitle = "Source: http://fueleconomy.gov"
  )
image

提供给labs()的值通常是文本字符串,其中\n用于指定换行符,但您也可以用quote()包装数学表达式。可以通过键入?plotmath找到这些表达式的使用规则。

values <- seq(from = -2, to = 2, by = .01)
df <- data.frame(x = values, y = values ^ 3)
ggplot(df, aes(x, y)) + 
  geom_path() + 
  labs(y = quote(f(x) == x^3))
image

在 ggtext 包和 ggplot2 主题系统(参见polishing)的帮助下,还可以在axislegend标题中包含(一些)降价。要启用markdown,需要将相关主题元素设置为ggtext::element_markdown(),如下所示:

df <- data.frame(x = 1:3, y = 1:3)
base <- ggplot(df, aes(x, y)) + 
  geom_point() + 
  labs(x = "Axis title with *italics* and **boldface**")

base 
base + theme(axis.title.x = ggtext::element_markdown())
image
image

有两种方法可以删除轴标签:

  • 设置labs(x = "")省略标签但仍分配空间;
  • 设置labs(x = NULL)删除标签及其空间。

8.2 文本标签

向图中添加文本是最常见的注释形式之一。大多数图不会从图中的每个观察中添加文本,但标记异常值和其他关键点非常有用。但是,由于 R 处理字体的方式,文本注释可能很棘手。ggplot2 包并没有提供所有的答案,但它确实提供了一些工具来让您的生活更轻松一些。标记图的主要工具是geom_text(),它在指定的xy位置添加label文本。geom_text()拥有所有几何图形中最美观的,因为有很多方法可以控制文本的外观:

  • family属性提供了字体的名称。这种图形属性允许您使用系统字体的名称,但需要小心。只有三种字体可以保证在任何地方都能使用:“sans”(默认字体)、“serif”或“mono”。如下所示:

    df <- data.frame(x = 1, y = 3:1, family = c("sans", "serif", "mono"))
    ggplot(df, aes(x, y)) + 
      geom_text(aes(label = family, family = family))
    
    image

    在绘图中使用系统字体可能很棘手,原因是每个图形设备 (GD) 对文本绘制的处理方式不同。有两组GDs:屏幕设备,如windows()(适用于windows)、quartz()(适用于mac)、x11()(主要适用于Linux)和RStudioGD()(在RStudio中)将图形绘制到屏幕上,而文件设备,如png()pdf()将图形写入文件。不幸的是,这些设备不会以相同的方式指定字体,所以如果你想让字体在任何地方都可以工作,你需要以不同的方式配置设备。以下两个包简化了这一问题:

    这两种方法各有利弊,因此您需要尝试这两种方法,看看哪种方法最适合您的需求。

  • fontface属性指定样式,有3个值:“plain”(默认),“bold”或“italic”。例如:

    df <- data.frame(x = 1, y = 3:1, face = c("plain", "bold", "italic"))
    ggplot(df, aes(x, y)) + 
      geom_text(aes(label = face, fontface = face))
    
    image
  • 您可以使用hjust(“left”, “center”, “right”, “inward”, “outward”) 和vjust(“bottom”, “middle”, “top”, “inward”, “向外”)属性来调整文本的对齐方式。默认情况下,对齐是居中的,但通常有更好的方式来代替。最有用的对齐方式之一是“向内”。它将文本向绘图中间对齐,以确保标签保持在绘图限制内:

    df <- data.frame(
      x = c(1, 1, 2, 2, 1.5),
      y = c(1, 2, 1, 2, 1.5),
      text = c(
        "bottom-left", "bottom-right", 
        "top-left", "top-right", "center"
      )
    )
    ggplot(df, aes(x, y)) +
      geom_text(aes(label = text))
    ggplot(df, aes(x, y)) +
      geom_text(aes(label = text), vjust = "inward", hjust = "inward")
    
    image
    image
  • size属性控制字体大小。与大多数工具不同,ggplot2 以毫米 (mm) 为单位指定大小,而不是通常的点 (pts)。这种选择的原因是为了让它的单位与ggplot2保持一致。(每英寸有72.27pts,所以要将点转换为毫米,只需乘以72.27 / 25.4)。

  • angle 指定文本的旋转角度。

ggplot2 包确实允许您将数据值映射到geom_text()所使用的图形属性,但您应该克制使用:很难理解映射到这些图形属性的变量之间的关系,而且这样做很少有用。

除了各种图形属性之外,geom_text()还有三个参数可以指定。与图形属性不同,它们只接受单个值,所以它们必须对所有标签都是相同的:

  • 通常你想在图形上标注现有的点,但你不希望文本与点(或条形图等)重叠。在这种情况下,稍微偏移文本是有用的,你可以使用nudge_xnudge_y参数:

    df <- data.frame(trt = c("a", "b", "c"), resp = c(1.2, 3.4, 2.5))
    ggplot(df, aes(resp, trt)) + 
      geom_point() + 
      geom_text(aes(label = paste0("(", resp, ")")), nudge_y = -0.25) + 
      xlim(1, 3.6)
    
    image

    (请注意,我手动调整了 x 轴限制以确保所有文本都适合绘图。)

  • 第三个参数是check_overlap。如果check_overlap = TRUE,重叠标签将自动从图中删除。算法很简单:标签按照它们在数据框中出现的顺序绘制;如果标签与现有点重叠,则将其省略。

    ggplot(mpg, aes(displ, hwy)) + 
      geom_text(aes(label = model)) + 
      xlim(1, 8)
    ggplot(mpg, aes(displ, hwy)) + 
      geom_text(aes(label = model), check_overlap = TRUE) + 
      xlim(1, 8)
    
    image
image.png
乍一看,这个功能似乎不是很有用,但算法的简单性却派上了用场。如果您按优先级对输入数据进行排序,则结果是带有强调重要数据点标签的图。

geom_text()的一个变体是geom_label():它在文本后面绘制一个圆角矩形。这对于向背景繁忙的绘图添加标签非常有用:

label <- data.frame(
  waiting = c(55, 80), 
  eruptions = c(2, 4.3), 
  label = c("peak one", "peak two")
)

ggplot(faithfuld, aes(waiting, eruptions)) +
  geom_tile(aes(fill = density)) + 
  geom_label(data = label, aes(label = label))
image

要想很好地标记数据仍然有一些挑战:

  • 文本不影响绘图的限制。不幸的是,由于标签具有绝对大小(例如 3 厘米),而不管绘图的大小,因此无法进行此操作。这意味着图的限制需要根据图的大小而不同——ggplot2 无法做到这一点。相反,你需要根据您的数据和绘图尺寸调整xlim()ylim()

  • 如果要标注很多点,就很难避免重叠。 check_overlap = TRUE很有用,但几乎无法控制删除哪些标签。解决此问题的一种常用的方法是使用Kamil Slowikowski的 ggrepel 包https://github.com/slowkow/ggrepel。该包提供geom_text_repel(),它优化标签定位以避免重叠。只要标签数量不多,它的效果就很好:

    mini_mpg <- mpg[sample(nrow(mpg), 20),]
    ggplot(mpg, aes(displ, hwy)) + geom_point(colour = "red") + 
    ggrepel::geom_text_repel(data = mini_mpg, aes(label = class))
    #> Warning: ggrepel: 8 unlabeled data points (too many overlaps). Consider
    #> increasing max.overlaps
    
    image
  • 有时很难确保文本标签适合您想要的空间。Claus Wilke的 ggfittext 包https://github.com/wilkox/ggfittext包含了一些有用工具,包括允许您在条形图中的列内放置文本标签函数。

8.3 构建自定义注释

用文本标记单个点是一种重要的注释,但它不是唯一有用的方法。ggplot2 包提供了其他几个工具来使用与显示数据相同的几何图形来注释绘图。例如,您可以使用:

通常,您可以将注释放在前面(alpha在需要时使用,以便您仍然可以看到数据)或在后面。在默认背景下,一条粗白线是一个有用的参考:它很容易看到,但不会跳到你身上。为了说明如何使用 ggplot2 工具来注释图表,我们将绘制美国失业率随时间变化的时间序列来理解:

ggplot(economics, aes(date, unemploy)) + 
  geom_line()
image

注释此图形的一种有用方法是使用阴影来指示当时哪位总统在位。为此,我们使用geom_rect()引入阴影、geom_vline()引入分隔符、geom_text()添加标签,然后使用geom_line()将数据覆盖在这些背景元素之上:

presidential <- subset(presidential, start > economics$date[1])

ggplot(economics) + 
  geom_rect(
    aes(xmin = start, xmax = end, fill = party), 
    ymin = -Inf, ymax = Inf, alpha = 0.2, 
    data = presidential
  ) + 
  geom_vline(
    aes(xintercept = as.numeric(start)), 
    data = presidential,
    colour = "grey50", alpha = 0.5
  ) + 
  geom_text(
    aes(x = start, y = 2500, label = name), 
    data = presidential, 
    size = 3, vjust = 0, hjust = 0, nudge_x = 50
  ) + 
  geom_line(aes(date, unemploy)) + 
  scale_fill_manual(values = c("blue", "red")) +
  xlab("date") + 
  ylab("unemployment")
image

请注意,这里没有什么新东西:在大多数情况下,注释图ggplot2是对现有几何图形的直接操作。也就是说,在这段代码中有一件特别的事情需要注意:使用-InfInf作为位置。这些是指图的顶部和底部(或左侧和右侧)限制。

这种方法也可以以其他方式使用。例如,您可以用它向绘图添加单个注释,但这有点繁琐,因为您必须创建一个单行数据框:

yrng <- range(economics$unemploy)
xrng <- range(economics$date)
caption <- paste(strwrap("Unemployment rates in the US have 
  varied a lot over the years", 40), collapse = "\n")

ggplot(economics, aes(date, unemploy)) + 
  geom_line() + 
  geom_text(
    aes(x, y, label = caption), 
    data = data.frame(x = xrng[1], y = yrng[2], caption = caption), 
    hjust = 0, vjust = 1, size = 4
  )

此代码有效,并生成所需的图,但非常麻烦。每次要添加单个注释时都必须这样做,过程会很烦人,因此 ggplot2 包的annotate()为您创建数据框的辅助函数:

ggplot(economics, aes(date, unemploy)) + 
  geom_line() + 
  annotate(
    geom = "text", x = xrng[1], y = yrng[2], 
    label = caption, hjust = 0, vjust = 1, size = 4
  )
image.png

annotate()函数的便利性在其他情况下会派上用场。例如,一种常见的注释形式是通过在主数据集下方以不同颜色绘制较大的点来突出显示点的子集。要突出显示"subaru"制造的车辆,您可以使用它来创建基本图:

p <- ggplot(mpg, aes(displ, hwy)) +
  geom_point(
    data = filter(mpg, manufacturer == "subaru"), 
    colour = "orange",
    size = 3
  ) +
  geom_point() 

这样做的问题是突出显示的类别不会被标记。使用annotate()很容易纠正。

p + 
  annotate(geom = "point", x = 5.5, y = 40, colour = "orange", size = 3) + 
  annotate(geom = "point", x = 5.5, y = 40) + 
  annotate(geom = "text", x = 5.6, y = 40, label = "subaru", hjust = "left")
image

这种方法的优点是在绘图区域内创建标签,但缺点是标签与它挑选的点相距较远(否则与标签相邻的橙色和黑色点可能会与真实数据混淆)。另一种方法是使用不同的 geom 来完成这项工作。geom_curve()geom_segment()可以用来绘制曲线和线连接点与标签,可以结合使用annotate()如下图所示:

p + 
  annotate(
    geom = "curve", x = 4, y = 35, xend = 2.65, yend = 27, 
    curvature = .3, arrow = arrow(length = unit(2, "mm"))
  ) +
  annotate(geom = "text", x = 4.1, y = 35, label = "subaru", hjust = "left")
image

8.4 直接标记

上面的 "subaru"图提供了“直接标记”的示例,其中绘图区域本身包含点的标签,而不是使用图例。这通常会使绘图更容易阅读,因为它使标签更接近数据。更广泛的 ggplot2 生态系统包含各种其他工具,可以以更自动化的方式完成此任务。由托比·迪伦·霍金 (Toby Dylan Hocking)提供的directlabels包有许多工具来简化这个过程:

ggplot(mpg, aes(displ, hwy, colour = class)) + 
  geom_point()

ggplot(mpg, aes(displ, hwy, colour = class)) + 
  geom_point(show.legend = FALSE) +
  directlabels::geom_dl(aes(label = class), method = "smart.grid")
image
image

Directlabels 提供了多种定位方法。smart.grid是散点图的合理起点,但还有其他方法对频率多边形和线图更有用。有关其他技术,请参阅 directlabels 网站http://directlabels.r-forge.r-project.org

另一个想法来自 Thomas Lin Pedersen https://github.com/thomasp85/ggforce的 ggforce 包。ggforce 包包含许多有用的工具来扩展 ggplot2 功能,包括geom_mark_ellipse()使用圆形“突出显示”标记覆盖绘图等功能。例如:

ggplot(mpg, aes(displ, hwy)) +
  geom_point() + 
  ggforce::geom_mark_ellipse(aes(label = cyl, group = cyl))
image.png

由 Hiroaki Yutani https://github.com/yutannihilation/gghighlight在 gghighlight 包中提供了第三种直接标记方法。在许多情况下,对于突出图中的点或线(或实际上是各种不同的几何图形)很有用,特别是对于纵向数据:

data(Oxboys, package = "nlme")
ggplot(Oxboys, aes(age, height, group = Subject)) + 
  geom_line() + 
  geom_point() + 
  gghighlight::gghighlight(Subject %in% 1:3)
#> Warning: Tried to calculate with group_by(), but the calculation failed.
#> Falling back to ungrouped filter operation...

#> Warning: Tried to calculate with group_by(), but the calculation failed.
#> Falling back to ungrouped filter operation...
#> label_key: Subject
image

8.5 跨分面注释

如果使用得当,注释可以成为帮助读者理解数据的强大工具。一个例子是当您希望读者跨分面比较组时。例如,在下面的图中,很容易看到每个分面内的关系,但分面之间的细微差异不会突然出现:

ggplot(diamonds, aes(log10(carat), log10(price))) + 
  geom_bin2d() + 
  facet_wrap(vars(cut), nrow = 1)
image.png

如果我们添加一条参考线,就更容易看到这些细微的差异:

mod_coef <- coef(lm(log10(price) ~ log10(carat), data = diamonds))
ggplot(diamonds, aes(log10(carat), log10(price))) + 
  geom_bin2d() + 
  geom_abline(intercept = mod_coef[1], slope = mod_coef[2], 
    colour = "white", size = 1) + 
  facet_wrap(vars(cut), nrow = 1)
image

在此图中,每个分面都使用同一条回归线显示一个类别的数据。由于有共享参考线来辅助比较,因此可以更轻松地将各个分面相互比较。

当您希望绘图的每个分面都显示来自单个组的数据时,会出现此主题的变化,并在每个面板中不显眼地绘制完整的数据集以帮助进行视觉比较。gghighlight 包在这种情况下特别有用:

ggplot(mpg, aes(displ, hwy, colour = factor(cyl))) +
  geom_point() + 
  gghighlight::gghighlight() + 
  facet_wrap(vars(cyl))
image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容