tidyverse / ggplot2

An implementation of the Grammar of Graphics in R
https://ggplot2.tidyverse.org
Other
6.47k stars 2.02k forks source link

aspect ratio of plot not working with of space = "free" in facet_grid #3834

Closed dvaiman closed 3 years ago

dvaiman commented 4 years ago

Hi, in ggplot I want to set the aspect ratio of the plot to a certain number. space = "free"in facet_grid does not seem to work with theme(aspect.ratio = 1). Is there a workaround for this. Also I use coord_flip which hinders the use of coord_fixed. This is what I get:

Rplot09

library(ggplot2)

ggplot(mtcars, aes(hp, mpg)) +
  geom_point(aes()) +
  coord_flip()+
  theme(aspect.ratio = 1)+
  facet_grid(vars(cyl), space = "free")
yutannihilation commented 4 years ago

space = "free" has no effect unless the scales also vary, so probably the bellow example is more appropriate? In my understanding, there's no workaround and specifying aspect.ratio = 1 should be errored as specifying coord_fixed() does.

library(ggplot2)

p <- ggplot(mtcars, aes(hp, mpg)) +
  geom_point(aes()) +
  facet_grid(vars(cyl), scales = "free", space = "free")

# works
p

# fails properly
p + coord_fixed()
#> Error: coord_fixed doesn't support free scales

# do not fail, but the plot is strange
p + theme(aspect.ratio = 1)

Created on 2020-02-29 by the reprex package (v0.3.0)

dvaiman commented 4 years ago

Thanks for the input, I think your example more correctly describes the problem!

baderstine commented 2 years ago

@thomasp85

