ggplot2: geom_text用plot和force/fit text在geom_bar中调整大小

时间:2021-04-12 15:01:02

This is actually two questions in one (not sure if goes against SO rules, but anyway).

这实际上是一个问题中的两个问题(不确定是否违反了SO规则,但无论如何)。

First question is how can I force a geom_text to fit within a geom_bar? (dynamically according to the values plotted)

第一个问题是,如何强制使一个geom_text适合于一个geom_bar?(根据绘制的值动态)

Looking around, the solutions I found were changing the size of the label. That certainly works, but not for every case. You can change the size for a specific plot to make the text fit within the bar, but when the data changes, you may need to manually change the size of the text again. My real-life problem is that I need to generate the same plot for constantly changing data (daily), so I cannot really manually adjust the size for each plot.

环顾四周,我发现解决方案是改变标签的大小。这当然行得通,但并非适用于所有情况。您可以更改某个特定的图的大小,以使文本适合于bar,但是当数据发生变化时,您可能需要再次手动更改文本的大小。我的现实问题是,我需要为不断变化的数据生成相同的情节,所以我不能真的手动调整每个情节的大小。

I tried setting the size of the label as a function of the data. It kinda works, not perfectly, but works for many cases.

我尝试将标签的大小设置为数据的函数。它有点作用,不是很完美,但在很多情况下都有效。

But here's another problem, even when the label fits within the bar, resizing the plot messes everything up. Looking into it, I also found in the ggplot documentation that

但还有另一个问题,即使标签在条内,调整情节大小也会把一切搞砸。我还在ggplot文档中找到了它

labels do have height and width, but they are physical units, not data units. The amount of space they occupy on that plot is not constant in data units: when you resize a plot, labels stay the same size, but the size of the axes changes.

标签确实有高度和宽度,但它们是物理单位,而不是数据单位。它们在这个图上占用的空间在数据单元中不是恒定的:当您调整一个图的大小时,标签保持相同的大小,但是坐标轴的大小会改变。

Which takes me to my second question: Is it possible to change this default behaviour and let/make the labels resize with the plot?

这就引出了我的第二个问题:是否有可能改变这种默认行为,并让标签随情节调整大小?

And also let me refine my first question. Is it possible to force a geom_text to fit within a geom_bar, dynamically setting the size of the text using a clever relation between physical units and data units?

我还要完善我的第一个问题。是否有可能强制一个地图文在一个地栏内,动态地设置文本的大小,使用物理单位和数据单位之间的巧妙关系?

So, to follow good practice, here's my reproducible example:

因此,为了遵循良好的实践,我有一个可重复的例子:

set.seed(1234567)
data_gd <- data.frame(x = letters[1:5], 
                      y = runif(5, 100, 99999))

ggplot(data = data_gd,
       mapping = aes(x = x, y = y, fill = x)) +
    geom_bar(stat = "identity") +
    geom_text(mapping = aes(label = y, y = y/2))

This code produces this plot:

这个代码产生了这个图:

ggplot2: geom_text用plot和force/fit text在geom_bar中调整大小

If I simply resize the plot, "labels stay the same size, but the size of the axes changes" thereby making the labels fit into the bars (now perhaps labels are even too small).

如果我简单地调整图的大小,“标签保持相同的大小,但是坐标轴的大小改变了”,从而使标签适合于条形图(现在可能标签甚至太小了)。

ggplot2: geom_text用plot和force/fit text在geom_bar中调整大小

So, this is my second question. It would be nice that the labels resize as well and keep the aspect ration in relation to the bars. Any ideas how to accomplish this or if it is possible at all?

这是我的第二个问题。如果标签也能调整大小并保持尺寸与条形的比例,那就太好了。任何想法如何实现这一点,或者是否有可能实现?

Ok, but going back to how to fit the labels within the bars, the simplest solution is to set the size of the labels.

好的,但是回到如何在条形图中填充标签,最简单的解决方案是设置标签的大小。

ggplot(data = data_gd,
       mapping = aes(x = x, y = y, fill = x)) +
    geom_bar(stat = "identity") +
    geom_text(mapping = aes(label = y, y = y/2), size = 3)

Again, this works as shown below, but it is not maintainable /nor robust to changes in the data.

同样,这个工作如下所示,但是对于数据的更改,它是不可维护的/或健壮的。

ggplot2: geom_text用plot和force/fit text在geom_bar中调整大小

For example, the very same code to generate the plot with different data yields catastrophic results.

例如,生成具有不同数据的图的相同代码会产生灾难性的结果。

data_gd <- data.frame(x = letters[1:30], 
                      y = runif(30, 100, 99999))
