Пользовательский масштаб оси Y и вторичные метки оси Y в ggplot2 3.1.0

Редактировать 2

Текущая версия ggplot2-пакета для разработки действительно решает проблему, упомянутую в моем вопросе ниже. Установите версию dev, используя

devtools::install_github("tidyverse/ggplot2")

редактировать

Кажется, что ошибочное поведение sec_axis в ggplot2 3.1.0 является ошибкой. Это было признано разработчиками, и они работают над исправлением (см. Ветку на GitHub).


Цель

У меня есть график, где ось Y колеблется от 0 до 1. Я хотел бы добавить вторичную ось Y, которая колеблется от 0 до 0,5 (так что ровно половина значений первичной оси Y). Пока проблем нет.

Что усложняет дело, так это то, что у меня есть пользовательское преобразование для оси Y, где часть оси Y отображается линейно, а остальные - логарифмически (см. Пример ниже). Для справки смотрите этот пост или этот.

проблема

Это прекрасно работает при использовании ggplot2 версии 3.0.0, но больше не работает при использовании новейшей версии (3.1.0). Смотрите пример ниже. Я не знаю, как это исправить в новейшей версии.

Из журнала изменений:

sec_axis() и dup_axis() теперь возвращают соответствующие разрывы для вторичной оси при применении к лог-преобразованным масштабам

Эта новая функциональность, кажется, ломается в случае смешанно-преобразованных Y-осей.

Воспроизводимый пример

Вот пример использования новейшей версии (3.1.0) ggplot2:

library(ggplot2)
library(scales)

#-------------------------------------------------------------------------------------------------------
# Custom y-axis
#-------------------------------------------------------------------------------------------------------

magnify_trans_log <- function(interval_low = 0.05, interval_high = 1,  reducer = 0.05, reducer2 = 8) {

  trans <- Vectorize(function(x, i_low = interval_low, i_high = interval_high, r = reducer, r2 = reducer2) {
    if(is.na(x) || (x >= i_low & x <= i_high)) {
      x
    } else if(x < i_low & !is.na(x)) {
      (log10(x / r)/r2 + i_low)
    } else {
      log10((x - i_high) / r + i_high)/r2
    }
  })

  inv <- Vectorize(function(x, i_low = interval_low, i_high = interval_high, r = reducer, r2 = reducer2) {
    if(is.na(x) || (x >= i_low & x <= i_high)) {
      x
    } else if(x < i_low & !is.na(x)) {
      10^(-(i_low - x)*r2)*r
    } else {
      i_high + 10^(x*r2)*r - i_high*r
    }
  })

  trans_new(name = 'customlog', transform = trans, inverse = inv, domain = c(1e-16, Inf))
}

#-------------------------------------------------------------------------------------------------------
# Create data
#-------------------------------------------------------------------------------------------------------

x <- seq(-1, 1, length.out = 1000)
y <- c(x[x<0] + 1, -x[x>0] + 1)

dat <- data.frame(
  x = x
  , y = y
)

#-------------------------------------------------------------------------------------------------------
# Plot using ggplot2
#-------------------------------------------------------------------------------------------------------

theme_set(theme_bw())
ggplot(dat, aes(x = x, y = y)) +
  geom_line(size = 1) +
  scale_y_continuous(
    , trans = magnify_trans_log(interval_low = 0.5, interval_high = 1, reducer = 0.5, reducer2 = 8)
    , breaks = c(0.001, 0.01, 0.1, 0.5, 0.6, 0.7, 0.8, 0.9, 1)
    , sec.axis = sec_axis(
      trans = ~.*(1/2)
      , breaks = c(0.001, 0.01, 0.1, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5)
    )
  ) + theme(
    axis.text.y=element_text(colour = "black", size=15)
  )

Это дает следующий сюжет:

ggplot_new

Маркировка вторичной оси Y является правильной для логарифмической части оси (ниже 0,5), но неверной для линейной части оси.

Если я устанавливаю ggplot2 3.0.0 с помощью

require(devtools)
install_version("ggplot2", version = "3.0.0", repos = "http://cran.us.r-project.org")

и запустить тот же код, что и выше, я получаю следующий график, который я хочу:

ggplot_old

