проверка имени аргумента в scale_continuous_fill () - PullRequest
0 голосов
/ 17 января 2019

Вопрос: Я использую пакет testthat для проверки фигуры ggplot2. Я не могу найти местоположение имени легенды (то есть аргумент name для scale_fill_continuous()). Где сохраняется name? (см. мой воспроизводимый пример в конце поста для конкретного примера).

Мои поисковики: Я искал SO, но другие вопросы с тегами [testthat] и [ggplot] не помогли (например, этот и этот один ). Я также просмотрел ggplot2 модульные тесты и не смог найти свой ответ.

Воспроизводимый пример: Я ищу расположение expression("Legend name"^2), чтобы проверить и убедиться, что оно правильное.

library(ggplot2)
library(testthat)

# Create example data and plots
df <- data.frame(
    x = c(1, 2, 3, 1, 4, 5, 6, 4), 
    y = c(1, 2, 1, 1, 1, 2, 1, 1),
    z = rep(1:2, each = 4),
    group = rep(letters[1:2], each = 4))

my_plot <- 
    ggplot(df, aes(x = x, y = y, group = group, fill = z )) +
    geom_polygon() +
    scale_fill_continuous(name = expression("Legend name"^2), 
                          low = "skyblue", high = "orange")

my_wrong_plot <- 
    ggplot(df, aes(x = x, y = y, group = group, fill = z)) +
    geom_polygon() +
    scale_fill_continuous(name = expression("Wrong name"^2), 
                          low = "skyblue", high = "orange")

# Example tests that work
test_that("plot is drawn correctly", {
    expect_identical(
        deparse(my_plot$mapping$group),
        deparse(my_wrong_plot$mapping$group),
        info = 'The `group` aesthetic is incorrect.'
    )
    expect_identical(
        deparse(my_plot$mapping$fill),
        deparse(my_wrong_plot$mapping$fill),
        info = 'The `fill` aesthetic is incorrect.'
    )     
    expect_identical(
        class(my_plot$layers[[1]]$geom)[1],
        class(my_wrong_plot$layers[[1]]$geom)[1],
        info = 'There is no polygon layer.'
    )         
    expect_identical(
        layer_data(my_plot),
        layer_data(my_wrong_plot),
        info = "The `scale_fill_continuous()` data is incorrect."
    )         
})

1 Ответ

0 голосов
/ 19 января 2019

Краткий ответ

Если ваш объект ggplot назван p, и вы указали аргумент name в своей шкале, он будет найден в p$scales$scales[[i]]$name (где i соответствует порядку шкалы).

Длинный ответ

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

Начальная точка : Часто полезно преобразовать объект ggplot в объект grob, так как последний позволяет нам делать все виды вещей, которые мы не можем легко взломать в ggplot (например, построить geom на краю области графика без обрезки закрасьте разные полосы фасетов разными цветами, вручную добавьте ширину фасета для каждого фасета, добавьте график на другую карту в качестве пользовательской аннотации и т. д.).

В пакете ggplot2 есть функция ggplotGrob, которая выполняет преобразование. Это означает, что если мы рассмотрим шаги на этом пути, мы сможем найти шаг, который найдет заголовок шкалы в объекте ggplot, чтобы преобразовать его в некоторый вид textGrob.

Это, в свою очередь, означает, что мы собираемся взять следующую единственную строку кода и перейти вниз по последовательным уровням, пока не выясним, что происходит под капотом:

ggplotGrob(my_plot)

Слой 1 : ggplotGrob сам по себе является просто оболочкой для двух функций, ggplot_build и ggplot_gtable.

> ggplotGrob
function (x) 
{
    ggplot_gtable(ggplot_build(x))
}

С ?ggplot_build:

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

С ?ggplot_gtable:

Эта функция создает все графы, необходимые для отображения графика, и сохраняет их в специальной структуре данных, которая называется gtable(). это объект поддается программным манипуляциям, если вы хотите (например) сделать поле легенды шириной 2 см или объединить несколько сюжетов в один дисплей с сохранением пропорций на графиках.