ggplot(data = data_gd,
       mapping = aes(x = x, y = y, fill = x)) +
    geom_bar(stat = "identity") +
    geom_text(mapping = aes(label = y, y = y/2), size = 3)

ggplot2: geom_text用plot和force/fit text在geom_bar中调整大小

And I can go on with the examples, setting the size of the labels as a function of the number of categories on x-axis and so on. But you get the point, and perhaps one of you ggplot2 experts can give me ideas.

我可以继续举例,将标签的大小设置为x轴上类别数量的函数。但是你明白我的意思了,也许你们中有一个ggplot2专家可以给我一些建议。

2 个解决方案

#1


10  

one option might be to write a geom that uses a textGrob with a custom drawDetails method to fit within the allocated space, set by the bar width.

一个选项可能是编写一个geom,该geom使用带有自定义drawDetails方法的textGrob,以适应由bar宽度设置的分配空间。

library(grid)
library(ggplot2)

fitGrob <- function(label, x=0.5, y=0.5, width=1){
  grob(x=x, y=y, width=width, label=label, cl = "fit")
}
drawDetails.fit <- function(x, recording=FALSE){
  tw <- sapply(x$label, function(l) convertWidth(grobWidth(textGrob(l)), "native", valueOnly = TRUE))
  cex <- x$width / tw
  grid.text(x$label, x$x, x$y, gp=gpar(cex=cex), default.units = "native")
}


`%||%` <- ggplot2:::`%||%`

GeomFit <- ggproto("GeomFit", GeomRect,
                   required_aes = c("x", "label"),

                   setup_data = function(data, params) {
                     data$width <- data$width %||%
                       params$width %||% (resolution(data$x, FALSE) * 0.9)
                     transform(data,
                               ymin = pmin(y, 0), ymax = pmax(y, 0),
                               xmin = x - width / 2, xmax = x + width / 2, width = NULL
                     )
                   },
                   draw_panel = function(self, data, panel_scales, coord, width = NULL) {
                     bars <- ggproto_parent(GeomRect, self)$draw_panel(data, panel_scales, coord)
                     coords <- coord$transform(data, panel_scales)    
                     width <- abs(coords$xmax - coords$xmin)
                     tg <- fitGrob(label=coords$label, y = coords$y/2, x = coords$x, width = width)

                     grobTree(bars, tg)
                   }
)

geom_fit <- function(mapping = NULL, data = NULL,
                     stat = "count", position = "stack",
                     ...,
                     width = NULL,
                     binwidth = NULL,
                     na.rm = FALSE,
                     show.legend = NA,
                     inherit.aes = TRUE) {

  layer(
    data = data,
    mapping = mapping,
    stat = stat,
    geom = GeomFit,
    position = position,
    show.legend = show.legend,
    inherit.aes = inherit.aes,
    params = list(
      width = width,
      na.rm = na.rm,
      ...
    )
  )
}


set.seed(1234567)
data_gd <- data.frame(x = letters[1:5], 
                      y = runif(5, 100, 99999))

ggplot(data = data_gd,
       mapping = aes(x = x, y = y, fill = x, label=round(y))) +
  geom_fit(stat = "identity") +
  theme()

ggplot2: geom_text用plot和force/fit text在geom_bar中调整大小

#2


6  

If horizontal bar charts are OK, then the issue is not the size of the labels but the placement. My solution would be

如果水平条形图是可以的,那么问题不是标签的大小,而是位置。我的解决方案是

ggplot2: geom_text用plot和force/fit text在geom_bar中调整大小

created by this code:

由这段代码:

library(ggplot2)
data_gd <- data.frame(x = letters[1:26], 
                      y = runif(26, 100, 99999))
ymid <- mean(range(data_gd$y))
ggplot(data = data_gd,
       mapping = aes(x = x, y = y, fill = x)) +
  geom_bar(stat = "identity") +
  geom_text(mapping = aes(label = y, y = y, 
            hjust = ifelse(y < ymid, -0.1, 1.1)), size = 3) +
  coord_flip()

The trick is done in three steps:

诀窍在于三个步骤:

  1. coord_flip makes a horizontal bar chart.
  2. coord_flip制作一个水平条形图。
  3. The mapping in geom_text uses also hjust depending on the value of y. If the bar is shorter than half of the range of y, the text is printed outside of the bar (right to the y value). If the bar is longer than half of the range of y, the text is printed inside the bar (left to the y value). This makes sure that the text is always printed inside the plot area (if not too long at all).
  4. geom_text中的映射也使用hjust,这取决于y的值。如果该条长于y范围的一半,则文本将打印在该条内(左至y值)。这确保文本始终打印在情节区域内(如果不是太长的话)。
  5. I have added some additional space between the bar and the text. If you want the text to start or end directly at the y-value you can use hjust = ifelse(y < ymid, 0, 1)).
  6. 我在栏和文本之间增加了一些额外的空间。如果希望文本直接从y值开始或结束,可以使用hjust = ifelse(y < ymid, 0,1)。

