Как я могу прикрепить список пользовательских цветовых палитр к списку объектов ggplot, не печатая каждый объект графика на экране?

У меня есть кадр данных в R, в котором несколько столбцов являются факторами. Я хотел бы создать ряд гистограмм, показывающих относительные размеры каждого из уровней фактора. Я хочу связать свои собственные цветовые палитры с каждым из факторов, а затем настроить окончательный макет всех столбцов и легенд с помощью gridExtra.

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

library(ggplot2)
library(grDevices)
library(gridExtra)

# Define some dummy data and put it in a data frame
fruit <- factor(c("apple", "orange", "pear", "pear", "pear",
                  "orange", "apple", "apple", "apple", "pear"))
cheese <- factor(c("cheddar", "mozarella", "gruyere", "gruyere", "gouda",
                   "parmesan", "gruyere", "gouda", "mozarella", "cheddar"))
mydata <- data.frame(fruit, cheese)
mydata$dummy <- 0

# Define some custom color schemes
foodclrs <- list()
# Plot the fruit factor in shades of red
h <- c(0.0,  0.0,  0.0)
s <- c(0.95, 0.85, 0.45)
v <- c(0.45, 0.85, 0.95)
foodclrs[[1]] <- hsv(h, s, v)
# Plot the cheese factor in shades of green
h <- c(0.33, 0.33, 0.33, 0.33, 0.33)
s <- c(0.95, 0.93, 0.85, 0.69, 0.45)
v <- c(0.45, 0.69, 0.85, 0.93, 0.95)
foodclrs[[2]] <- hsv(h, s, v)

# Create vectors with individualized text for each plot
bsiz=20
fillvars <- c("fruit", "cheese")
xlabels <- c("Fruits", "Cheeses")
lgdlabels <- c("Types of Fruit", "Types of Cheese")

# Generate a list of plots
plots <- list()
for (ii in 1:2) {
  plots[[ii]] <- ggplot(data=mydata) + 
    geom_bar(aes_string(x="dummy", fill=fillvars[ii]),
             position=position_stack(reverse=TRUE)) +
    scale_fill_manual(values=foodclrs[[ii]], drop=FALSE) +
    theme_bw(base_size=bsiz) +
    labs(x=xlabels[ii], y="") + 
    theme(axis.ticks.y=element_blank(),
          axis.text.y=element_blank()) +
    guides(fill=guide_legend(title=lgdlabels[ii])) +
    coord_flip()
#  print(plots[[ii]])
}

# Print the plots on my own custom-shaped grid
print(grid.arrange(plots[[1]], plots[[2]], ncol=1, nrow=2))

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

Это не то, чего я ожидал: цветовая палитра для верхней гистограммы должна была представлять собой диапазон оттенков красного. Кажется, что, хотя я изначально определил объект графика plots[[1]] так, чтобы с ним была связана красная цветовая палитра, когда я действительно пошел его печатать, либо R, либо ggplot2 (я не уверен, что именно) решили использовать самую последнюю цветовую палитру. вместо; то есть тот, который связан с plots[[2]].

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

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

Другими словами, как получилось, что вызов оператора «print» в правильный момент внутри цикла for приводит к тому, что цветовая палитра становится постоянно привязанной к каждому объекту ggplot, в то время как в противном случае этого бы не произошло?

Что на самом деле происходит здесь, так сказать, «под капотом»? А также, есть ли менее неуклюжий способ, которым я мог бы исправить проблему? Например, есть ли какая-то другая функция, которую я мог бы вызвать вместо print(), чтобы цветовая палитра правильно прикреплялась к каждому объекту графика, не создавая кучу отдельных «фиктивных» графиков, которые мне на самом деле не нужны?


person stachyra    schedule 11.09.2017    source источник
comment
вы можете хранить гробы вместо графиков, используя ggplotGrob().   -  person baptiste    schedule 12.09.2017
comment
Судя по вашему профилю, вы на самом деле являетесь основным автором и сопровождающим пакет gridExtra? Спасибо что нашли время ответить; Я знаю, что ты, должно быть, очень занят! FWIW, я думаю, что на самом деле я предпочитаю ваше предложение использовать функцию ggplotGrob(), даже больше, чем мне нравится ваш официальный ответ - это кажется проще и чище, чем инкапсуляция вызова ggplot внутри функции и использование purrr::pmap() для реализации цикла . Тем не менее, я проголосовал за них обоих, поскольку оба пути казались допустимыми решениями.   -  person stachyra    schedule 12.09.2017


