You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

223 line
7.0KB

  1. #' Animation Options
  2. #'
  3. #' Helper function to set animation and plotting options to be passed to
  4. #' [animate_plot()] and [static_plot()].
  5. #'
  6. #' @param text_family Font family for the plot text
  7. #' @param title_family Font family for the plot title
  8. #' @param text_size Font size of the plot text
  9. #' @param title_size Font size of the plot title
  10. #' @param ease_default Default aes easing function. See [tweenr::display_ease()]
  11. #' for more options.
  12. #' @param ease_other Additional aes easing options, specified as a named list.
  13. #' List entries are named with the aesthetic to which the easeing should be
  14. #' applied, consistent with [gganimate::ease_aes()].
  15. #' E.g. `list(color = "sine")`.
  16. #' @param enter Enter fading function applied to objects in the animation. See
  17. #' [gganimate::enter_exit] for a complete list of options.
  18. #' @param exit Exit fading function applied to objects in the animation. See
  19. #' [gganimate::enter_exit] for a complete list of options.
  20. #' @inheritParams gganimate::transition_states
  21. #' @export
  22. anim_options <- function(
  23. transition_length = 2,
  24. state_length = 1,
  25. ease_default = "sine-in-out",
  26. ease_other = NULL,
  27. enter = enter_fade(),
  28. exit = exit_fade(),
  29. text_family = "Fira Sans",
  30. title_family = "Fira Mono",
  31. text_size = NULL,
  32. title_size = NULL,
  33. ...
  34. ){
  35. enter_name <- rlang::quo_name(rlang::enquo(enter))
  36. exit_name <- rlang::quo_name(rlang::enquo(exit))
  37. structure(
  38. list(
  39. transition_length = transition_length,
  40. state_length = state_length,
  41. ease_default = ease_default,
  42. ease_other = ease_other,
  43. enter = setNames(list(enter), enter_name),
  44. exit = setNames(list(exit), exit_name),
  45. text_family = text_family,
  46. text_size = text_size,
  47. title_family = title_family,
  48. title_size = title_size,
  49. ...
  50. ),
  51. class = "anim_opts"
  52. )
  53. }
  54. print.anim_opts <- function(ao) {
  55. aop <- ao
  56. # Replace ggproto (enter/exit functions) with their names
  57. aop$enter <- paste("ggproto:", names(ao$enter))
  58. aop$exit <- paste("ggproto:", names(ao$exit))
  59. str(aop)
  60. invisible(ao)
  61. }
  62. validate_anim_opts <- function(ao, quiet = FALSE, strict = getOption("tidyexplain.strict_dots", FALSE)) {
  63. if (!inherits(ao, "anim_opts")) {
  64. rlang::warn("Use `anim_options()` to set `anim_opts`")
  65. }
  66. stopifnot(is.ggproto(ao$enter[[1]]), is.ggproto(ao$exit[[1]]))
  67. extra_names <- setdiff(names(ao), names(formals(anim_options)))
  68. if (!quiet && length(extra_names)) {
  69. extra_names <- paste0(sprintf("`%s`", extra_names), collapse = ", ")
  70. msg <- paste("Unknown animation options will be ignored:", extra_names)
  71. if (isTrue(strict)) rlang::abort(msg) else rlang::warn(msg)
  72. }
  73. invisible(ao)
  74. }
  75. merge.anim_opts <- function(ao_new, ao_base = anim_options()) {
  76. ao_new <- remove_default_anim_opts(ao_new)
  77. utils::modifyList(ao_base, ao_new, TRUE)
  78. }
  79. remove_default_anim_opts <- function(ao) {
  80. ao_default <- anim_options()
  81. same_names <- purrr::map2_lgl(ao, ao_default, ~ identical(names(.x), names(.y)))
  82. same <- purrr::map2_lgl(ao, ao_default, ~ identical(.x, .y))
  83. same[["enter"]] <- same_names[["enter"]]
  84. same[["exit"]] <- same_names[["exit"]]
  85. ao[!same]
  86. }
  87. default_anim_opts <- function(family, ao_custom = NULL) {
  88. family_options <- c("join", "set", "gather", "spread")
  89. family <- match.arg(family, family_options, several.ok = FALSE)
  90. ao_default <- switch(
  91. family,
  92. "gather" = anim_options(enter = enter_fade(), exit = exit_fade(),
  93. ease_default = "sine-in-out",
  94. ease_other = list(y = "cubic-out", x = "cubic-in")),
  95. "spread" = anim_options(enter = enter_fade(), exit = exit_fade(),
  96. ease_default = "sine-in-out",
  97. ease_other = list(y = "cubic-out", x = "cubic-in")),
  98. anim_options()
  99. )
  100. if (is.null(ao_custom)) return(ao_default)
  101. merge(ao_custom, ao_default)
  102. }
  103. #' Animates a plot
  104. #'
  105. #' @param d a processed dataset
  106. #' @param title the title of the plot
  107. #' @param anim_opts Animation options generated with [anim_options()]. Overrides
  108. #' any options set in `...`.
  109. #' @return a `gganim` object
  110. #' @examples
  111. #' NULL
  112. animate_plot <- function(
  113. d,
  114. title = "",
  115. ...,
  116. anim_opts = anim_options(...)
  117. ) {
  118. ao <- validate_anim_opts(anim_opts)
  119. ease_opts <- if (!is.null(ao$ease_other)) {
  120. ao$ease_other$default <- ao$ease_default
  121. ao$ease_other
  122. } else list(default = ao$ease_default)
  123. ao_ease_aes <- do.call(ease_aes, ease_opts)
  124. static_plot(d, title, anim_opts = ao) +
  125. transition_states(.frame, ao$transition_length, ao$state_length) +
  126. ao$enter[[1]] +
  127. ao$exit[[1]] +
  128. ao_ease_aes
  129. }
  130. #' Prints the tiles for a processed dataset statically
  131. #'
  132. #' @inheritParams animate_plot
  133. #' @inheritDotParams anim_options
  134. #'
  135. #' @return a ggplot
  136. #'
  137. #' @examples
  138. #' NULL
  139. static_plot <- function(
  140. d,
  141. title = "",
  142. ...,
  143. anim_opts = anim_options(...)
  144. ) {
  145. ao <- validate_anim_opts(anim_opts)
  146. text_size <- get_text_size(ao$text_size, default = 5)
  147. title_size <- get_title_size(ao$title_size, default = 17)
  148. if (!".alpha" %in% names(d)) d <- d %>% mutate(.alpha = 1)
  149. if (!".textcolor" %in% names(d))
  150. d <- d %>% mutate(.textcolor = choose_text_color(.color))
  151. if (".id_long" %in% names(d)) {
  152. d <- d %>% mutate(.item_id = paste(.id_long, .col, sep = "-"))
  153. } else {
  154. # tidyr
  155. d <- d %>% mutate(.item_id = .id)
  156. }
  157. ggplot(d, aes(x = .x, y = .y, fill = .color, alpha = .alpha, group = .item_id)) +
  158. geom_tile(width = 0.9, height = 0.9) +
  159. coord_equal() +
  160. geom_text(data = d %>% filter(!is.na(.val)), aes(label = .val, color = .textcolor),
  161. family = ao$text_family, size = text_size) +
  162. scale_fill_identity() +
  163. scale_color_identity() +
  164. scale_alpha_identity() +
  165. labs(title = title) +
  166. theme_void() +
  167. theme(plot.title = element_text(family = ao$title_family, hjust = 0.5, size = title_size))
  168. }
  169. #' Set Default Text Sizes for Animation Plots
  170. #'
  171. #' Sets the default text sizes for the animated and static plots produced by
  172. #' this package during the current session.
  173. #'
  174. #' @param text_size Font size of value labels inside the data frame squares
  175. #' @param title_size Font size of the function call or plot title
  176. #' @export
  177. set_font_size <- function(text_size = NULL, title_size = NULL) {
  178. old <- list()
  179. if (!is.null(text_size)) old$text_size <- set_text_size(text_size)
  180. if (!is.null(title_size)) old$title_size <- set_title_size(title_size)
  181. invisible(old)
  182. }
  183. set_text_size <- function(size) {
  184. old <- plot_settings$text_size
  185. plot_settings$text_size <- size
  186. invisible(old)
  187. }
  188. set_title_size <- function(size) {
  189. old <- plot_settings$title_size
  190. plot_settings$title_size <- size
  191. invisible(old)
  192. }
  193. get_text_size <- function(x = NULL, default = 5) {
  194. if (!is.null(x)) return(x)
  195. plot_settings$text_size %||%
  196. getFromNamespace("theme_env", "ggplot2")$current$text$size %||%
  197. default
  198. }
  199. get_title_size <- function(x = NULL, default = 17) {
  200. if (!is.null(x)) return(x)
  201. plot_settings$title_size %||%
  202. getFromNamespace("theme_env", "ggplot2")$current$plot.title$size %||%
  203. default
  204. }