Слой 2 : и ggplot_build, и ggplot_gtable просто возвращают универсальный UseMethod("<function name>" при вводе в консоль, и соответствующие функции не экспортируются из пакета ggplot2. Тем не менее, вы можете найти их на GitHub ( link ) или получить к ним доступ в любом случае, используя тройное двоеточие :::.

> ggplot2:::ggplot_build.ggplot
function (plot) 
{
    plot <- plot_clone(plot)
    # ... omitted for space
    layout <- create_layout(plot$facet, plot$coordinates)
    data <- layout$setup(layer_data, plot$data, plot$plot_env)
    # ... omitted for space
    structure(list(data = data, layout = layout, plot = plot), 
        class = "ggplot_built")
}

> ggplot2:::ggplot_gtable.ggplot_built
function (data) 
{
    plot <- data$plot
    layout <- data$layout
    data <- data$data
    theme <- plot_theme(plot)
    # ... omitted for space
    position <- theme$legend.position %||% "right"
    # ... omitted for space
    legend_box <- if (position != "none") {
        build_guides(plot$scales, plot$layers, plot$mapping, 
            position, theme, plot$guides, plot$labels)
    }
    # ... omitted for space
}

Мы видим, что в ggplot2:::ggplot_gtable.ggplot_built есть фрагмент кода, который создает поле легенды:

    legend_box <- if (position != "none") {
        build_guides(plot$scales, plot$layers, plot$mapping, 
            position, theme, plot$guides, plot$labels)
    }

Давайте проверим, так ли это на самом деле:

g.build <- ggplot_build(my_plot)

legend.box <- ggplot2:::build_guides(
  g.build$plot$scales, 
  g.build$plot$layers, 
  g.build$plot$mapping, 
  "right", 
  ggplot2:::plot_theme(g.build$plot), 
  g.build$plot$guides, 
  g.build$plot$labels)

grid::grid.draw(legend.box)

legend box

И это действительно так. Давайте увеличим масштаб, чтобы увидеть, что делает ggplot2:::build_guides.

Слой 3 : В ggplot2:::build_guides мы видим, что после некоторых строк кода, которые обрабатывают положение и выравнивание блока легенды, определения руководства (gdefs) генерируются функцией с именем guides_train:

> ggplot2:::build_guides
function (scales, layers, default_mapping, position, theme, guides, 
    labels) 
{
    # ... omitted for space
    gdefs <- guides_train(scales = scales, theme = theme, guides = guides, 
        labels = labels)
    # .. omitted for space
}

Как и прежде, мы можем подключить соответствующее значение для каждого аргумента и проверить, что говорят эти определения руководства:

gdefs <- ggplot2:::guides_train(
  scales = g.build$plot$scales, 
  theme = ggplot2:::plot_theme(g.build$plot),
  guides = g.build$plot$guides,
  labels = g.build$plot$labels
  )

> gdefs    
[[1]]
$title
expression("Legend name"^2)

$title.position
NULL
#... omitted for space

Да, есть имя масштаба, которое мы ожидали: expression("Legend name"^2). ggplot2:::guides_train (или какая-то функция внутри него) вытащила его из g.build$plot$<something> / ggplot2:::plot_theme(g.build$plot), но нам нужно копать глубже, чтобы увидеть, что и как.

Слой 4 : В пределах ggplot2:::guides_train мы находим строку кода, которая берет заголовок легенды из одного из нескольких возможных мест:

> guides_train
function (scales, theme, guides, labels) 
{
    gdefs <- list()
    for (scale in scales$scales) {
        for (output in scale$aesthetics) {
            guide <- guides[[output]] %||% scale$guide
            # ... omitted for space
            guide$title <- scale$make_title(guide$title %|W|% 
                scale$name %|W|% labels[[output]])
            # ... omitted for space
        }
    }
    gdefs
}

(ggplot2:::%||% и ggplot2:::%|W|% - неэкспортированные функции из пакета. Они принимают два значения, возвращая первое значение, если оно определено / не отменено, и второе в противном случае.)

Annnnnnnnnnd Мы внезапно переходим от того, что слишком мало мест, чтобы искать название легенды, к слишком большому количеству. Вот они, в порядке приоритета:

  1. Если определено g.build$plot$guides[["fill"]] и значение g.build$plot$guides[["fill"]]$title не равно waiver(): g.build$plot$guides[["fill"]]$title;
  2. Иначе, если значение g.build$plot$scales$scales[[1]]$guide$title не равно waiver(): g.build$plot$scales$scales[[1]]$guide$title;
  3. Иначе, если значение g.build$plot$scales$scales[[1]]$name не равно waiver(): g.build$plot$scales$scales[[1]]$name;
  4. Остальное: g.build$plot$labels[["fill"]].

Мы также знаем из анализа кода, стоящего за ggplot2:::ggplot_build.ggplot, что g.build$plot по существу совпадает с первоначально введенным my_plot, поэтому вы можете заменить каждый экземпляр g.build$plot в приведенном выше списке на my_plot.

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

base.plot <- ggplot(df, 
         aes(x = x, y = y, group = group, fill = z )) +
  geom_polygon()

cowplot::plot_grid(
  # plot 1: title defined in guides() overrides titles defined in `scale_...`
  base.plot + ggtitle("1") +
    scale_fill_continuous(
      name = "scale",
      low = "skyblue", high = "orange",
      guide = guide_colorbar(title = "guide in scale")) +
    guides(fill = guide_colorbar(title = "guide")),
  # plot 2: title defined in scale_...'s guide overrides scale_...'s name
  base.plot + ggtitle("2") +
    scale_fill_continuous(
      name = "scale",
      low = "skyblue", high = "orange",
      guide = guide_colorbar(title = "guide in scale")),
  # plot 3: title defined in `scale_...'s name
  base.plot + ggtitle("3") +
    scale_fill_continuous(
      name = "scale",
      low = "skyblue", high = "orange"),
  # plot 4: with no title defined anywhere, defaults to variable name
  base.plot + ggtitle("4") +
    scale_fill_continuous(
      low = "skyblue", high = "orange"),
  nrow = 2
)

multiple titles for the same scale

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

sample.plot <- ggplot(df, 
         aes(x = x, y = y, group = group, fill = z )) +
  geom_polygon() +
    scale_fill_continuous(
      name = "title3",
      guide = guide_colorbar(title = "title2")) +
    guides(fill = guide_colorbar(title = "title1"))

> sample.plot$guides[["fill"]]$title
[1] "title1"
> sample.plot$scales$scales[[1]]$guide$title
[1] "title2"
> sample.plot$scales$scales[[1]]$name
[1] "title3"
> sample.plot$labels[["fill"]]
[1] "z"
...