ralphagganimategeom-sf

"Replacement has x rows" error in code that previously worked (geom_sf/gganimate/shadow_mark)


I have data with observations of species at different locations across multiple years. Some years don't have any data. I wanted to create an animated map that included the years for which there's no data, and I succeeded at doing so, but it appears only temporarily.

Here's some reprex:

library(sf)
library(ggplot2)
library(gganimate)
library(transformr)
library(tidyr)

rb <- data.frame(YEAR = c(1995, 2000, 2000, 2001, 2002, 2002, 2002, 2004), 
                 TOTAL = c(5, 10, 8, 14, 15, 12, 16, 15),
                 LONG = c(runif(8, -80, -78)),
                 LAT = c(runif(8, 38, 40)))

Filling in the missing years with a random set of coordinates within the range of my coordinates:

rb.zero.df <- data.frame(rb %>% 
                           complete(YEAR = full_seq(1995:2004, 1), 
                                    fill = list(LONG = -78.3893, 
                                                LAT = 39.63938, 
                                                TOTAL = 0)))

rb.zero <- st_as_sf(rb.zero.df, coords = c("LONG", "LAT"), crs = 4326)

Then using ifelse for alpha in aes and shadow_mark to make sure the zero points don't show up:

rb.zero.map <- ggplot() +
  geom_sf(data = rb.zero, aes(size = TOTAL, alpha = ifelse(TOTAL == 0, 0, 1))) +
  scale_alpha(range = c(0,1)) +
  guides(alpha = "none") +
  transition_states(YEAR, transition_length = 0, state_length = 1) +
  ggtitle("{closest_state}") +
  shadow_mark(alpha = ifelse(rb.zero$TOTAL == 0, 0, 0.25))
animate(rb.zero.map)

This used to work, but now it produces this error:

Error in `[[<-.data.frame`(`*tmp*`, i, value = c(0.25, 0, 0, 0, 0, 0.25,  : 
  replacement has 13 rows, data has 12

I don't know how to interpret this error in this context.

If I take the ifelse out of shadow_mark, the code works.

My session info:

R version 4.3.2 (2023-10-31)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Ventura 13.3.1

Matrix products: default
BLAS:   /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.11.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/New_York
tzcode source: internal

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

other attached packages:
 [1] transformr_0.1.3      gifski_1.12.0-2       gganimate_1.0.7       tidyr_1.3.0          
 [5] viridis_0.6.4         viridisLite_0.4.2     spData_2.3.0          dplyr_1.1.4          
 [9] terra_1.7-65          sf_1.0-15             ggplot2_3.4.4         leaflet.extras2_1.2.2
[13] leaflet_2.2.1         tmap_3.99.9000       

loaded via a namespace (and not attached):
 [1] DBI_1.2.1               gridExtra_2.3           tmaptools_3.1-1         remotes_2.4.2.1        
 [5] rlang_1.1.3             magrittr_2.0.3          e1071_1.7-14            compiler_4.3.2         
 [9] mgcv_1.9-0              png_0.1-8               vctrs_0.6.5             stringr_1.5.1          
[13] profvis_0.3.8           pkgconfig_2.0.3         crayon_1.5.2            fastmap_1.1.1          
[17] ellipsis_0.3.2          labeling_0.4.3          lwgeom_0.2-13           leafem_0.2.3           
[21] utf8_1.2.4              promises_1.2.1          sessioninfo_1.2.2       purrr_1.0.2            
[25] cachem_1.0.8            progress_1.2.3          later_1.3.2             tweenr_2.0.2           
[29] parallel_4.3.2          prettyunits_1.2.0       R6_2.5.1                stringi_1.8.3          
[33] RColorBrewer_1.1-3      pkgload_1.3.4           stars_0.6-4             Rcpp_1.0.12            
[37] usethis_2.2.2           base64enc_0.1-3         leaflet.providers_2.0.0 Matrix_1.6-1.1         
[41] httpuv_1.6.13           splines_4.3.2           tidyselect_1.2.0        rstudioapi_0.15.0      
[45] dichromat_2.0-0.1       abind_1.4-5             codetools_0.2-19        miniUI_0.1.1.1         
[49] pkgbuild_1.4.3          lattice_0.21-9          tibble_3.2.1            leafsync_0.1.0         
[53] plyr_1.8.9              shiny_1.8.0             withr_3.0.0             units_0.8-5            
[57] proxy_0.4-27            urlchecker_1.0.1        lpSolve_5.6.20          pillar_1.9.0           
[61] KernSmooth_2.23-22      generics_0.1.3          sp_2.1-2                hms_1.1.3              
[65] munsell_0.5.0           scales_1.3.0            xtable_1.8-4            class_7.3-22           
[69] glue_1.7.0              tools_4.3.2             leaflegend_1.2.0        data.table_1.14.10     
[73] fs_1.6.3                XML_3.99-0.16.1         grid_4.3.2              crosstalk_1.2.1        
[77] devtools_2.4.5          colorspace_2.1-0        nlme_3.1-163            cols4all_0.7           
[81] raster_3.6-26           cli_3.6.2               fansi_1.0.6             gtable_0.3.4           
[85] digest_0.6.34           widgetframe_0.3.1       classInt_0.4-10         htmlwidgets_1.6.4      
[89] farver_2.1.1            memoise_2.0.1           htmltools_0.5.7         lifecycle_1.0.4        
[93] mime_0.12 

I have no clue why this code previously worked and now doesn't - the data and the code are the same. I'd appreciate thoughts on this and/or alternate solutions to the missing years problem. Thank you!


Solution

  • TL;DR

    Use

    shadow_mark(alpha = alpha / 4)
    

    instead of

    shadow_mark(alpha = ifelse(rb.zero$TOTAL == 0, 0, 0.25))
    

    Preface

    When I tried your example I got a different error:

    Error: arguments have different crs

    But this seems to be a bug in gganimate and the temporary fix was to remove the crs altogether.

    Explanation of the issue

    To your problem at hand, you are using shadow_mark not in its intended way. If you debug the train function of ShadowMark(debug(get("train", ShadowMark))) you will see that whatever you pass to shadow_mark will be evaluated in the context of the data slice which is shown in the current animation step.

    I am not deep into the internals of gganimate but apparently they split the original data for each slice and do their magic.

    The important bit is, that at a certain step the "current" data is just a subset of the whole dataset (enriched with aesthetics data), that is, it contains only a subset of the original rows.

    In this context evaluating ifelse(rb.zero$TOTAL == 0, 0, 0.25)) does not make sense, because it evaluates always to a vector of length nrow(rb.zero.df) (=13) which may result in the error you are seeing, especially when the current data slice is only 12 rows long (while ifelse(rb.zero$TOTAL == 0, 0, 0.25) is always 13 elements long).


    N.B. The mean thing is that for certain combinations you do not see the error immediately because of R's recycling rules, only if the lengths are not compatible (typically not being a multiple of each other) the error is thrown. This, by the way, may explain why you did not see the error for certain combinations, because it happened to result always in compatible lengths.


    Solution

    Having said that, the way shadow_mark is supposed to be used, is to supply the names of aesthetics as parameters. This works, because shadow_mark uses quosures, that is it evaluates its parameters later and in the right context. The context is the data slice mentioned before, where there is a alpha slot in the aesthetics enriched data slice (as you defined this aesthatic).

    Long story short you should create your plot like this:

    ## drop crs b/c of unfixed bug
    rb.zero <- st_as_sf(rb.zero.df, coords = c("LONG", "LAT")) 
    
    rb.zero.map <- ggplot() +
      geom_sf(data = rb.zero, aes(size = TOTAL, alpha = ifelse(TOTAL == 0, 0, 1))) +
      scale_alpha(range = c(0,1)) +
      guides(alpha = "none") +
      transition_states(YEAR, transition_length = 0, state_length = 1) +
      ggtitle("{closest_state}") +
      ### use alpha, gganimate knows how to evaluate it properly
      shadow_mark(alpha = alpha / 4)
    animate(rb.zero.map)