如何处理离散轴上ggplot2和重叠标签

9

ggplot2似乎没有内置的处理散点图上文字重叠的方法。但是,我有一个不同的情况,标签是离散轴上的标签,我想知道这里是否有比我一直在做的更好的解决方案。

一些示例代码:

library(ggplot2)

#some example data
test.data = data.frame(text = c("A full commitment's what I'm thinking of",
                                "History quickly crashing through your veins",
                                "And I take A deep breath and I get real high",
                                "And again, the Internet is not something that you just dump something on. It's not a big truck."),
                       mean = c(3.5, 3, 5, 4),
                       CI.lower = c(4, 3.5, 5.5, 4.5),
                       CI.upper = c(3, 2.5, 4.5, 3.5))

#plot
ggplot(test.data, aes_string(x = "text", y = "mean")) +
  geom_point(stat="identity") +
  geom_errorbar(aes(ymax = CI.upper, ymin = CI.lower), width = .1) +
  scale_x_discrete(labels = test.data$text, name = "")

输入图片描述

我们可以看到x轴标签是重叠的。有两种解决方案:1)缩写标签,2)在标签中添加换行符。在许多情况下,(1)就可以解决问题,但在某些情况下可能无法实现。因此,我编写了一个函数,用于将换行符(\n)添加到字符串的每n个字符,以避免名称重叠:

library(ggplot2)

#Inserts newlines into strings every N interval
new_lines_adder = function(test.string, interval){
  #length of str
  string.length = nchar(test.string)
  #split by N char intervals
  split.starts = seq(1,string.length,interval)
  split.ends = c(split.starts[-1]-1,nchar(test.string))
  #split it
  test.string = substring(test.string, split.starts, split.ends)
  #put it back together with newlines
  test.string = paste0(test.string,collapse = "\n")
  return(test.string)
}

#a user-level wrapper that also works on character vectors, data.frames, matrices and factors
add_newlines = function(x, interval) {
  if (class(x) == "data.frame" | class(x) == "matrix" | class(x) == "factor") {
    x = as.vector(x)
  }

  if (length(x) == 1) {
    return(new_lines_adder(x, interval))
  } else {
    t = sapply(x, FUN = new_lines_adder, interval = interval) #apply splitter to each
    names(t) = NULL #remove names
    return(t)
  }
}

#plot again
ggplot(test.data, aes_string(x = "text", y = "mean")) +
  geom_point(stat="identity") +
  geom_errorbar(aes(ymax = CI.upper, ymin = CI.lower), width = .1) +
  scale_x_discrete(labels = add_newlines(test.data$text, 20), name = "")

输出结果如下:

enter image description here

然后,您可以花一些时间调整间隔大小,以避免标签之间有太多的空白。

如果标签数量不同,则这种解决方案并不是很好,因为最佳间隔大小会改变。此外,由于正常字体不是等宽字体,标签文本也会影响宽度,因此必须特别注意选择一个好的间隔(可以通过使用等宽字体来避免这个问题,但它们的宽度更宽)。最后,new_lines_adder()函数很愚蠢,它会将单词分成两个部分,这是人类不会做的傻瓜式拆分。例如,在上面的例子中,它将“breath”拆分为“br\nreath”。可以重新编写它以避免这个问题。

您还可以减小字体大小,但这是可读性和易读性之间的权衡,通常减小字体大小是不必要的。

如何处理这种标签重叠的最佳方法是什么?


我通常通过旋转标签来处理重叠的标签:+ theme(axis.text.x = element_text(angle = 60, hjust = 1))(但如果它们非常长,这并不理想,因为它会产生大的边距)。 - scoa
2个回答

4

我尝试创建一个不同版本的new_lines_adder

new_lines_adder = function(test.string, interval) {
   #split at spaces
   string.split = strsplit(test.string," ")[[1]]
   # get length of snippets, add one for space
   lens <- nchar(string.split) + 1
   # now the trick: split the text into lines with
   # length of at most interval + 1 (including the spaces)
   lines <- cumsum(lens) %/% (interval + 1)
   # construct the lines
   test.lines <- tapply(string.split,lines,function(line)
      paste0(paste(line,collapse=" "),"\n"),simplify = TRUE)
   # put everything into a single string
   result <- paste(test.lines,collapse="")
   return(result)
}

