Расширение ggplot2 с пользовательской геометрией для объектов SF - PullRequest
6 голосов
/ 23 апреля 2019

Я пытаюсь создать новую геометрию для ggplot, как описано здесь , одновременно адаптируя ее для работы с объектами простых функций.

В качестве примера, давайте возьмем то же упражнение по построению выпуклой оболочки набора точек. Таким образом, я написал новую функцию geom_envelope(), заимствующую элементы у geom_sf() и соответствующий объект GeomEnvelope ggproto, который выполняет вычисления, переопределяющие метод draw_group() (поскольку я хочу, чтобы для полный набор баллов).

Однако я должен что-то упустить, так как не могу построить полигон. Я пытался некоторое время, но я либо получаю ошибки, либо ничего не пишу.

library(sf); library(ggplot2); library(dplyr)

Npts <- 10
pts <- matrix(runif(2*Npts), ncol = 2) %>% 
  st_multipoint() %>% 
  st_sfc() %>% 
  st_cast("POINT") %>% 
  st_sf()

GeomEnvelope <- ggproto(
  "GeomEnvelope", GeomSf,

  required_aes = "geometry",

  default_aes = aes(
    shape = NULL,
    colour = "grey20",
    fill = "white",
    size = NULL,
    linetype = 1,
    alpha = 0.5,
    stroke = 0.5
  ),

  draw_key = draw_key_polygon,

  draw_group = function(data, panel_params, coord) {
    n <- nrow(data)
    if (n <= 2) return(grid::nullGrob())

    gp <- gpar(
      colour = data$colour,
      size = data$size,
      linetype = data$linetype,
      fill = alpha(data$fill, data$alpha),
      group = data$group,
      stringsAsFactors = FALSE
    )

    geometry <- sf::st_convex_hull(st_combine(sf::st_as_sf(data)))

    sf::st_as_grob(geometry, pch = data$shape, gp = gp)

  }
)


geom_envelope <- function(
  mapping = aes(),
  data = NULL,
  position = "identity",
  na.rm = FALSE,
  show.legend = NA,
  inherit.aes = TRUE,
  ...) {

  if (!is.null(data) && ggplot2:::is_sf(data)) {
    geometry_col <- attr(data, "sf_column")
  }
  else {
    geometry_col <- "geometry"
  }
  if (is.null(mapping$geometry)) {
    mapping$geometry <- as.name(geometry_col)
  }
  c(
    layer(
      geom = GeomEnvelope,
      data = data,
      mapping = mapping,
      stat = "identity",
      position = position,
      show.legend = if (is.character(show.legend))
        TRUE
      else
        show.legend,
      inherit.aes = inherit.aes,
      params = list(
        na.rm = na.rm,
        legend = if (is.character(show.legend))
          show.legend
        else
          "polygon",
        ...
      )
    ),
    coord_sf(default = TRUE)
  )
}

ggplot(pts) + geom_sf() + geom_envelope() + theme_bw()

Создано в 2019-04-23 пакетом представ (v0.2.1)

1 Ответ

5 голосов
/ 24 апреля 2019

Если это ваш реальный вариант использования (а не упрощенный пример), то я бы сказал, что основная часть того, что вы ищете, это пользовательский Стат , а не пользовательский Geom .Вычисления / манипуляции с данными должны происходить в первом.

(Для справки я обычно смотрю на код в GeomBoxplot / StatBoxplot, чтобы выяснить, гдевещи должны произойти, так как этот вариант использования включает в себя кучу вычислений для квантилей / выбросов, а также комбинацию различных элементов grob, которые принимают различные эстетические отображения.)

Данные со случайным начальным числом для воспроизводимости:

set.seed(123)

pts <- matrix(runif(2*Npts), ncol = 2) %>% 
  st_multipoint() %>% 
  st_sfc() %>% 
  st_cast("POINT") %>% 
  st_sf()

Базовая демонстрация

Следующая StatEnvelope будет принимать набор данных, переданный в соответствующий слой geom, и преобразовывать коллекцию значений геометрии в каждой группе (если эстетика группировки не указана, всянабор данных будет рассматриваться как одна группа) в выпуклый корпус:

