Issue with gganimate and (sometimes) empty facets

怎甘沉沦 提交于 2021-01-24 11:05:30

问题


I'm observing some behavior in gganimate that I cannot explain, and I would like to understand what I am doing wrong (or whether it is a bug).

For example, here is a very simple dataset and a plot of it:

library(dplyr) # dplyr_0.7.8
library(tidyr) # tidyr_0.8.2 

crossing(p = 1:2, 
         t = seq(0, 1, len = 30),
         s = c(0, .5)) %>%
  mutate(x = t,
         y = t^p) %>%
  filter(t > s) ->
  Z

library(ggplot2) # ggplot2_3.1.0

Z %>%
  ggplot(aes(x,y)) +
  facet_wrap(~s) +
  geom_point()

As expected the second facet (s=0.5) only has data for x > 0.5, which (from how the tibble Z is constructed) comes from t > 0.5.

If one were to animate the above data (using t as time) I would expect the second facet to be empty for the first half of the animation, and then show the same as the first facet for the second half. However:

library(gganimate) # gganimate_1.0.2
Z %>%
  ggplot(aes(x, y, group = interaction(p,s))) +
  facet_wrap(~s) +
  geom_point() +
  transition_time(t) +
  ggtitle('{frame_time}')

The above code generates an animation (using gifski_0.8.6) with two facets where the second facet only shows its points briefly, and furthermore shows them at the wrong time (namely at the start of the animation).

Am I missing something, or is this a bug?


回答1:


This is going to be a rather long answer in 3 parts. You can start here for the explanation, or scroll down for two proposed workarounds.

Explanation

This does appear to be an issue with transition_time, which acts weird when it starts with an empty facet.

After debug through the underlying code, I figure the problem lies with the expand_panel function under TransitionTime. We can demonstrate this by running debug(environment(TransitionTime$expand_panel)) before plotting the animation in question. Look out for what happens before & after lines A-B in the debugged code below:

> TransitionTime$expand_panel
<ggproto method>
  <Wrapper function>
    function (...) 
f(..., self = self)

  <Inner function (f)>
    function (self, data, type, id, match, ease, enter, exit, params, 
    layer_index) 
{
    ... # omitted

    true_frame <- seq(times[1], times[length(times)])

    # line A
    all_frames <- all_frames[
      all_frames$.frame %in% which(true_frame > 0 & true_frame <= params$nframes), 
      , 
      drop = FALSE]

    # line B
    all_frames$.frame <- all_frames$.frame - min(all_frames$.frame) + 1

    ... # omitted
}

Within each facet panel, all_frames is a data frame that holds the raw data rows corresponding to that specific facet, as well as additional rows transiting between them. true_frame is a vector of integers for valid frames during which the data should show up.

For the first panel (i.e. where s = 0), this is what we have before line A:

> true_frame
  [1]   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22
 [23]  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44
 [45]  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66
 [67]  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88
 [89]  89  90  91  92  93  94  95  96  97  98  99 100

> head(all_frames)
            x           y group PANEL shape    colour size fill alpha stroke .id     .phase .frame
1  0.03448276 0.034482759     1     1    19     black  1.5   NA    NA    0.5   1        raw      1
45 0.03448276 0.001189061     2     1    19     black  1.5   NA    NA    0.5   2        raw      1
3  0.04310345 0.043103448     1     1    19 #000000FF  1.5   NA    NA    0.5   1 transition      2
4  0.04310345 0.002080856     2     1    19 #000000FF  1.5   NA    NA    0.5   2 transition      2
5  0.05172414 0.051724138     1     1    19 #000000FF  1.5   NA    NA    0.5   1 transition      3
6  0.05172414 0.002972652     2     1    19 #000000FF  1.5   NA    NA    0.5   2 transition      3

> tail(all_frames)
            x         y group PANEL shape    colour size fill alpha stroke .id     .phase .frame
530 0.9827586 0.9827586     1     1    19 #000000FF  1.5   NA    NA    0.5   1 transition     98
629 0.9827586 0.9661118     2     1    19 #000000FF  1.5   NA    NA    0.5   2 transition     98
716 0.9913793 0.9913793     1     1    19 #000000FF  1.5   NA    NA    0.5   1 transition     99
816 0.9913793 0.9830559     2     1    19 #000000FF  1.5   NA    NA    0.5   2 transition     99
434 1.0000000 1.0000000     1     1    19     black  1.5   NA    NA    0.5   1        raw    100
871 1.0000000 1.0000000     2     1    19     black  1.5   NA    NA    0.5   2        raw    100

