Расширение ggplot собственными геомами: адаптация масштаба по умолчанию

Фон

Прочитав этот красивый ответ о том, как расширить ggplot и соответствующая виньетка Я пытался понять, как расширить ggplot.

Кратко

Я понимаю, как собираются части, но мне не хватает важной информации: как ggplot определяет диапазон по умолчанию для оси?

Код

Рассмотрим следующий пример с игрушкой:

library(grid)
library(ggplot2)

GeomFit <- ggproto("GeomFit", GeomBar,
                   required_aes = c("x", "y"),
                   setup_data = .subset2(GeomBar, "setup_data"),
                   draw_panel = function(self, data, panel_scales, coord, width = NULL) {
                     bars <- ggproto_parent(GeomBar, self)$draw_panel(data,
                                                                      panel_scales, 
                                                                      coord)
                     coords <- coord$transform(data, panel_scales)    
                     tg <- textGrob("test", coords$x, coords$y * 2 - coords$ymin)
                     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 = 1:5)

p <- ggplot(data = data_gd, aes(x = x, y = y, fill = x)) + 
  geom_fit(stat = "identity")

Что создает этот сюжет: Барплот с текстом

Проблема

Как видите, часть текста не отображается. Я предполагаю, что ggplot каким-то образом вычисляет диапазоны для оси, и поскольку он не знает о дополнительном пространстве, необходимом для моего textGrob. Как я могу это решить? (Желаемый результат эквивалентен p + expand_limits(y = 10)

Примечание. Конечно, я мог бы довести проблему до конечного пользователя, потребовав добавить ручную шкалу. Но в идеале хотелось бы, чтобы весы были правильно настроены.


person thothal    schedule 01.04.2019    source источник
comment
ggplot расширяет диапазон данных мультипликативно и аддитивно. Множители и коэффициенты добавления могут быть установлены пользователем в аргументе expand функции scale. см. пример здесь. Это часто происходит, когда люди хотят, чтобы график вообще не расширялся. В последних версиях вы можете использовать expand_scale для расширения оси только в одном направлении. ?expand_scale — хорошее место для начала.   -  person Gregor Thomas    schedule 01.04.2019
comment
Я не знаю, как внутренне определяется начальный диапазон осей, который нужно расширить (поэтому я просто комментирую, а не отвечаю).   -  person Gregor Thomas    schedule 01.04.2019
comment
Насколько я понимаю, диапазон масштаба обучается на основе диапазона данных. Если вы отметите layer_data(p), значения y находятся в диапазоне от 1 до 5, так что это диапазон масштаба графика.   -  person Z.Lin    schedule 01.04.2019
comment
Как ggplot узнает, в каком диапазоне он должен искать? С одними и теми же данными я могу получить совершенно разные диапазоны на основе моего сопоставления: d <- data.frame(x = rep(1:10, 10), y = sample(3, 100, TRUE)): p <- ggplot(d, aes(x = x)); p + geom_bar() против p + geom_bar(aes(y=y), stat = "identity"). Поэтому только после того, как мы узнаем, какие отображения мы используем в geom, мы можем определить диапазон для графика. -> 1. какая функция отвечает за определение дальности? 2. из какой функции вызывается его функция?   -  person thothal    schedule 02.04.2019
comment
@thothal Это будет функция train_position из Layout. Один трюк, который мне нравится использовать при копании объектов ggplot, заключается в запуске отладки на ggplot2:::ggplot_build.ggplot или ggplot2:::ggplot_gtable.ggplot_built (два этапа ggplotGrob()) и проверке вывода на каждом этапе, чтобы найти, где происходит интересующее явление (в данном случае создание масштаба). Но будьте осторожны, кроличья нора может быть очень, очень глубокой...   -  person Z.Lin    schedule 02.04.2019
comment
+1, это то, чем я сейчас занимаюсь, так многому нужно научиться. И благодаря вашему другому ответу я не смог использовать debug для некоторых внутренних функций в разных Geom*. Я думаю, что нашел уродливое обходное решение, которое я опубликую для дальнейшего использования.   -  person thothal    schedule 02.04.2019


Ответы (1)


Эврика

Благодаря потрясающей помощи @Z.Lin нашел уродливый хак в том, как отлаживать ggplot код. Вот как я придумал этот довольно уродливый хак для дальнейшего использования:

Как я туда попал

Во время отладки debug(ggplot2:::ggplot_build.ggplot) я узнал, что виновник может быть найден где-то в FacetNull$train_scales (в моем графике без граней другие грани, такие как FacetGrid, работают аналогичным образом). Это я узнал через debug(environment(FacetNull$train_scales)$f), который, в свою очередь, я узнал в ответе Z.Lin в другом потоке .

Как только мне удалось отладить ggproto объектов, я увидел, что именно в этой функции тренируются весы. По сути, функция смотрит на эстетику, которая имеет отношение к конкретному масштабу (не знаю, где эта информация настраивается в первую очередь, есть какие-нибудь идеи у кого-нибудь?) и смотрит, какая из этих эстетик присутствует в данных слоя.

Я увидел, что поле ymax_final (которое согласно этой таблицы) используется только для stat_boxplot) входит в число тех, которые рассматриваются при настройке весов. С помощью этой части информации было легко найти уродливый взлом, установив это поле в setup_data на соответствующее значение.

Код

GeomFit <- ggproto("GeomFit", GeomBar,
                   required_aes = c("x", "y"),
                   setup_data = function(self, data, params) {
                      data <- ggproto_parent(GeomBar, self)$setup_data(data, params)
                      ## here's the hack: add a field which is not needed in this geom, 
                      ## but which is used by Facet*$train_scales which 
                      ## eventually sets up the scales
                      data$ymax_final <- 2 * data$y
                      data
                   }, 
                   draw_panel = function(self, data, panel_scales, coord, width = NULL) {
                     bars <- ggproto_parent(GeomBar, self)$draw_panel(data,
                                                                      panel_scales, 
                                                                      coord)
                     coords <- coord$transform(data, panel_scales)    
                     tg <- textGrob("test", coords$x, coords$y * 2 - coords$ymin)
                     grobTree(bars, tg)
                   }
)

Результат

Гистограмма с метками

Открытый вопрос

Где устанавливаются шкалы (осей), если вы не определяете их вручную? Я предполагаю, что там настроены поля, которые имеют значение для масштабирования.

person thothal    schedule 02.04.2019