Вопросы

  1. Есть ли способ исправить эту проблему в новейшей версии ggplot2 (3.1.0)? В идеале я хотел бы воздержаться от использования более старой версии ggplot2 (т.е. 3.0.0).
  2. Есть ли альтернативы sec_axis которые будут работать в этом случае?

Ответ 1

Вот решение, которое работает с ggplot2 версии 3.1.0 с использованием sec_axis(), и которое требует создания только одного графика. Мы по-прежнему используем sec_axis() как и раньше, но вместо того, чтобы масштабировать преобразование на 1/2 для вторичной оси, мы вместо этого масштабируем разрывы на вторичной оси.

В этом конкретном случае у нас это довольно просто, так как нам просто нужно умножить желаемые позиции точек останова на 2. Затем полученные точки останова будут правильно размещены как для логарифмической, так и для линейной частей вашего графика. После этого все, что нам нужно сделать, - это сопоставить разрывы с их желаемыми значениями. Это обходит проблему ggplot2 с размещением разрыва, когда он должен масштабировать смешанное преобразование, как мы делаем масштабирование сами. Грубый, но эффективный.

К сожалению, в настоящий момент, похоже, нет никаких других альтернатив sec_axis() (кроме dup_axis() который здесь мало поможет). Я был бы счастлив, чтобы быть исправленным в этом пункте, как бы то ни было! Удачи, и я надеюсь, что это решение окажется полезным для вас!

Вот код:

# Vector of desired breakpoints for secondary axis
sec_breaks <- c(0.001, 0.01, 0.1, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5)
# Vector of scaled breakpoints that we will actually add to the plot
scaled_breaks <- 2 * sec_breaks

ggplot(data = dat, aes(x = x, y = y)) +
  geom_line(size = 1) +
  scale_y_continuous(trans = magnify_trans_log(interval_low = 0.5,
                                               interval_high = 1,
                                               reducer = 0.5,
                                               reducer2 = 8),
                     breaks = c(0.001, 0.01, 0.1, 0.5, 0.6, 0.7, 0.8, 0.9, 1),
                     sec.axis = sec_axis(trans = ~.,
                                         breaks = scaled_breaks,
                                         labels = sprintf("%.3f", sec_breaks))) +
  theme_bw() +
  theme(axis.text.y=element_text(colour = "black", size=15))

И полученный сюжет:

enter image description here

Ответ 2

Можете ли вы создать два отдельных графика для разных диапазонов оси Y и сложить их вместе? У меня на ggplot2 3.1.0 работает следующее:

library(cowplot)

theme_set(theme_bw())

p.bottom <- ggplot(dat, aes(x = x, y = y)) +
  geom_line(size = 1) +
  scale_y_log10(breaks = c(0.001, 0.01, 0.1, 0.5),
                expand = c(0, 0),
                sec.axis = sec_axis(trans = ~ . * (1/2),
                                    breaks = c(0.001, 0.01, 0.1, 0.25))) +
  coord_cartesian(ylim = c(0.001, 0.5)) + # limit y-axis range
  theme(axis.text.y=element_text(colour = "black", size=15),
        axis.title.y = element_blank(),
        axis.ticks.length = unit(0, "pt"),
        plot.margin = unit(c(0, 5.5, 5.5, 5.5), "pt")) #remove any space above plot panel

p.top <- ggplot(dat, aes(x = x, y = y)) +
  geom_line(size = 1) +
  scale_y_continuous(breaks = c(0.6, 0.7, 0.8, 0.9, 1),
                     labels = function(y) sprintf("%.3f", y), #ensure same label format as p.bottom
                expand = c(0, 0),
                sec.axis = sec_axis(trans = ~ . * (1/2),
                                    breaks = c(0.3, 0.35, 0.4, 0.45, 0.5),
                                    labels = function(y) sprintf("%.3f", y))) +
  coord_cartesian(ylim = c(0.5, 1)) + # limit y-axis range
  theme(axis.text.y=element_text(colour = "black", size=15),
        axis.text.x = element_blank(),       # remove x-axis labels / ticks / title &
        axis.ticks.x = element_blank(),      # any space below the plot panel
        axis.title.x = element_blank(),
        axis.ticks.length = unit(0, "pt"),
        plot.margin = unit(c(5.5, 5.5, 0, 5.5), "pt"))

plot_grid(p.top, p.bottom, 
          align = "v", ncol = 1)

plot