Ответы (2)


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

f <- function(var, fill, xlab, lab){
  force(fill)
  ggplot(data=mydata) + 
    geom_bar(aes_string(x="dummy", fill=var),
             position=position_stack(reverse=TRUE)) +
    scale_fill_manual(values=fill) +
    theme_bw(base_size=bsiz) +
    labs(x=xlab, y="") +
    theme(axis.ticks.y=element_blank(),
          axis.text.y=element_blank()) +
    guides(fill=guide_legend(title=lab)) +
    coord_flip()
}

pl <- purrr::pmap(.f = f, 
                  .l = list(var=fillvars, fill=foodclrs,
                            xlab=xlabels,lab=lgdlabels))

grid.arrange(grobs=pl, nrow=2)
person baptiste    schedule 12.09.2017
comment
На основании того, что вы заключили вызов ggplot() внутри функции, а затем, кроме того, нужно было использовать force(), я подозреваю, что странное поведение в моем примере связано с какой-то причудой ленивую оценку. Возможно, внутри объекта ggplot есть что-то, что обычно оценивается лениво, и причина, по которой функция print() решает проблему, заключается в том, что она каким-то образом вызывает вычисление? - person stachyra; 12.09.2017
comment
В любом случае, спасибо за это; Раньше я не знал, что в R реализованы ленивые вычисления; попытка понять, что на самом деле делает функция force() в этом примере, привела меня к чтению некоторых более неясных/сложных деталей о том, как работает R. - person stachyra; 12.09.2017

Объяснение того, что происходит под капотом: вы указали значения шкалы как foodclrs[[ii]]. Все объекты графика оценивались в конце цикла с текущим (т.е. последним) значением ii.

Обходной путь 1. Предполагая, что вы заранее знаете, сколько цветов вам нужно из каждой палитры, один из возможных обходных путей – явно назвать их все в одной объединенной палитре:

combined.foodclrs <- c(foodclrs[[1]][seq_along(levels(fruit))], 
                       foodclrs[[2]][seq_along(levels(cheese))])
names(combined.foodclrs) <- c(levels(fruit), levels(cheese))

> combined.foodclrs
    apple    orange      pear   cheddar     gouda   gruyere mozarella  parmesan 
"#730606" "#D92121" "#F28585" "#087306" "#10B00C" "#24D921" "#4DED4A" "#87F285" 

Затем вы можете использовать комбинированную палитру в цикле и получать правильные цвета на каждом графике:

plots <- list()
for (ii in 1:2) {
  plots[[ii]] <- ggplot(data=mydata) + 
    geom_bar(aes_string(x="dummy", fill=fillvars[ii]),
             position=position_stack(reverse=TRUE)) +
    scale_fill_manual(values=combined.foodclrs, drop=FALSE) + #one combined palette
    theme_bw(base_size=bsiz) +
    labs(x=xlabels[ii], y="") + 
    theme(axis.ticks.y=element_blank(),
          axis.text.y=element_blank()) +
    guides(fill=guide_legend(title=lgdlabels[ii])) +
    coord_flip()
}

print(grid.arrange(plots[[1]], plots[[2]], ncol=1, nrow=2))

Обходной путь 2: согласно комментарию @baptiste, вы можете хранить их с помощью grobs:

plots <- list()
for (ii in 1:2) {
  p <- ggplot(data=mydata) + 
    geom_bar(aes_string(x="dummy", fill=fillvars[ii]),
             position=position_stack(reverse=TRUE)) +
    scale_fill_manual(values=foodclrs[[ii]], drop=FALSE) +
    theme_bw(base_size=bsiz) +
    labs(x=xlabels[ii], y="") + 
    theme(axis.ticks.y=element_blank(),
          axis.text.y=element_blank()) +
    guides(fill=guide_legend(title=lgdlabels[ii])) +
    coord_flip()
  plots[[ii]] <- ggplotGrob(p)
}

print(grid.arrange(plots[[1]], plots[[2]], ncol=1, nrow=2))

сюжет

(Вывод одинаков для обоих методов)

person Z.Lin    schedule 12.09.2017
comment
Я не такой большой поклонник обходного пути № 1, поскольку он, кажется, создает ненужную связь между двумя графиками, которая на самом деле не должна существовать. Однако обходной путь № 2 — это именно то, что я искал — очень чистое и простое; выполняет свою работу с минимальными изменениями по сравнению с исходным дизайном. Это также заставило меня прочитать о том, что такое объект grob (в отличие от объекта сюжета). - person stachyra; 12.09.2017