Возможно ли реализовать функциональность base-r plot type-b = g в ggplot2?

Функциональность base plot() позволяет установить type='b' и получить комбинированный линейный и точечный график, в котором точки смещены относительно отрезков линии

plot(pressure, type = 'b', pch = 19)

enter image description here

Я могу легко создать ggplot с линиями и точками следующим образом.

ggplot(pressure, aes(temperature, pressure)) + 
  geom_line() + 
  geom_point()

enter image description here

Линии, однако, идут прямо до точек. Я могу представить себе способ, которым я мог бы взломать что-то вроде функциональности type='b' используя другие geoms (например, geom_segment()?), Но мне интересно, есть ли более прямой способ сделать это с помощью geom_line() и geom_point()

Ответы

Ответ 1

Немного хакерский способ сделать это - наложить маленькую черную точку на большую белую точку:

ggplot(pressure, aes(temperature, pressure)) + 
  geom_line() +
  geom_point(size=5, colour="white") + 
  geom_point(size=2) + 
  theme_classic() +
  theme(panel.background = element_rect(colour = "black"))

Кроме того, следуя толщине границы контрольной точки в ggplot, в версии 2.0.0 ggplot2 можно использовать аргумент stroke geom_point для управления толщиной границы, поэтому две точки geom_point можно заменить просто (например) geom_point(size=2, shape=21, fill="black", colour="white", stroke=3), устраняя необходимость наложения точек.

enter image description here

Ответ 2

Один из вариантов, который менее удачен, чем ручное сопоставление цвета обводки с фоном панели, - это предварительно получить фон панели, либо из theme_get для темы по умолчанию, либо с определенной темой, которую вы будете использовать. Использование обведенной формы, например, 21 позволяет сделать внутренний круг черным, а обводку - цветом фона.

library(ggplot2)

bgnd <- theme_get()$panel.background$fill

ggplot(pressure, aes(x = temperature, y = pressure)) + 
  geom_line() + 
  geom_point(shape = 21, fill = "black", size = 2, stroke = 1, color = bgnd)

Пара вопросов SO (здесь один) имеет дело с математикой за сокращение отрезков между точками. Это простая, но утомительная геометрия. Но со времени, когда этот вопрос был впервые опубликован, вышел пакет с lemon, в котором есть что сделать. Он получил аргументы в пользу того, как рассчитать сокращение, которое, вероятно, потребует простой настройки.

library(lemon)

ggplot(pressure, aes(x = temperature, y = pressure)) +
  geom_pointline()

Ответ 3

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

* обратите внимание, что поведение пути заключается не в соединении точек вдоль оси x, а в порядке строк в data.frame который передается ggplot. Вы можете отсортировать данные заранее, если хотите geom_line() поведение geom_line().

Основная проблема для меня заключалась в том, чтобы получить внутреннюю работу кода для рисования geom, чтобы получить тему текущего графика и извлечь цвет фона панели. Из-за этого я очень не уверен, насколько стабильной это будет (и приветствовал бы любые подсказки), но по крайней мере это работает.

РЕДАКТИРОВАТЬ: должно быть более стабильным сейчас

Давайте ggproto, по- ggproto, ggproto объектному коду ggproto:

GeomPointPath <- ggproto(
  "GeomPointPath", GeomPoint,
  draw_panel = function(self, data, panel_params, coord, na.rm = FALSE)
  {

    # bgcol <- sys.frame(4)$theme$panel.background$fill
    # if (is.null(bgcol)) {
    #   bgcol <- theme_get()$panel.background$fill
    # }

    # EDIT: More robust bgcol finding -----------
    # Find theme, approach as in https://github.com/tidyverse/ggplot2/issues/3116
    theme <- NULL
    for(i in 1:20) {
      env <- parent.frame(i)
      if("theme" %in% names(env)) {
        theme <- env$theme
        break
      }
    }
    if (is.null(theme)) {
      theme <- theme_get()
    }

    # Lookup likely background fills
    bgcol <- theme$panel.background$fill
    if (is.null(bgcol)) {
      bgcol <- theme$plot.background$fill
    }
    if (is.null(bgcol)) {
      bgcol <- theme$rect$fill
    }
    if (is.null(bgcol)) {
      # Default to white if no fill can be found
      bgcol <- "white"
    }
    # END EDIT ------------------

    if (is.character(data$shape)) {
      data$shape <- ggplot2:::translate_shape_string(data$shape)
    }

    coords <- coord$transform(data, panel_params)

    # Draw background points
    bgpoints <- grid::pointsGrob(
      coords$x, coords$y, pch = coords$shape,
      gp = grid::gpar(
        col = alpha(bgcol, NA), 
        fill = alpha(bgcol, NA),
        fontsize = (coords$size * .pt + coords$stroke * .stroke/2) * coords$mult,
        lwd = coords$stroke * .stroke/2
      )
    )

    # Draw actual points
    mypoints <- grid::pointsGrob(
      coords$x, coords$y, pch = coords$shape, 
      gp = grid::gpar(
        col = alpha(coords$colour, coords$alpha), 
        fill = alpha(coords$fill, coords$alpha), 
        fontsize = coords$size * .pt + coords$stroke * .stroke/2, 
        lwd = coords$stroke * .stroke/2
      )
    )

    # Draw line
    myline <- grid::polylineGrob(
      coords$x, coords$y, 
      id = match(coords$group, unique(coords$group)),
      default.units = "native",
      gp = grid::gpar(
        col = alpha(coords$colour, coords$alpha),
        fill = alpha(coords$colour, coords$alpha),
        lwd = (coords$linesize * .pt),
        lty = coords$linetype,
        lineend = "butt",
        linejoin = "round", linemitre = 10
      )
    )

    # Place graphical objects in a tree
    ggplot2:::ggname(
      "geom_pointpath",
      grid::grobTree(myline, bgpoints, mypoints) 
    )
  },
  # Set some defaults, assures that aesthetic mappings can be made
  default_aes = aes(
    shape = 19, colour = "black", size = 1.5, fill = NA, alpha = NA, stroke = 0.5,
    linesize = 0.5, linetype = 1, mult = 3,
  )
)

Наблюдающие люди, возможно, заметили строку bgcol <- sys.frame(4)$theme$panel.background$fill. Я не смог найти другой способ доступа к текущей теме сюжета, не настроив хотя бы несколько других функций для передачи темы в качестве аргумента. В моей версии ggplot (3.1.0) 4-й sys.frame() является средой ggplot2:ggplot_gtable.ggplot_built которой оценивается код рисования geom. Довольно легко представить, что эта функция может быть обновлена в будущем. -which может изменить scoping-, следовательно, предупреждение о стабильности. В качестве резервной копии по умолчанию используются глобальные настройки темы, когда не удается найти текущую тему.

РЕДАКТИРОВАТЬ: теперь должно быть более стабильным

Вперед к обертке слоя, которая в значительной степени говорит сама за себя:

geom_pointpath <- function(mapping = NULL, data = NULL, stat = "identity",
                           position = "identity", ..., na.rm = FALSE, show.legend = NA,
                           inherit.aes = TRUE)
{
  layer(data = data, mapping = mapping, stat = stat, geom = GeomPointPath,
        position = position, show.legend = show.legend, inherit.aes = inherit.aes,
        params = list(na.rm = na.rm, ...))
}

Добавление его в ggplot должно быть привычным делом. Просто установите для темы значение theme_gray() по умолчанию, чтобы проверить, действительно ли она принимает текущую сюжетную тему.

theme_set(theme_gray())
g <- ggplot(pressure, aes(temperature, pressure)) +
  geom_pointpath() +
  theme(panel.background = element_rect(fill = "dodgerblue"))

Конечно, этот метод будет затенять линии сетки фоновыми точками, но это был компромисс, который я был готов сделать, чтобы предотвратить сомнительность из-за сокращения пути линии. Размеры линий, типы линий и относительный размер точек фона могут быть установлены с помощью aes(linesize =..., linetype =..., mult =...) или для аргумента ... в geom_pointpath(). Он наследует другую эстетику от GeomPoint.

enter image description here

Ответ 4

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

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

Это почти невозможно знать соотношение сторон или точные размеры сюжета априори, за исключением того, установив соотношение сторон вручную или с помощью space аргумент facet_grid(). Поскольку это невозможно, любой предварительно вычисленный набор координат будет неадекватным, если размер графика будет изменен.

Я бесстыдно украл некоторые хорошие идеи от других людей, так что спасибо @Tjebo и @moody_mudskipper за математику и кредит ggplot guru thomasp85 и пакет ggforce для расчета во время вдохновения.

С этим; сначала мы определим наше ggproto, как и раньше, теперь создадим собственный класс grob для нашего пути. Важной деталью является то, что мы конвертируем наши координаты xy в формальные единицы.

