ggplot2 icon indicating copy to clipboard operation
ggplot2 copied to clipboard

scale_*_reverse() is ignored when limits are set on Coord

Open vitor-mendes-iq opened this issue 4 years ago • 32 comments

Since I updated R to version 4.0 the scale_x_reverse () function has stopped working together with renderPlot () or renderPlotly( ), which I use in a shiny app that I develop ... However when I test scale_x_reverse () out of the render it works fine. It is worth mentioning that in all other versions of R it was working normally in both Windowns (7, 8 and 10) and Linux (Ubunto, mint, Debian, Suse). Now I am use Windowns 10, R (4.0), RStudio (1.2.5042).

Can someone help me?


Brief description of the problem

# ` # Variables
  peran_multi <<- 0
  ysup_multi <- max(NMRData[1,])
  yinf_multi <- min(NMRData[1,])
  ysup_multi <- ysup_multi + ysup_multi*0.03
  chkzoom_multi <<- 1
  idb_multi <<- 0


  # Data.frame
  testy_multi <<- data.frame(Chemical_Shift=CS_values_real[1,],Spectrum=NMRData[1,])
  ranges_multi <- reactiveValues(x = c((min(testy_multi$Chemical_Shift)), max(testy_multi$Chemical_Shift)), y = c(-100,ysup_multi))
  # ranges_multi_plot <- reactiveValues(x = sort(c((min(testy_multi$Chemical_Shift)), max(testy_multi$Chemical_Shift)), decreasing = TRUE), y = c(yinf_multi,ysup_multi))
  spectrums_multi <- reactiveValues(dat = data.frame(Chemical_Shift=CS_values_real[1,],Spectrum=NMRData[1,]))

  # Plot
    output$plot_multi <- plotly::renderPlotly({

      ggplot2::ggplot(spectrums_multi$dat,ggplot2::aes(Chemical_Shift,Spectrum)) + ggplot2::geom_line(color='blue') + ggplot2::scale_x_reverse() +

        ggplot2::coord_cartesian(xlim = ranges_multi$x, ylim = ranges_multi$y, expand = FALSE) +

        ggplot2::theme(axis.text.x = ggplot2::element_text(size = 12, color = "#000000"),
                       axis.text.y = ggplot2::element_text(size = 12, color = "#000000"),
                       title = ggplot2::element_text(face = "bold", color = "#000000", size = 17),
                       axis.title = ggplot2::element_text(face = "bold", color = "#000000", size = 15)
                       ) +

        ggplot2::labs(x = "Chemical Shift", y = "Intensity")

    })
`

vitor-mendes-iq avatar May 22 '20 13:05 vitor-mendes-iq

Could you ask on plotly's repo (with a minimal reproducible example)? I'm not sure if this is what should be fixed on plotly's side, but we have too little knowledge about plotly's internals to identify the cause of the problem.

yutannihilation avatar May 22 '20 13:05 yutannihilation

Sorry yutannihilation for not put a reproducible example. I am a new programmer and this is my first question on forum. try to see this new reproducible example now

`library(shiny)
library(ggplot2)
library(plotly)


ui <- fluidPage(
        fluidRow(align = "center",
          mainPanel(
           plotly::plotlyOutput("plot_multi")
          )
        )
)
  

server <- function(input, output) {




# Data.frame
z <<- runif(1001, min=0, max=1000000)
f <<- seq(0,10,0.01)
testy_multi <<- data.frame(Chemical_Shift=f,Spectrum=z)
ranges_multi <- reactiveValues(x = c((min(testy_multi$Chemical_Shift)), max(testy_multi$Chemical_Shift)), y = c(-100,1000000))
spectrums_multi <- reactiveValues(dat = data.frame(Chemical_Shift=f,Spectrum=z))

# Plot
output$plot_multi <- plotly::renderPlotly({
  
  ggplot2::ggplot(spectrums_multi$dat,ggplot2::aes(Chemical_Shift,Spectrum)) + ggplot2::geom_line(color='blue') + 
    
    ggplot2::coord_cartesian(xlim = ranges_multi$x, ylim = ranges_multi$y, expand = FALSE) + ggplot2::scale_x_reverse() +
    
    ggplot2::theme(axis.text.x = ggplot2::element_text(size = 12, color = "#000000"),
                   axis.text.y = ggplot2::element_text(size = 12, color = "#000000"),
                   title = ggplot2::element_text(face = "bold", color = "#000000", size = 17),
                   axis.title = ggplot2::element_text(face = "bold", color = "#000000", size = 15)
    ) +
    
    ggplot2::labs(x = "Chemical Shift", y = "Intensity")
  
})

}

