Проблема с gganimate и (иногда) пустыми гранями - PullRequest
1 голос
/ 12 марта 2019

Я наблюдаю какое-то поведение в gganimate, которое не могу объяснить, и мне хотелось бы понять, что я делаю неправильно (или это ошибка).

Например, вот очень простой набор данных и его график:

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()

Как и ожидалось, вторая грань (s = 0,5) имеет данные только для x> 0,5, который (из того, как построен тиббл Z) получается из t> 0,5.

Если бы кто-то анимировал вышеуказанные данные (используя t как время), я бы ожидал, что второй фасет будет пустым для первой половины анимации, а затем покажу то же самое, что и первый фасет для второй половины. Однако:

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}')

Приведенный выше код генерирует анимацию (используя gifski_0.8.6) с двумя фасетами, где второй фасет только кратко показывает свои точки и, кроме того, показывает их в неправильное время (а именно в начале анимации).

Я что-то упустил, или это ошибка?

1 Ответ

2 голосов
/ 12 марта 2019

Это будет довольно длинный ответ в 3 частях. Вы можете начать здесь с объяснения или прокрутить вниз для двух предложенных обходных путей.

Объяснение

Это похоже на проблему с transition_time, которая ведет себя странно, когда начинается с пустого фасета.

После отладки базового кода, я полагаю, что проблема заключается в функции expand_panel в TransitionTime . Мы можем продемонстрировать это, запустив debug(environment(TransitionTime$expand_panel)) перед построением анимации. Посмотрите, что происходит до и после строк A-B в отлаженном коде ниже:

> 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
}

Внутри каждой панели фасетов all_frames - это фрейм данных, который содержит строки необработанных данных, соответствующие этому конкретному фасету, а также дополнительные строки, проходящие между ними. true_frame - вектор целых чисел для действительных кадров, в течение которых должны отображаться данные.

Для панели first (т.е. где s = 0) это то, что мы имеем перед строкой 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 не изменяется после строк A-B, поэтому я не буду повторять распечатки с консоли снова.

Для панели секунда (то есть s = 0,5), с другой стороны, линии A-B существенно изменились. Вот что мы имеем перед строкой 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 охватывает диапазон 50-100, в то время как номера кадров в all_frames начинаются с 49. Хорошо, достаточно близко, мы можем установить подкадр данных для кадров, совпадающих с кадрами в true_frames, и отбросить строки с помощью .frame < 50, но это не , что происходит в строке A. Обратите внимание:

> 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

Теперь мы переходим к строке B (all_frames$.frame <- all_frames$.frame - min(all_frames$.frame) + 1), которая по существу перецентрирует кадры, чтобы начать с 1. В результате, это то, что мы получаем после строки 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

Вот оно: из-за линий AB в expand_panel мы получаем явление, описанное в вопросе: анимация на второй панели начинается с кадра 1 и длится всего 3 кадра до полного исчезновения вместе.

Обходной путь 1

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

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))
}

Результат:

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

workaround 1

Обходной путь 2

Определение совершенно новых объектов ggproto может быть излишним, и, честно говоря, я не знаю достаточно о пакете gganimate, чтобы точно знать, что это ничего не сломало.

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

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}')

workaround 2

...