【问题标题】:How to deal with ggplot2 and overlapping labels on a discrete axis如何处理 ggplot2 和离散轴上的重叠标签
【发布时间】:2015-08-16 09:06:18
【问题描述】:

ggplot2 似乎没有内置的方法来处理scatter plots 上的文本过度绘图。但是,我有另一种情况,标签是离散轴上的标签,我想知道这里是否有人有比我一直在做的更好的解决方案。

一些示例代码:

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 = "")

输出是:

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

如果标签数量不同,这种解决方案就不是很好,因为最佳区间大小会发生变化。此外,由于普通字体不是等宽字体,标签的文本也会影响宽度,因此在选择一个好的间隔时必须格外小心(可以通过使用等宽字体来避免这种情况,但它们特别宽)。最后,new_lines_adder() 函数很愚蠢,因为它会以人类不会做的愚蠢方式将单词分成两个。例如。在上面它将“呼吸”分成“br\nreath”。可以重写它来避免这个问题。

也可以减小字体大小,但这是对可读性的折衷,而且通常没有必要减小字体大小。

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

【问题讨论】:

  • 我通常通过旋转它们来处理重叠标签:+ theme(axis.text.x = element_text(angle = 60, hjust = 1))(但如果它们很长则不理想,因为它会产生很大的边距)

标签: r plot ggplot2 axis-labels


【解决方案1】:

我尝试将不同版本的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 给出的字符数。有了这个,你的情节如下所示:

我不会声称这是最好的方法。它仍然忽略了并非所有字符都具有相同的宽度。也许使用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 左右。
  • 我不确定我理解你的意思。所有标签加在一起的总宽度是 72(字符)吗?到现在为止,您使用的是 4*20 = 80,这似乎是合理的。当然,你可以做的是重写add_newlines,这样它就需要所有标签的总长度,然后用这个数字除以标签的数量。所以你会打电话给add_newlines(test.data$text,80),然后会打电话四次new_lines_adder(x,80/4)
  • 这是因为我用数字(123456789)来估计数字,数字比字母宽(例如etaoinshr [英语中最常见的9个字母]),所以结果有点小。在new_lines_adder() 中添加自动处理组数的好主意。我会尝试这种方法。也可以将add_newlines() 的默认值设置为 80,因为这不应该在不同的地块之间有所不同(我希望!)。
【解决方案2】:

在@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 中的任何更高的“脉络”都会向上移动,并且离第三个标签太近。

它的外观如下:

不过,最好使用总文本宽度的实际度量,而不是字符数,因为必须依赖此代理通常意味着标签会浪费大量空间。或许可以在strwidth的基础上用一些代码重写new_lines_adder()来解决字符宽度不等的问题。

如果有人能找到解决方法,我将不回答这个问题。

我已将这两个函数添加到my personal package on github,因此任何想使用它们的人都可以从那里获取它们。

【讨论】:

    猜你喜欢
    • 2019-10-26
    • 1970-01-01
    • 2016-06-05
    • 1970-01-01
    • 2015-02-11
    • 2017-03-01
    • 2015-03-08
    • 2018-01-03
    相关资源
    最近更新 更多