#1


10  

one option might be to write a geom that uses a textGrob with a custom drawDetails method to fit within the allocated space, set by the bar width.

一个选项可能是编写一个geom,该geom使用带有自定义drawDetails方法的textGrob,以适应由bar宽度设置的分配空间。

library(grid)
library(ggplot2)

fitGrob <- function(label, x=0.5, y=0.5, width=1){
  grob(x=x, y=y, width=width, label=label, cl = "fit")
}
drawDetails.fit <- function(x, recording=FALSE){
  tw <- sapply(x$label, function(l) convertWidth(grobWidth(textGrob(l)), "native", valueOnly = TRUE))
  cex <- x$width / tw
  grid.text(x$label, x$x, x$y, gp=gpar(cex=cex), default.units = "native")
}


`%||%` <- ggplot2:::`%||%`

GeomFit <- ggproto("GeomFit", GeomRect,
                   required_aes = c("x", "label"),

                   setup_data = function(data, params) {
                     data$width <- data$width %||%
                       params$width %||% (resolution(data$x, FALSE) * 0.9)
                     transform(data,
                               ymin = pmin(y, 0), ymax = pmax(y, 0),
                               xmin = x - width / 2, xmax = x + width / 2, width = NULL
                     )
                   },
                   draw_panel = function(self, data, panel_scales, coord, width = NULL) {
                     bars <- ggproto_parent(GeomRect, self)$draw_panel(data, panel_scales, coord)
                     coords <- coord$transform(data, panel_scales)    
                     width <- abs(coords$xmax - coords$xmin)
                     tg <- fitGrob(label=coords$label, y = coords$y/2, x = coords$x, width = width)

                     grobTree(bars, tg)
                   }
)

geom_fit <- function(mapping = NULL, data = NULL,
                     stat = "count", position = "stack",
                     ...,
                     width = NULL,
                     binwidth = NULL,
                     na.rm = FALSE,
                     show.legend = NA,
                     inherit.aes = TRUE) {

  layer(
    data = data,
    mapping = mapping,
    stat = stat,
    geom = GeomFit,
    position = position,
    show.legend = show.legend,
    inherit.aes = inherit.aes,
    params = list(
      width = width,
      na.rm = na.rm,
      ...
    )
  )
}


set.seed(1234567)
data_gd <- data.frame(x = letters[1:5], 
                      y = runif(5, 100, 99999))

ggplot(data = data_gd,
       mapping = aes(x = x, y = y, fill = x, label=round(y))) +
  geom_fit(stat = "identity") +
  theme()

ggplot2: geom_text用plot和force/fit text在geom_bar中调整大小

#2


6  

If horizontal bar charts are OK, then the issue is not the size of the labels but the placement. My solution would be

如果水平条形图是可以的,那么问题不是标签的大小,而是位置。我的解决方案是

ggplot2: geom_text用plot和force/fit text在geom_bar中调整大小

created by this code:

由这段代码:

library(ggplot2)
data_gd <- data.frame(x = letters[1:26], 
                      y = runif(26, 100, 99999))
ymid <- mean(range(data_gd$y))
ggplot(data = data_gd,
       mapping = aes(x = x, y = y, fill = x)) +
  geom_bar(stat = "identity") +
  geom_text(mapping = aes(label = y, y = y, 
            hjust = ifelse(y < ymid, -0.1, 1.1)), size = 3) +
  coord_flip()

The trick is done in three steps:

诀窍在于三个步骤:

  1. coord_flip makes a horizontal bar chart.
  2. coord_flip制作一个水平条形图。
  3. The mapping in geom_text uses also hjust depending on the value of y. If the bar is shorter than half of the range of y, the text is printed outside of the bar (right to the y value). If the bar is longer than half of the range of y, the text is printed inside the bar (left to the y value). This makes sure that the text is always printed inside the plot area (if not too long at all).
  4. geom_text中的映射也使用hjust,这取决于y的值。如果该条长于y范围的一半,则文本将打印在该条内(左至y值)。这确保文本始终打印在情节区域内(如果不是太长的话)。
  5. I have added some additional space between the bar and the text. If you want the text to start or end directly at the y-value you can use hjust = ifelse(y < ymid, 0, 1)).
  6. 我在栏和文本之间增加了一些额外的空间。如果希望文本直接从y值开始或结束,可以使用hjust = ifelse(y < ymid, 0,1)。