GeomPointPath <- ggproto(
  "GeomPointPath", GeomPoint,
  draw_panel = function(data, panel_params, coord, na.rm = FALSE){

    # Default geom point behaviour
    if (is.character(data$shape)) {
      data$shape <- translate_shape_string(data$shape)
    }
    coords <- coord$transform(data, panel_params)
    my_points <- pointsGrob(
      coords$x, 
      coords$y, 
      pch = coords$shape, 
      gp = gpar(col = alpha(coords$colour, coords$alpha), 
                fill = alpha(coords$fill, coords$alpha), 
                fontsize = coords$size * .pt + coords$stroke * .stroke/2, 
                lwd = coords$stroke * .stroke/2))

    # New behaviour
    ## Convert x and y to units
    x <- unit(coords$x, "npc")
    y <- unit(coords$y, "npc")

    ## Make custom grob class
    my_path <- grob(
      x = x,
      y = y,
      mult = (coords$size * .pt + coords$stroke * .stroke/2) * coords$mult,
      name = "pointpath",
      gp = grid::gpar(
        col = alpha(coords$colour, coords$alpha),
        fill = alpha(coords$colour, coords$alpha),
        lwd = (coords$linesize * .pt),
        lty = coords$linetype,
        lineend = "butt",
        linejoin = "round", linemitre = 10
      ),
      vp = NULL,
      ### Now this is the important bit:
      cl = 'pointpath'
    )

    ## Combine grobs
    ggplot2:::ggname(
      "geom_pointpath",
      grid::grobTree(my_path, my_points) 
    )
  },
  # Adding some defaults for lines and mult
  default_aes = aes(
    shape = 19, colour = "black", size = 1.5, fill = NA, alpha = NA, stroke = 0.5,
    linesize = 0.5, linetype = 1, mult = 0.5,
  )
)

Благодаря магии объектно-ориентированного программирования мы теперь можем написать новый метод для нашего нового класса grob. Хотя это может быть неинтересно само по себе, особенно интересно, если мы напишем этот метод для makeContent, который вызывается каждый раз, когда рисуется grob. Итак, давайте напишем метод, который вызывает математические операции с точными координатами, которые графическое устройство будет использовать:

# Make hook for drawing
makeContent.pointpath <- function(x){
  # Convert npcs to absolute units
  x_new <- convertX(x$x, "mm", TRUE)
  y_new <- convertY(x$y, "mm", TRUE)

  # Do trigonometry stuff
  hyp <- sqrt(diff(x_new)^2 + diff(y_new)^2)
  sin_plot <- diff(y_new) / hyp 
  cos_plot <- diff(x_new) / hyp

  diff_x0_seg <- head(x$mult, -1) * cos_plot
  diff_x1_seg <- (hyp - head(x$mult, -1)) * cos_plot
  diff_y0_seg <- head(x$mult, -1) * sin_plot
  diff_y1_seg <- (hyp - head(x$mult, -1)) * sin_plot

  x0 = head(x_new, -1) + diff_x0_seg
  x1 = head(x_new, -1) + diff_x1_seg
  y0 = head(y_new, -1) + diff_y0_seg
  y1 = head(y_new, -1) + diff_y1_seg
  keep <- unclass(x0) < unclass(x1)

  # Remove old xy coordinates
  x$x <- NULL
  x$y <- NULL

  # Supply new xy coordinates
  x$x0 <- unit(x0, "mm")[keep]
  x$x1 <- unit(x1, "mm")[keep]
  x$y0 <- unit(y0, "mm")[keep]
  x$y1 <- unit(y1, "mm")[keep]

  # Set to segments class
  class(x)[1] <- 'segments'
  x
}

Теперь все, что нам нужно, это оболочка слоя, как и раньше, которая не делает ничего особенного:

geom_pointpath <- function(mapping = NULL, data = NULL, stat = "identity",
                           position = "identity", ..., na.rm = FALSE, show.legend = NA,
                           inherit.aes = TRUE)
{
  layer(data = data, mapping = mapping, stat = stat, geom = GeomPointPath,
        position = position, show.legend = show.legend, inherit.aes = inherit.aes,
        params = list(na.rm = na.rm, ...))
}

Демонстрация:

g <- ggplot(pressure, aes(temperature, pressure)) +
  # Ribbon for showing no point-over-point background artefacts
  geom_ribbon(aes(ymin = pressure - 50, ymax = pressure + 50), alpha = 0.2) +
  geom_pointpath()

enter image description here

И это должно быть стабильным для любого измененного соотношения сторон. Вы можете указать aes(mult =...) или просто mult =... для контроля размера промежутков между сегментами. По умолчанию он пропорционален размерам точек, поэтому изменение размера точек при сохранении контуров зазора является сложной задачей. Сегменты, которые короче двухкратного зазора, удаляются.