shinyApp(ui = ui, server = server)
`

vitor-mendes-iq avatar May 22 '20 14:05 vitor-mendes-iq

Thanks for the reproducible example, I confirmed your example works with ggplot2 v3.2.1 but won't with v3.3.0 (or the current master). But, again, could you file this issue on plotly's repo? I wasn't complaining about the lack of reproducibility, I'm just saying we cannot fix this (at least without any help from plotly's maintainers).

yutannihilation avatar May 22 '20 15:05 yutannihilation

Thanks for responding yutannihilation , I agree with what you said about going to plotly and now I go to the forum, but I have the same problem using renderPlot () which is not from plotly. This was the reason for looking for this forum before.

vitor-mendes-iq avatar May 22 '20 15:05 vitor-mendes-iq

@vitor-mendes-iq Ah, OK. I misunderstood your point, sorry... Here's a minimal reprex:

library(ggplot2)

d <- data.frame(x = 1:2)

p <- ggplot(d, aes(x, x)) +
  geom_point() +
  scale_x_reverse()

p

p + coord_cartesian(xlim = range(d$x))

Created on 2020-05-23 by the reprex package (v0.3.0)

The reversed x scale is ignored because you specifies xlim in the order.

yutannihilation avatar May 22 '20 16:05 yutannihilation

@thomasp85 Sorry, at the time of implementing #3958, I thought this is the correct behaviour, but I found it didn't behave so in the past. Do you think I need to fix this before releasing v3.3.1?

yutannihilation avatar May 22 '20 16:05 yutannihilation

I would expect all these plots to produce the same result. That is not currently the case. I'm particularly confused by the cases that remove the points. Note that this is not a rounding error; if I make the limits broader the points are still removed.

library(ggplot2)

d <- data.frame(x = 1:2)

p <- ggplot(d, aes(x, x)) + geom_point()
  
p + xlim(2, 1)

p + scale_x_continuous(limits = c(2, 1))
#> Warning: Removed 2 rows containing missing values (geom_point).

p + coord_cartesian(xlim = c(2, 1))

p + scale_x_reverse()

p + scale_x_reverse(limits = c(1, 2))
#> Warning: Removed 2 rows containing missing values (geom_point).

p + scale_x_reverse() + coord_cartesian(xlim = c(1, 2))

Created on 2020-05-22 by the reprex package (v0.3.0)

clauswilke avatar May 22 '20 17:05 clauswilke

The difference between xlim() and scale_x_continuous() is that the former automatically sets the trans to reverse when the limits are specified in the reverse order.

https://github.com/tidyverse/ggplot2/blob/48268385c414702bc343e46d51144846d39232fa/R/limits.r#L118-L122

This heuristic made me think that the order of the limit do matter, and maybe the user is supposed to specify them in the same order of the scale...? Sorry, I'm still confused.

yutannihilation avatar May 23 '20 02:05 yutannihilation

I guess this is also a consistent principle: If explicit limits are provided, they always supersede the natural direction of the scale.

clauswilke avatar May 23 '20 04:05 clauswilke

Ah, thanks, makes sense. Then, we have two issues to consider:

  1. When scale limits are provided in the opposite order to the direction of the scale, the limits are ignored and all data are removed (This should be fixed).
  2. When coord limits are provided in the opposite order to the direction of the scale, the limits supersede the natural direction of the scale. This seems to do the right thing, but it didn't behave so before v3.2.1 (v3.3.0 was broken here).

The examples below shows issue 2. What I was wondering is whether we should fall back to the behaviour of v3.2.1, but now I feel the current behaviour is OK.

d <- data.frame(x = 1:2)

ggplot(d, aes(x, x)) +
  geom_point() +
  scale_x_reverse() +
  coord_cartesian(xlim = range(d$x))

v3.2.1

image

v3.3.0

image

current master

image

yutannihilation avatar May 23 '20 06:05 yutannihilation

@yutannihilation You are right, but version 3.3 does not have scale_x_reverse () responding within renderPlot () and renderplotly (), while version 3.2.1 does. Out of renders 3.3 works perfectly

vitor-mendes-iq avatar May 23 '20 11:05 vitor-mendes-iq

There's another issue to consider: scale_*_reverse() uses reverse_trans(). But what if we use a scale with a different transformation (e.g., scale_*_log10()) and then specify reversed limits? The user would expect this to work also.

clauswilke avatar May 23 '20 14:05 clauswilke

@vitor-mendes-iq As you can see above, v3.3.0 has a problem about scale_*_reverse() no matter it's used in Shiny or not. Btw, you can change this line

    ggplot2::coord_cartesian(xlim = ranges_multi$x, ylim = ranges_multi$y, expand = FALSE) + ggplot2::scale_x_reverse() +

to this (I think we don't reach a consensus whether this is the right way or not, but if you need an immediate workaround, this should work, at least until we make some change):

    ggplot2::coord_cartesian(xlim = rev(ranges_multi$x), ylim = ranges_multi$y, expand = FALSE) + 

@clauswilke Yeah, the user does expect so... Do you mean it should work? Currently we can choose only one trans, but it might not be very clear to the users (e.g. https://github.com/tidyverse/ggplot2/issues/4014).

yutannihilation avatar May 24 '20 08:05 yutannihilation

Yes, the more I think about it, the more I believe reversing a scale should be handled separately from transformations.

clauswilke avatar May 24 '20 16:05 clauswilke

I got it. Sounds good to me!

yutannihilation avatar May 24 '20 22:05 yutannihilation

I ran into this last week. Not sure I completely understand the path forward but here an example from my use case.

this works as expected

library(ggplot2)
 ggplot(mtcars, aes(x=disp, y=mpg))+geom_point()+coord_cartesian(xlim=c(0,200))

but if you add a scale_x_reverse() it doesn't do anything (and oddly removes the x axis text!?)

 ggplot(mtcars, aes(x=disp, y=mpg))+geom_point()+scale_x_reverse()+coord_cartesian(xlim=c(0,200))

image

instead I had to do

 ggplot(mtcars, aes(x=disp, y=mpg))+geom_point()+scale_x_reverse()+coord_cartesian(xlim=c(200,0))

from a user pov I don't expect to have to change my coord_cartesion limits order just because I reversed the scale direction. that would be unnecessarily verbose. I think this happened during the CoordCartesian refactor because it doesn't happen with 3.2.1. I was a bit surprised because the release notes explicitly state there were no user facing changes :)

I think the answer is yes, but is the proposed solution to revert to the old behavior?

Yes, the more I think about it, the more I believe reversing a scale should be handled separately from transformations.

>sessionInfo()
R version 3.6.3 (2020-02-29)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 10 x64 (build 18363)

Matrix products: default

locale:
[1] LC_COLLATE=English_United States.1252  LC_CTYPE=English_United States.1252   
[3] LC_MONETARY=English_United States.1252 LC_NUMERIC=C                          
[5] LC_TIME=English_United States.1252    

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] ggplot2_3.3.1  devtools_2.3.0 usethis_1.6.1 

loaded via a namespace (and not attached):
 [1] Rcpp_1.0.4.6      pillar_1.4.4      compiler_3.6.3    prettyunits_1.1.1 remotes_2.1.1    
 [6] tools_3.6.3       testthat_2.3.2    digest_0.6.25     pkgbuild_1.0.8    packrat_0.5.0    
[11] pkgload_1.0.2     tibble_3.0.1      lifecycle_0.2.0   memoise_1.1.0     evaluate_0.14    
[16] gtable_0.3.0      pkgconfig_2.0.3   rlang_0.4.6       cli_2.0.2         rstudioapi_0.11  
[21] yaml_2.2.1        xfun_0.14         dplyr_0.8.5       withr_2.2.0       knitr_1.28       
[26] vctrs_0.3.0       desc_1.2.0        fs_1.4.1          rsthemes_0.0.7    tidyselect_1.1.0 
[31] rprojroot_1.3-2   grid_3.6.3        glue_1.4.1        R6_2.4.1          processx_3.4.2   
[36] fansi_0.4.1       rmarkdown_2.2     sessioninfo_1.1.1 farver_2.0.3      purrr_0.3.4      
[41] callr_3.4.3       magrittr_1.5      backports_1.1.7   scales_1.1.1      ps_1.3.3         
[46] ellipsis_0.3.1    htmltools_0.4.0   assertthat_0.2.1  colorspace_1.4-1  labeling_0.3     
[51] munsell_0.5.0     crayon_1.3.4     

dschneiderch avatar Jun 01 '20 15:06 dschneiderch

from a user pov I don't expect to have to change my coord_cartesion limits order

You might not expect, but another user might. I'm yet to figure out what is the correct semantics here... Anyway thanks for your feedback.

yutannihilation avatar Jun 07 '20 01:06 yutannihilation

(Mainly a note to self) coord_sf() seems to ignore reversed limits. Should it respect?

library(ggplot2)
library(patchwork)

nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)

p <- ggplot(nc) +
  geom_sf(aes(fill = AREA))

p1 <- p + coord_sf(xlim = c(-76, -82))
p2 <- p + coord_sf(xlim = c(-82, -76))

p1 / p2

Created on 2020-06-28 by the reprex package (v0.3.0)

yutannihilation avatar Jun 28 '20 06:06 yutannihilation

I think we can safely ignore this unless it is dead simple. The application of reversed spatial coords is doubtful

thomasp85 avatar Jun 28 '20 08:06 thomasp85

Agreed, I think it's fine coord_sf() ignores reversed limits. Only concern is that it might look inconsistent with the current behaviour of coord_cartesian().

yutannihilation avatar Jun 28 '20 09:06 yutannihilation

I don’t see a big issue in simply documenting this lack of functionality for coord_sf()

thomasp85 avatar Jun 28 '20 15:06 thomasp85

I agree it's not a big issue. Thinking this again, maybe we should start from assuming the reversing functionality is a special feature of CoodCartesian? Apparently it's up to Coord how to treat the limits.

yutannihilation avatar Jun 28 '20 15:06 yutannihilation

Yeah, I think that sounds reasonable

thomasp85 avatar Jun 28 '20 15:06 thomasp85

Thanks, I think I got less confused now...

yutannihilation avatar Jun 28 '20 16:06 yutannihilation

Looks like you've got this all sorted. I just wanted to add that I wouldn't even know how to implement coordinate reversion in coord_sf() in the general case, because projections can do pretty arbitrary things to the coordinate system, so it's not even clear what numbers to revert.

One issue that may be useful for coord_sf() (but surprisingly difficult to implement) is that for long-lat coordinate systems the order of the x limits could specify which way round the globe you go. I.e., -178:178 would be a map of the entire world excluding a little sliver around the international date line, but 178:-178 would be a map of just the region around the international date line.

clauswilke avatar Jun 28 '20 16:06 clauswilke

That would indeed be super nice, but I think we should wait until someone super motivated comes along. It seems like the cost/benefit of this is pretty low

thomasp85 avatar Jun 28 '20 16:06 thomasp85

Have we aligned around the idea of separating scale reversion out of the transformation completely? I think this is the correct approach as the current system is way too limited and has resulted in some nonsensical code paths to work around the limitations?

thomasp85 avatar Aug 31 '20 10:08 thomasp85

I think so, yes.

clauswilke avatar Aug 31 '20 14:08 clauswilke

I'm not sure whether the thread has drifted away, or I might be missing something, but the following still shows an unintended behavior:

library(dplyr, warn.conflicts = FALSE)
library(ggplot2)
library(tibble)

mtcars %>%
  rownames_to_column("cars") %>%
  ggplot(aes(x = mpg, y = cars)) +
  geom_col() +
  scale_x_reverse() +    # <------------------I added this, then why does the plot remain left-to-right?
  coord_cartesian(xlim = c(10, 30)) # <------- had I taken away this line, the plot would have been reversed to right-to-left

Created on 2021-08-16 by the reprex package (v2.0.0)

Is there any conclusion regarding this?

emmansh avatar Aug 16 '21 12:08 emmansh

I think it might be possible to implement reverse scales implicitly by exposing the rescaler argument on continuous position scales. This argument is available in continuous_scale(), so the internal changes needed would be pretty minimal. One benefit might be that it circumvents the ambiguity of setting the limits in the reverse order in the coord/scale.

An example with a date scale, that currently doesn't have a reverse variant (as mentioned in #4014).

library(ggplot2)
library(scales)

# Hacky workaround for demo purposes
scale_x_date2 <- function(..., rescaler) {
  x <- scale_x_date(...)
  x$rescaler <- rescaler
  x
}

# The default `to` argument of the `rescale()` function is `c(0, 1)`.
# Here, we reverse that.
invert_scale <- function(x, to = c(1, 0), from = range(x)) {
  rescale(x, to, from)
}

ggplot(economics, aes(date, unemploy)) +
  geom_line() +
  scale_x_date2(rescaler = invert_scale)

To show that it plays nice with coord limits:

ggplot(economics, aes(date, unemploy)) +
  geom_line() +
  scale_x_date2(rescaler = invert_scale) +
  coord_cartesian(xlim = as.Date(c("1990-01-01", "1980-01-01")))

Simultaneous with another transformation:

scale_y_continuous2 <- function(..., rescaler) {
  x <- scale_y_continuous(...)
  x$rescaler <- rescaler
  x
}

ggplot(pressure, aes(temperature, pressure)) +
  geom_point() +
  scale_y_continuous2(trans = "log10", rescaler = invert_scale)

Created on 2021-09-02 by the reprex package (v1.0.0)

One limitation as far as I know is that coord_sf()$transform and coord_trans()$transform bypass the scales' rescaler function. So these coords wouldn't work with an exposed rescaler argument, but they currently don't work well with scale_{x/y}_reverse() either (#4206 for example, or try coord_trans(y = "log10") with scale_y_reverse()).

teunbrand avatar Sep 02 '21 17:09 teunbrand