ggplot2 有没有办法将文本放置在弯曲的路径上?

byt*_*101 41 visualization r data-visualization ggplot2

有没有办法在 ggplot2 中沿着密度线放置文本,或者就此而言,沿着任何路径放置文本?我的意思是一次作为标签,采用这种 xkcd 风格:18351950(中间面板)、13922234(中间面板)。或者,有没有办法让该行重复文本,例如xkcd #930?我对所有 xkcd 表示歉意,我不确定这些样式叫什么,这是我能想到的唯一一个我以前见过的以这种方式区分区域的地方。

注意:我不是在谈论手绘的 xkcd 风格,也不是在顶部放置平面标签

我知道我可以放置一段笔直/平坦的文本,例如 viaannotategeom_text,但我很好奇弯曲此类文本,使其看起来沿着数据的曲线。

我也很好奇这种文本风格有名字吗?

ggplot2 图表示例使用annotate(...)

带有水平文本的 ggplot 图表

上面的示例图在 Inkscape 中用弯曲文本修改:

第一张图的修改版本,其中文本遵循弯曲路径


编辑:根据要求,这是 3 月和 4 月前两次试运行的数据:

df <- data.frame(
  monthly_run = c('March', 'March', 'March', 'March', 'March', 'March', 'March', 
                  'March', 'March', 'March', 'March', 'March', 'March', 'March', 
                  'April', 'April', 'April', 'April', 'April', 'April', 'April', 
                  'April', 'April', 'April', 'April', 'April', 'April', 'April'),
  duration    = c(36, 44, 45, 48, 50, 50, 51, 54, 55, 57, 60, 60, 60, 60, 30,
                  40, 44, 47, 47, 47, 53, 53, 54, 55, 56, 57, 69, 77)
  )

ggplot(df, aes(x = duration, group = monthly_run, color = monthly_run)) + 
  geom_density() + 
  theme_minimal()`
Run Code Online (Sandbox Code Playgroud)

All*_*ron 62

很好的问题。我经常思考这个问题。我不知道有什么软件包本身允许它,但自己做并不是非常困难,因为它geom_text被接受angle为一种美学映射。

假设我们有以下情节:

library(ggplot2)

df <- data.frame(y = sin(seq(0, pi, length.out = 100)),
                 x = seq(0, pi, length.out = 100))

p <- ggplot(df, aes(x, y)) + 
  geom_line() + 
  coord_equal() +
  theme_bw()

p
Run Code Online (Sandbox Code Playgroud)

我们想要沿着它运行以下标签:

label <- "PIRATES VS NINJAS"
Run Code Online (Sandbox Code Playgroud)

我们可以将标签拆分为字符:

label <- strsplit(label, "")[[1]]
Run Code Online (Sandbox Code Playgroud)

现在是棘手的部分。我们需要沿着路径均匀地间隔字母,这需要计算出实现此目的的 x 坐标。我们这里需要几个辅助函数:

label <- strsplit(label, "")[[1]]
Run Code Online (Sandbox Code Playgroud)

这些使我们能够创建一个由字母、坐标和角度组成的小数据框来绘制字母:

next_x_along_sine <- function(x, d)
{
  y <- sin(x)
  uniroot(f = \(b) b^2 + (sin(x + b) - y)^2 - d^2, c(0, 2*pi))$root + x
}
  
x_along_sine <- function(x1, d, n)
{
  while(length(x1) < n) x1 <- c(x1, next_x_along_sine(x1[length(x1)], d))
  x1
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以用普通的 old 进行绘图geom_text

p + geom_text(aes(y = y + 0.1, label = label, angle = angle), data = df2,
              vjust = 1, size = 4, fontface = "bold")
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

或者,如果我们想用文本替换部分行:

df2 <- as.data.frame(approx(df$x, df$y,  x_along_sine(1, 1/13, length(label))))
df2$label <- label
df2$angle <- atan(cos(df2$x)) * 180/pi
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

或者,进行一些主题调整:

在此输入图像描述


附录

实际上,我可能不会抽出时间来编写一个geom_textpath包,但我认为展示可能适用于按照OP示例标记密度曲线的方法会很有用。它需要以下一组功能:

p + geom_text(aes(y = y + 0.1, label = label, angle = angle), data = df2,
              vjust = 1, size = 4, fontface = "bold")
Run Code Online (Sandbox Code Playgroud)

定义了这些函数后,我们现在可以执行以下操作:

df$col <- cut(df$x, c(-1, 0.95, 2.24, 5), c("black", "white", "#000000"))

ggplot(df, aes(x, y)) + 
  geom_line(aes(color = col, group = col)) + 
  geom_text(aes(label = label, angle = angle), data = df2,
            size = 4, fontface = "bold") +
  scale_color_identity() +
  coord_equal() +
  theme_bw()
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

  • @teunbrand 我实际上超前了,根据这里的答案制作了一个 `geom_textpath` 包。你可以通过 `remotes::install_github("AllanCameron/geomtextpath")` 安装它(它通过了 CRAN 检查),并查看 `?geom_textpath` 下的示例。它已经可以做一些非常巧妙的事情了,尽管我几天前才开始使用它。如果您愿意,我很高兴您将其采用到 ggh4x 中 - 虽然我确实考虑过扩展它以涵盖其他一些用例,但实际上还不足以证明它自己的软件包的合理性。 (12认同)
  • @tjebo 和 Allan Cameron 是的,我看到了 textpath/pathtext grob 的潜力(这也是我问[这个问题](/sf/ask/4895486431/)的原因方向),Allan Cameron 再次没有让人失望!)如果您对 geom 层应该能够做什么有想法,您可以在[此处](https://github.com/teunbrand/ggh4x/issues)留下建议/58)。 (2认同)
  • 这太棒了,干得好!我见过功能较少的软件包,所以它可能是合理的:) (2认同)

All*_*ron 19

最后,这个问题促使我和 Teun van den Brand (@teunbrand) 开发了这个geomtextpath软件包,该软件包现已发布在 CRAN 上。

所以现在这个问题可以更直接、更简单地回答:

library(geomtextpath)

ggplot(df, aes(x = duration, color = monthly_run)) + 
  geom_textdensity(aes(label = monthly_run, hjust = monthly_run,
                       vjust = monthly_run), size = 6) +
  scale_hjust_manual(values = c(0.4, 0.55)) +
  scale_vjust_manual(values = c(1.1, -0.2)) +
  scale_y_continuous(limits = c(0, 0.06)) +
  theme_minimal() +
  theme(legend.position = "none")
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

  • 杰出的!尽管我可能会为包选择不同的名称,例如“ggtextpath”:) (2认同)
  • @tjebo 谢谢。这个名字是通过这个问题产生的,一开始似乎很合适。随着范围的扩大,这种情况已经变得不那么严重了,但到那时改变它已经太晚了。呃,好吧! (2认同)