它仅在空格处分割行,并确保每行最多包含interval个字符。这样,您的图表如下所示:

enter image description here

我不会说这是最好的方法。它仍然忽略了并非所有字符具有相同的宽度。也许可以使用strwidth实现更好的效果。

顺便说一句:您可以将add_newlines简化为以下内容:

add_newlines = function(x, interval) {

   # make sure, x is a character array   
   x = as.character(x)
   # apply splitter to each
   t = sapply(x, FUN = new_lines_adder, interval = interval,USE.NAMES=FALSE)
   return(t)
}

起初,as.character 确保您拥有一个字符串。如果您已经拥有一个字符串,则这样做也没有任何问题,因此不需要 if 语句。
另外,下一个 if 语句是不必要的:如果 x 只包含一个元素,则 sapply 的工作非常完美。通过设置 USE.NAMES=FALSE,您可以禁止使用名称,这样您就不需要在额外的一行中删除名称。

适当的数字似乎在72左右。 - CoderGuy123
1
我不确定我理解你的意思。72(字符)是所有标签在一起应该具有的总宽度吗?到目前为止,您已经使用了4 * 20 = 80,这似乎是合理的。当然,您可以重写add_newlines,使其获取所有标签的总长度,然后将此数字除以标签数。因此,您将调用add_newlines(test.data$text,80),然后四次调用new_lines_adder(x,80/4) - Stibu
这是因为我使用数字(123456789)来估算数量,而数字比字母更宽(例如etaoinshr [英语中最常见的9个字母),所以结果会略小。在new_lines_adder()中添加自动处理组数的好主意。我会尝试这种方法。人们还可以将add_newlines()的默认值设置为80,因为这不应该在图之间变化(希望如此!)。 - CoderGuy123

0

在 @Stibu 的回答和评论的基础上,这个解决方案考虑了组数,并使用了由 Stibu 开发的智能分割,同时为斜杠分隔的单词添加了修复。

功能:

#Inserts newlines into strings every N interval
new_lines_adder = function(x, interval) {
  #add spaces after /
  x = str_replace_all(x, "/", "/ ")
  #split at spaces
  x.split = strsplit(x, " ")[[1]]
  # get length of snippets, add one for space
  lens <- nchar(x.split) + 1
  # now the trick: split the text into lines with
  # length of at most interval + 1 (including the spaces)
  lines <- cumsum(lens) %/% (interval + 1)
  # construct the lines
  x.lines <- tapply(x.split, lines, function(line)
    paste0(paste(line, collapse=" "), "\n"), simplify = TRUE)
  # put everything into a single string
  result <- paste(x.lines, collapse="")
  #remove spaces we added after /
  result = str_replace_all(result, "/ ", "/")
  return(result)
}

#wrapper for the above, meant for users
add_newlines = function(x, total.length = 85) {
  # make sure, x is a character array   
  x = as.character(x)
  #determine number of groups
  groups = length(x)
  # apply splitter to each
  t = sapply(x, FUN = new_lines_adder, interval = round(total.length/groups), USE.NAMES=FALSE)
  return(t)
}

我尝试了一些默认输入的值,发现当数值为85时,对于示例数据文本的输出效果比较好。如果再高,标签2中的"veins"会被上移并过于靠近第三个标签。

以下是它的样子:

enter image description here

然而,最好使用实际的文本总宽度来衡量,而不是字符数,因为必须依靠这个代理通常意味着标签浪费了很多空间。也许可以通过一些基于 strwidth 的代码重写 new_lines_adder() 来处理字符宽度不均的问题。

我将此问题保留未答复,以防有人能够找到解决方法。

我已将这两个函数添加到 我的 Github 个人包 中,因此任何想要使用它们的人都可以从那里获取。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接