all_frames is unchanged after lines A-B, so I won't repeat the console printouts again.

For the second panel (i.e. s = 0.5), on the other hand, lines A-B made a significant difference. Here's what we have before line A:

> true_frame
 [1]  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73
[25]  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97
[49]  98  99 100

> head(all_frames)
           x         y group PANEL shape    colour size fill alpha stroke .id     .phase .frame
16 0.5172414 0.5172414     3     2    19     black  1.5   NA    NA    0.5  NA        raw     49
60 0.5172414 0.2675386     4     2    19     black  1.5   NA    NA    0.5  NA        raw     49
3  0.5241379 0.5241379     3     2    19 #000000FF  1.5   NA    NA    0.5   3 transition     50
4  0.5241379 0.2749108     4     2    19 #000000FF  1.5   NA    NA    0.5   4 transition     50
5  0.5310345 0.5310345     3     2    19 #000000FF  1.5   NA    NA    0.5   3 transition     51
6  0.5310345 0.2822830     4     2    19 #000000FF  1.5   NA    NA    0.5   4 transition     51

> tail(all_frames)
            x         y group PANEL shape    colour size fill alpha stroke .id     .phase .frame
513 0.9827586 0.9827586     3     2    19 #000000FF  1.5   NA    NA    0.5   3 transition     98
617 0.9827586 0.9661118     4     2    19 #000000FF  1.5   NA    NA    0.5   4 transition     98
710 0.9913793 0.9913793     3     2    19 #000000FF  1.5   NA    NA    0.5   3 transition     99
87  0.9913793 0.9830559     4     2    19 #000000FF  1.5   NA    NA    0.5   4 transition     99
441 1.0000000 1.0000000     3     2    19     black  1.5   NA    NA    0.5   3        raw    100
88  1.0000000 1.0000000     4     2    19     black  1.5   NA    NA    0.5   4        raw    100

true_frames covers the range 50-100, while the frame numbers in all_frames start from 49. Fine, close enough, we can subset the data frame for frames that match those in true_frames & drop the rows with .frame < 50, but that isn't what happens in line A. Observe:

> true_frame > 0 & true_frame <= params$nframes # all TRUE
 [1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
[20] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
[39] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE

> which(true_frame > 0 & true_frame <= params$nframes) 
# values start from 1, rather than 1st frame number
 [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
[33] 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51

> all_frames$.frame %in% which(true_frame > 0 & true_frame <= params$nframes) 
# only the first few frames match the last few values!
  [1]  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
 [16] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
 [31] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
 [46] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
 [61] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
 [76] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
 [91] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE

> all_frames
# consequently, only the first few frames are left after the subsetting
           x         y group PANEL shape    colour size fill alpha stroke .id     .phase .frame
16 0.5172414 0.5172414     3     2    19     black  1.5   NA    NA    0.5  NA        raw     49
60 0.5172414 0.2675386     4     2    19     black  1.5   NA    NA    0.5  NA        raw     49
3  0.5241379 0.5241379     3     2    19 #000000FF  1.5   NA    NA    0.5   3 transition     50
4  0.5241379 0.2749108     4     2    19 #000000FF  1.5   NA    NA    0.5   4 transition     50
5  0.5310345 0.5310345     3     2    19 #000000FF  1.5   NA    NA    0.5   3 transition     51
6  0.5310345 0.2822830     4     2    19 #000000FF  1.5   NA    NA    0.5   4 transition     51

We now come to line B (all_frames$.frame <- all_frames$.frame - min(all_frames$.frame) + 1), which essentially re-centers the frames to start from 1. As a result, this is what we get after line B:

> all_frames
           x         y group PANEL shape    colour size fill alpha stroke .id     .phase .frame
16 0.5172414 0.5172414     3     2    19     black  1.5   NA    NA    0.5  NA        raw      1
60 0.5172414 0.2675386     4     2    19     black  1.5   NA    NA    0.5  NA        raw      1
3  0.5241379 0.5241379     3     2    19 #000000FF  1.5   NA    NA    0.5   3 transition      2
4  0.5241379 0.2749108     4     2    19 #000000FF  1.5   NA    NA    0.5   4 transition      2
5  0.5310345 0.5310345     3     2    19 #000000FF  1.5   NA    NA    0.5   3 transition      3
6  0.5310345 0.2822830     4     2    19 #000000FF  1.5   NA    NA    0.5   4 transition      3

There you have it: due to lines A-B in expand_panel, we get the phenomenon described in the question: animation in the second panel starts from frame 1, & only lasts a measly 3 frames before disappearing all together.

Workaround 1

Since we know what's causing the problem, we can tweak the code for expand_panel, and define a slightly different version of transition_time that uses it instead:

library(tweenr)

TransitionTime2 <- ggproto(
  "TransitionTime2",
  TransitionTime,
  expand_panel = function (self, data, type, id, match, ease, enter, exit, params, 
                           layer_index) {
    row_time <- self$get_row_vars(data)
    if (is.null(row_time)) 
      return(data)
    data$group <- paste0(row_time$before, row_time$after)
    time <- as.integer(row_time$time)
    states <- split(data, time)
    times <- as.integer(names(states))
    nframes <- diff(times)
    nframes[1] <- nframes[1] + 1
    if (times[1] <= 1) {
      all_frames <- states[[1]]
      states <- states[-1]
    }
    else {
      all_frames <- data[0, , drop = FALSE]
      nframes <- c(times[1] - 1, nframes)
    }
    if (times[length(times)] < params$nframes) {
      states <- c(states, list(data[0, , drop = FALSE]))
      nframes <- c(nframes, params$nframes - times[length(times)])
    }
    for (i in seq_along(states)) {
      all_frames <- switch(type, point = tween_state(all_frames, 
                                                     states[[i]], ease, nframes[i], 
                                                     !!id, enter, exit), 
                           path = transform_path(all_frames, 
                                                 states[[i]], ease, nframes[i], 
                                                 !!id, enter, exit, match), 
                           polygon = transform_polygon(all_frames, 
                                                       states[[i]], ease, nframes[i], 
                                                       !!id, enter, exit, match), 
                           sf = transform_sf(all_frames, 
                                             states[[i]], ease, nframes[i], 
                                             !!id, enter, exit), 
                           stop(type, 
                                " layers not currently supported by transition_time", 
                                call. = FALSE))
    }
    true_frame <- seq(times[1], times[length(times)])
    all_frames <- all_frames[
      all_frames$.frame %in% 
        # which(true_frame > 0 & true_frame <= params$nframes),
        true_frame[which(true_frame > 0 & true_frame <= params$nframes)], # tweak line A
      , 
      drop = FALSE]
    # all_frames$.frame <- all_frames$.frame - min(all_frames$.frame) + 1 # remove line B
    all_frames$group <- paste0(all_frames$group, "<", all_frames$.frame, ">")
    all_frames$.frame <- NULL
    all_frames
  })

transition_time2 <- function (time, range = NULL) {
  time_quo <- enquo(time)
  gganimate:::require_quo(time_quo, "time")
  ggproto(NULL, TransitionTime2, 
          params = list(time_quo = time_quo, range = range))
}

Result:

Z %>%
  ggplot(aes(x, y, group = interaction(p,s))) +
  geom_point() +
  facet_wrap(~s) +
  transition_time2(t) +
  ggtitle('{frame_time}')

Workaround 2

Defining entirely new ggproto objects may be overkill, and frankly I don't know enough about the gganimate package to know for certain that doing so hasn't broken anything else down the line.

As a less disruptive alternative, we can simply pre-process the data frame to include the same range of time values for each facet (as well as any other grouping variable of interest), and make the new rows invisible instead:

Z %>% 
  mutate(alpha = 1) %>%
  tidyr::complete(t, s, p, fill = list(alpha = 0)) %>%
  group_by(s, p) %>%
  arrange(t) %>%
  tidyr::fill(x, y, .direction = "up") %>%
  ungroup() %>%

  ggplot(aes(x, y, group = interaction(p, s), alpha = alpha)) +
  geom_point() +
  facet_wrap(~ s) +
  scale_alpha_identity() +
  transition_time(t) +
  ggtitle('{frame_time}')



来源:https://stackoverflow.com/questions/55110492/issue-with-gganimate-and-sometimes-empty-facets

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!