ggplot2: изменить размер geom_text с графиком и принудительно / подогнать текст в geom_bar

На самом деле это два вопроса в одном (не уверен, что это противоречит правилам SO, но в любом случае).

Первый вопрос: как заставить geom_text вписаться в geom_bar? (динамически в соответствии со значениями, нанесенными на график)

Оглядываясь вокруг, я обнаружил, что решения, которые я нашел, меняли размер этикетки. Это, безусловно, работает, но не во всех случаях. Вы можете изменить размер определенного графика, чтобы текст поместился в полосе, но при изменении данных вам может потребоваться снова вручную изменить размер текста. Моя реальная проблема заключается в том, что мне нужно создать один и тот же график для постоянно меняющихся данных (ежедневно), поэтому я не могу вручную настроить размер для каждого графика.

Я попытался установить размер метки в зависимости от данных. Это вроде как работает, не идеально, но работает во многих случаях.

Но вот еще одна проблема, даже когда метка умещается в полосе, изменение размера сюжета все портит. Заглянув в него, я также обнаружил в документации ggplot, что

метки имеют высоту и ширину, но они являются физическими единицами, а не единицами данных. Объем места, которое они занимают на этом графике, не является постоянным в единицах данных: при изменении размера графика метки остаются того же размера, но размер осей изменяется.

Это подводит меня ко второму вопросу: можно ли изменить это поведение по умолчанию и разрешить / изменить размер меток вместе с графиком?

А также позвольте мне уточнить свой первый вопрос. Можно ли заставить geom_text вписаться в geom_bar, динамически устанавливая размер текста, используя разумное соотношение между физическими единицами и единицами данных?

Итак, чтобы следовать хорошей практике, вот мой воспроизводимый пример:

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

Этот код создает такой график:

введите описание изображения здесь

Если я просто изменю размер графика, метки останутся того же размера, но размер осей изменится ", благодаря чему метки помещаются в полосы (теперь, возможно, метки даже слишком малы).

введите описание изображения здесь

Итак, это мой второй вопрос. Было бы неплохо, если бы метки также изменили размер и сохранили соотношение сторон по отношению к полосам. Есть идеи, как это сделать или возможно ли это вообще?

Хорошо, но возвращаясь к тому, как разместить метки внутри полос, самое простое решение - установить размер меток.

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)

Опять же, это работает, как показано ниже, но не поддерживает / не устойчиво к изменениям данных.

введите описание изображения здесь

Например, один и тот же код для создания графика с разными данными дает катастрофические результаты.

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)

введите описание изображения здесь

И я могу продолжить с примерами, установив размер меток в зависимости от количества категорий по оси x и так далее. Но вы уловили суть, и, возможно, один из вас ggplot2 экспертов может подсказать мне идеи.


person elikesprogramming    schedule 30.03.2016    source источник


Ответы (2)


один из вариантов может заключаться в том, чтобы написать geom, который использует textGrob с настраиваемым методом drawDetails, чтобы поместиться в выделенное пространство, установленное шириной полосы.

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()

введите описание изображения здесь

person baptiste    schedule 31.03.2016
comment
потрясающе !!!, ... все еще пытаюсь понять, как это работает, потому что я знаю только основы ggplot2, но код работает плавно и решает обе проблемы: он идеально вписывается в текст в полосах, а размер текста изменяется вместе с сценарий. Я действительно не думал, что это возможно. Большое спасибо - person elikesprogramming; 31.03.2016
comment
Очень поздно на вечеринку, но это действительно здорово. Узнал так много, спасибо. Только один вопрос к моему пониманию: почему ваш GeomFit не использовал GeomBar (вместо GeomRect) в качестве родительского? В этом случае вы также можете пропустить setup_data, который AFAICT является копией / вставкой самого GeomBar$setup_data? - person thothal; 22.03.2019

Если горизонтальные гистограммы в порядке, проблема не в размере меток, а в их размещении. Мое решение было бы

введите описание изображения здесь

созданный этим кодом:

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()

Уловка выполняется в три этапа:

  1. coord_flip создает горизонтальную гистограмму.
  2. Отображение в geom_text также использует hjust в зависимости от значения y. Если полоса короче половины диапазона y, текст печатается за пределами полосы (справа от значения y). Если полоса длиннее половины диапазона y, текст печатается внутри полосы (слева от значения y). Это гарантирует, что текст всегда печатается внутри области печати (если не слишком длинный).
  3. Я добавил дополнительное пространство между полосой и текстом. Если вы хотите, чтобы текст начинался или заканчивался непосредственно на значении y, вы можете использовать hjust = ifelse(y < ymid, 0, 1)).
person Uwe    schedule 30.03.2016
comment
Спасибо. Я думаю, это так хорошо, как есть, не так ли? Для меня вертикальные полосы - необходимость, но я думаю, что было бы приемлемо повернуть только метки (angle = 90) и использовать свой трюк с hjust, чтобы убедиться, что текст печатается внутри области печати. - person elikesprogramming; 31.03.2016
comment
Все еще не понимаю, как изменить размер сюжета и меток одновременно, ... но я почти уверен, что это вообще невозможно - person elikesprogramming; 31.03.2016
comment
Размер текста необходимо отрегулировать в зависимости от количества полосок, например text_size <- if (n_bar <= 20) 4 else 3 или чего-то более сложного с использованием scale_size. - person Uwe; 31.03.2016
comment
только теперь, глядя на ваш ответ, я осознал ошибку, которую совершил в вопросе (letters[1:30]), который вы любезно и незаметно исправили letters[1:26]. Еще раз спасибо, ваш ответ очень полезен (но ответ @baptiste ниже касается обеих проблем). - person elikesprogramming; 31.03.2016