Ah bummer. This change breaks a perfectly fine plot. :(

MWE below... If I want to make a gridded heatmap plot with geom_tile(), y is a factor, and x is character there is really no inherent scale to either axis. However, the only way to guarantee that my output plot has square tiles, while dropping irrelevant categories, is to use theme(aspect.ratio = 1) in combination with facet_grid(..., scales="free_x", and space="free_x") (unless you have other ideas?). Now this plot is impossible because it is blocked by this new error message. Can the error be changed to a warning() instead so that it is not a blocking change? There are valid use cases for this combination.

Example:

y = factor(x=c(1:6), labels=letters[1:6])
x = c("cat","dog","house","farm","road","tree")
xgrp = c("animal","animal","building","building","infrastructure","nature")
xval = rnorm(6)

dat = data.frame(y,x,xgrp, stringsAsFactors=F)

# this plot has the correct aspect ratio, but it includes every single x category in every facet, regardless of whether a value is actually present: 
ggplot(dat, aes(y = y,  x = x)) +
  geom_tile(width = .8, height = .8) +
  theme(aspect.ratio = 1) +
  facet_grid(cols = vars(xgrp, x), drop=T) # , scales="free_x", space="free_x")

# this plot drops the irrelevant x categories in each facet, however my tiles are no longer guaranteed to be square: 
ggplot(dat, aes(y = y,  x = x)) +
  geom_tile(width = .8, height = .8) +
  # theme(aspect.ratio = 1) +
  facet_grid(cols = vars(xgrp, x), drop=T, scales="free_x", space="free_x")

# this plot is perfect (in a previous version of ggplot2), but now I'm not allowed to make it:
ggplot(dat, aes(y = y,  x = x)) +
  geom_tile(width = .8, height = .8) +
  theme(aspect.ratio = 1) +
  facet_grid(cols = vars(xgrp, x), drop=T, scales="free_x", space="free_x")
clauswilke commented 2 years ago

@baderstine I'm surprised that this ever worked. It's unclear in the general case what exactly the output should be. You can always make individual plots and stitch them together with patchwork. Shouldn't be that much more work. See below for a rough example. You'd have to put in a little more work to remove the y axis labels from the interior plots, but that's maybe 5 additional lines of code or so.

library(tidyverse)
library(patchwork)

y <- factor(x=c(1:6), labels=letters[1:6])
x <- c("cat","dog","house","farm","road","tree")
xgrp <- c("animal","animal","building","building","infrastructure","nature")

data <- tibble(y, x, xgrp)

make_plot <- function(data) {
  ggplot(data, aes(y = y,  x = x)) +
    geom_tile(width = .8, height = .8) +
    scale_y_discrete(limits = letters[1:6]) +
    coord_fixed() +
    facet_grid(cols = vars(xgrp, x))
}

data %>%
  mutate(
    group = interaction(x, xgrp)
  ) %>%
  nest(data = -group) %>%
  mutate(
    plots = map(data, ~make_plot(.x))
  ) %>%
  pull(plots) %>%
  wrap_plots(nrow = 1)

Created on 2021-10-21 by the reprex package (v2.0.0)

baderstine commented 2 years ago

Interesting... so something like the following does the trick (although this is digging into a separate patchwork issue https://github.com/thomasp85/patchwork/issues/150)

library(tidyverse)
library(patchwork)

y <- factor(x=c(1:6), labels=letters[1:6])
x <- c("cat","dog","house","farm","road","tree")
xgrp <- c("animal","animal","building","building","infrastructure","nature")

data <- tibble(y, x, xgrp)

make_plot <- function(data) {
  p <- ggplot(data, aes(y = y,  x = x)) +
    geom_tile(width = .8, height = .8) +
    scale_y_discrete(limits = letters[1:6]) +
    coord_fixed() +
    facet_grid(cols = vars(xgrp, x)) + 
    theme(axis.title = element_blank(), 
          axis.ticks.y = element_blank())
  if(data$plotnum != 1)
    p <- p + theme(axis.text.y = element_blank())
  p
}

data %>%
  mutate(
    group = interaction(x, xgrp),
    plotnum = row_number()
  ) %>%
  nest(data = -group) %>%
  mutate(
    plots = map(.x=data, .f=~make_plot(.x))
  ) %>%
  pull(plots) %>%
  wrap_plots(nrow = 1)
FaizanKhalidMohsin commented 2 years ago

I was just wondering is it possible to have this properly fixed?

baderstine commented 2 years ago

as in, restore the previous functionality when the x and y axes are discrete or what?

Gsmith535 commented 2 years ago

Yes, please!

FaizanKhalidMohsin commented 2 years ago

@baderstine @Gsmith535 Yes, please, can space = "free" correctly work for facet_grid().

baderstine commented 2 years ago

Yep it's a bummer that a plot is possible to render but instead we get an error message.

yutannihilation commented 2 years ago

restore the previous functionality when the x and y axes are discrete

I'm getting to feel this might sound right, though I'm still in confusion what's the appropriate behavior... Anyway, it's not a good idea to keep discussing on a closed issue. Could you file a new issue describing what's the problem and what would be the "fix", with a minimal reprex?

The rendered version of https://github.com/tidyverse/ggplot2/issues/3834#issuecomment-948966404 at v3.3.3 tag.

y <- factor(x = c(1:6), labels = letters[1:6])
x <- c("cat", "dog", "house", "farm", "road", "tree")
xgrp <- c("animal", "animal", "building", "building", "infrastructure", "nature")
xval <- rnorm(6)

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

devtools::load_all("~/GitHub/ggplot2/")
#> ℹ Loading ggplot2

# this plot has the correct aspect ratio, but it includes every single x category in every facet, regardless of whether a value is actually present:
ggplot(dat, aes(y = y, x = x)) +
  geom_tile(width = .8, height = .8) +
  theme(aspect.ratio = 1) +
  facet_grid(cols = vars(xgrp, x), drop = T) # , scales="free_x", space="free_x")


# this plot drops the irrelevant x categories in each facet, however my tiles are no longer guaranteed to be square:
ggplot(dat, aes(y = y, x = x)) +
  geom_tile(width = .8, height = .8) +
  # theme(aspect.ratio = 1) +
  facet_grid(cols = vars(xgrp, x), drop = T, scales = "free_x", space = "free_x")

# this plot is perfect (in a previous version of ggplot2), but now I'm not allowed to make it:
ggplot(dat, aes(y = y, x = x)) +
  geom_tile(width = .8, height = .8) +
  theme(aspect.ratio = 1) +
  facet_grid(cols = vars(xgrp, x), drop = T, scales = "free_x", space = "free_x")

Created on 2022-06-24 by the reprex package (v2.0.1)

FaizanKhalidMohsin commented 2 years ago

@baderstine @yutannihilation I was just wondering why is this issue closed? Is the proper thing to reopen this issue or to open a new issue. Thank you.

clauswilke commented 2 years ago

I think the real issue is that we (maybe just me) have confused coord_fixed() with the aspect.ratio setting in the theme at some places in the code. I thought they did the same, but they do not.

library(tidyverse)

df <- tibble(
  x = 1:3,
  y = 2*(1:3),
  z = 3*(1:3)
)

ggplot(df, aes(x, y)) +
  geom_point() +
  theme(aspect.ratio = 1)

ggplot(df, aes(x, y)) +
  geom_point() +
  coord_fixed()

Created on 2022-06-24 by the reprex package (v2.0.1)

Allowing coord_fixed() with free scales is not possible, because it creates facets of all different shapes and sizes and then we don't know how to tile them. But allowing a fixed aspect ratio for all facets should always be possible.

The new issue should focus on this point specifically.

yutannihilation commented 2 years ago

But allowing a fixed aspect ratio for all facets should always be possible.

In my understanding, aspect.ratio is applied per panel, so it should conflict with scales = "free" scales = "free" + space = "free" because it varies the aspect ratios, at least the scale is continuous, isn't it...?

yutannihilation commented 2 years ago

Are you suggesting

ggplot(mtcars, aes(hp, mpg)) +
  geom_point() +
  theme(aspect.ratio = 1) +
  facet_grid(vars(cyl), scales = "free", space = "free")

should draw something like this, ignoring space = "free"?

library(ggplot2)

l <- split(mtcars, mtcars$cyl)

plots <- purrr::imap(unname(l), \(x, i) {
  ggplot(x, aes(hp, mpg)) +
    theme(aspect.ratio = 1) +
    geom_point() +
    facet_grid(vars(cyl)) +
    if (i != 3L) theme(axis.text.x = element_blank(), axis.title.x = element_blank())
})

patchwork::wrap_plots(plots, ncol = 1)

Created on 2022-06-25 by the reprex package (v2.0.1)

clauswilke commented 2 years ago

@yutannihilation I think that's the request, yes, and it would be consistent with what theme(aspect.ratio = 1) does for a plot with a single panel.

And then this should also be properly documented, because right now if you read the documentation for aspect.ratio in theme() and for ratio in coord_fixed() they basically use the same language but apparently do entirely different things. There may also be other places in the code where this has been confused.

baderstine commented 2 years ago

@yutannihilation better version of the reprex:


devtools::install_version("ggplot2", "3.3.3")
library(ggplot2)

y <- factor(x = c(1:6), labels = letters[1:6])
x <- c("cat", "dog", "house", "farm", "road", "tree")
xgrp <- c("animal", "animal", "building", "building", "infrastructure", "nature")
xval <- rnorm(6)
dat <- data.frame(y, x, xgrp, stringsAsFactors = F)

# this plot has the correct aspect ratio, but it includes every single x category in every facet, regardless of whether a value is actually present:

ggplot(dat, aes(y = y, x = x)) +
  geom_tile(width = .8, height = .8) +
  theme(aspect.ratio = 1) +
  facet_grid(cols = vars(xgrp, x), drop = T) # , scales="free_x", space="free_x")

# this plot drops the irrelevant x categories in each facet, however my tiles are no longer guaranteed to be square:
ggplot(dat, aes(y = y, x = x)) +
  geom_tile(width = .8, height = .8) +
  # theme(aspect.ratio = 1) +
  facet_grid(cols = vars(xgrp, x), drop = T, scales = "free_x", space = "free_x")

# this plot is perfect (in ggplot2 v <=3.3.3), but now I'm not allowed to make it:
# I am allowed to supply a custom aspect ratio, so that I can make square tiles, and therefore makes a rather nice heatmap plot.

my.aspect.ratio = length(unique(dat$y))
ggplot(dat, aes(y = y, x = x)) +
  geom_tile(width = .8, height = .8) +
  theme(aspect.ratio = my.aspect.ratio) +
  facet_grid(cols = vars(xgrp, x), drop = T, scales = "free_x", space = "free_x")
yutannihilation commented 2 years ago

@clauswilke Sorry, I edited the comment above. Then, I found we can just remove space = "free".

library(ggplot2)

ggplot(mtcars, aes(hp, mpg)) +
  geom_point() +
  theme(aspect.ratio = 1) +
  facet_grid(vars(cyl), scales = "free")

Created on 2022-06-25 by the reprex package (v2.0.1)

@baderstine Would you mind rendering the reprex by yourself?

clauswilke commented 2 years ago

@yutannihilation Wait, does @baderstine just have to remove space = "free_x" to get the plot he wants?

yutannihilation commented 2 years ago

@baderstine Is this what you want?

library(ggplot2)

y <- factor(x = c(1:6), labels = letters[1:6])
x <- c("cat", "dog", "house", "farm", "road", "tree")
xgrp <- c("animal", "animal", "building", "building", "infrastructure", "nature")
xval <- rnorm(6)
dat <- data.frame(y, x, xgrp, stringsAsFactors = F)

my.aspect.ratio <- length(unique(dat$y))
ggplot(dat, aes(y = y, x = x)) +
  geom_tile(width = .8, height = .8) +
  theme(aspect.ratio = my.aspect.ratio) +
  facet_grid(cols = vars(xgrp, x), drop = T, scales = "free_x")

Created on 2022-06-25 by the reprex package (v2.0.1)

clauswilke commented 2 years ago

I think he wants the individual facets square rather than the overall plot.

baderstine commented 2 years ago

Yep, it's about the shape of the individual plotted elements within each facet, not about the shape of the overall plot.

baderstine commented 2 years ago

I guess i don't have a good enough reprex to demonstrate the issue. If I remove the space = "free_x" then individual facets that contain multiple x values are squished. I'll work on a new example.

baderstine commented 2 years ago

ok, this should help:

devtools::install_version("ggplot2", "3.3.3")
library(ggplot2)

ggplot(dat, aes(y = y, x = var)) +
  geom_tile(width = .8, height = .8) +
  theme(aspect.ratio = 1) +
  facet_grid(cols = vars(vargrp, varunt), drop = T) # , scales="free_x", space="free_x")

Plot 1: has the correct aspect ratio, but it includes every single x category in every facet, regardless of whether a value is actually present:

plot1

ggplot(dat, aes(y = y, x = var)) +
  geom_tile(width = .8, height = .8) +
  # theme(aspect.ratio = 1) +
  facet_grid(cols = vars(vargrp, varunt), drop = T, scales="free_x", space="free_x")

Plot 2: drops the irrelevant x categories in each facet, however my tiles are not the shape that i want (square):

plot2

my.aspect.ratio = length(unique(dat$y))

# without space = "free_x"
ggplot(dat, aes(y = y, x = var, color=xval)) +
  geom_tile(width = .8, height = .8) +
  theme(aspect.ratio = my.aspect.ratio) +
  facet_grid(cols = vars(vargrp, varunt), drop = T, scales="free_x")

Plot 3: drops irrelevant x categories and each facet's size is constant but the facets with more x categories are now squished... more categories, more squish. :( plot3

# this plot is perfect (in ggplot2 v <=3.3.3), but now I'm not allowed to make it:
# I supply a custom aspect ratio, so that I can change the shape (aspect ratio) of my resulting tiles.

ggplot(dat, aes(y = y, x = var, color=xval)) +
  geom_tile(width = .8, height = .8) +
  theme(aspect.ratio = my.aspect.ratio) +
  facet_grid(cols = vars(vargrp, varunt), drop = T, scales="free_x", space="free_x")

Plot 4: drops irrelevant x categories and allows the size of each x facet to vary based on how many categories are in it, and with a specific aspect ratio applied, i can now make nice little squarish boxes. plot4

yutannihilation commented 2 years ago

Thanks, now I understand your intention, but I don't feel aspect.ratio works as intended (again, I believe it should be reflected to the ratios of each panel). I agree it's nice if ggplot2 can provide some way to do it.

@clauswilke Do you think what the last example shows is the correct use of aspect.ratio and space = "free*"?

clauswilke commented 2 years ago

Re-reading the documentation of everything, I think aspect.ratio should set the overall aspect ratio of the plot and space = "free*" should work as shown. So no, the example output does not look right. I don't think the aspect ratio for the overall plot is correct. I think it's awkward to apply the aspect ratio to individual facets, in particular if there's an option space = "free*" that we also expect to work.

We also have to accept that sometimes we have to carefully calculate the width and height of a plot such that individual facets come out just right, and this can't always be the job of ggplot2, sometimes it's on the user. That's what I did for example to make sure that all the individual squares came out right in this chapter of my book: https://clauswilke.com/dataviz/directory-of-visualizations.html

yutannihilation commented 2 years ago

Hmm. thanks. I don't agree with the idea of the overall aspect ratio of the plot at the moment, but it might be just that I don't read the documentation enough. As it turned out we all have different opinions on this topic, probably a closed issue is too small for the discussion. But let me clarify before filing a new issue. Do you mean both of the following results are wrong as aspect.ratio sets the aspect ratio of the individual facets, not that of the whole plot? Or, are you discussing only about the cases with space = "free*"?

library(ggplot2)

d <- data.frame(
  x = c(1, 2, 3, 1, 1, 2),
  y = c(1, 2, 3, 2, 3, 2),
  g = rep(c("a", "b", "c"), times = c(3, 1, 2))
)

p <- ggplot(d, aes(x, y)) +
  geom_point() +
  theme(aspect.ratio = 1)

p + facet_wrap(vars(g))

p + facet_grid(vars(g))

Created on 2022-06-25 by the reprex package (v2.0.1)

clauswilke commented 2 years ago

Oh, sorry, maybe I misunderstood and was wrong about the aspect ratio. Are you saying it currently applies consistently to individual facets? Maybe that's fine then, except I don't know what it would/should do when the axis ranges are different in different facets.

yutannihilation commented 2 years ago

Are you saying it currently applies consistently to individual facets?

Yes, I believe that's the current behavior.

what it would/should do when the axis ranges are different in different facets.

In my understanding, as you commented on https://github.com/tidyverse/ggplot2/issues/3834#issuecomment-1165656653, the aspect ratio is about the frame of the plot/panel and irrelevant to the actual values inside. So, I think the same rule applies to scales = "free" without any problems (while it might not be always a good idea to use it especially when the scale is continuous).

clauswilke commented 2 years ago

Ok, that seems reasonable to me, but it would probably not produce the plot @baderstine was hoping for.

yutannihilation commented 2 years ago

Thanks. Yeah, let's think about what option is needed to make it possible again next...

yutannihilation commented 2 years ago

I was about to file a new issue, but it seems #4584 is the one.

Bigsealion commented 1 year ago

this plot is perfect (in ggplot2 v <=3.3.3), but now I'm not allowed to make it:

I supply a custom aspect ratio, so that I can change the shape (aspect ratio) of my resulting tiles.

ggplot(dat, aes(y = y, x = var, color=xval)) + geom_tile(width = .8, height = .8) + theme(aspect.ratio = my.aspect.ratio) + facet_grid(cols = vars(vargrp, varunt), drop = T, scales="free_x", space="free_x") Plot 4: drops irrelevant x categories and allows the size of each x facet to vary based on how many categories are in it, and with a specific aspect ratio applied, i can now make nice little squarish boxes.

Is there any way to get the same plot as plot4 in the new version of ggplot2 (e.g., 3.4.0)? Thanks!

smouksassi commented 1 year ago

have you tried ggforce ? library(ggforce) ggplot(dat, aes(y = y, x = x)) + geom_tile(width = .8, height = .8) + theme(aspect.ratio = 1) + facet_row(~ xgrp+ x, drop = T, scales="free_x")+ theme(aspect.ratio = 6)

venkmurthy commented 1 month ago

I believe the ggh4x package solves this problem if you use ggh4x::facet_grid2 with options space=free and scale=free along with theme(aspect.ratio=1