StatEnvelope <- ggproto(
  "StatEnvelope", Stat,
  required_aes = "geometry",
  compute_group = function(data, scales) {
    if(nrow(data) <= 2) return (NULL)
    data %>%
      group_by_at(vars(-geometry)) %>%
      summarise(geometry = sf::st_convex_hull(sf::st_combine(geometry))) %>%
      ungroup()
  }
)

ggplot(pts) + 
  geom_sf() +
  geom_sf(stat = StatEnvelope, 
          alpha = 0.5, color = "grey20", fill = "white", size = 0.5) +
  theme_bw()

plot

Обновление

Приведенный выше подход с использованиемсуществующий geom_sf, отлично справляется с работой при создании конверта.Если мы хотим указать некоторые эстетические параметры по умолчанию, а не повторять их в каждом экземпляре geom_sf, нам все же не нужно определять новый Geom.Хорошо подойдет функция, которая модифицирует существующий geom_sf.

geom_envelope <- function(...){
  suppressWarnings(geom_sf(stat = StatEnvelope, 
                           ..., # any aesthetic argument specified in the function 
                                # will take precedence over the default arguments
                                # below, with suppressWarning to mute warnings on
                                # any duplicated aesthetics
                           alpha = 0.5, color = "grey20", fill = "white", size = 0.5))
}

# outputs same plot as before
ggplot(pts) + 
  geom_sf() +
  geom_envelope() +
  theme_bw()

# with different aesthetic specifications for demonstration
ggplot(pts) + 
  geom_sf() +
  geom_envelope(alpha = 0.1, colour = "brown", fill = "yellow", size = 3) +
  theme_bw()

plot 2


Объяснение того, что происходит с кодом, размещенным ввопрос

Когда я возлюсь с настроенными объектами ggproto, мне нравится использовать один полезный прием - вставлять операторы печати в каждую изменяемую мной функцию, например, "setting up parameters" или "drawing panel, step 3" и т. д. Это позволяет мнеиметь хорошее представление о том, что происходит под капотом, и отслеживать, где что-то пошло не так, когда функция (неизбежно) возвращает ошибку с 1-й / 2-й / ... / n-й попыткой.

В этом случаеесли мы вставим print("draw group") в начале функции GeomEnvelope draw_group перед запуском ggplot(pts) + geom_sf() + geom_envelope() + theme_bw(), мы увидим отсутствие какого-либо печатного сообщения в консоли.Другими словами, функция draw_group никогда не вызывалась , поэтому любые манипуляции с данными, определенные в ней, не влияют на вывод.

В Geom* есть несколько draw_* функций,что может сбивать с толку, когда мы хотим внести изменения.Из кода для Geom мы можем видеть иерархию следующим образом:

  1. draw_layer (включая строку do.call(self$draw_panel, args))
  2. draw_panel (включая строку self$draw_group(group, panel_params, coord, ...))
  3. draw_group (что не реализовано для Geom).

Итак draw_layer триггеры draw_paneldraw_panel запускает draw_group.(Отражая это, в Stat, compute_layer запускаются compute_panel и compute_panel запускаются compute_group.)

GeomSf, который наследуется от Geom (код здесь ), переопределяет функцию Geom draw_panel фрагментом кода, который возвращает sf_grob(...) и НЕ триггер draw_group.

Следовательно, когда GeomEnvelope наследует функцию GeomSf draw_panel, ничто в ее функции draw_group не имеет значения.То, что нарисовано на графике, зависит от draw_panel, и слой geom_envelope в вопросе выполняет по существу ту же задачу, что и geom_sf, составляя каждую отдельную точку отдельно.Если вы удалите / закомментируете слой geom_sf, вы увидите те же точки;только с color = "grey20", alpha = 0.5 и т. д., как указано в GeomSf s default_aes.

(Примечание: fill = "white" не используется, потому что geom_sf по умолчанию использует GeomPoint эстетику по умолчанию для точечных данных, что означает, что он наследует GeomPoint pch = 19 для своей точечной формы и строит сплошной круг, не подверженный влиянию любого значения заливки.)

...