ABCDEFGHIJKLMNOPQRSTVWXYZ abcdefghijklmnopqrstuvwxyz 🐒📊🚀

now I know my ABC’s, what’s next?


ggkids is about bringing the grammar of graphics and ggplot2 to young learners.

ggplot2 is quite logical and accessible, but may present some challenges for young learners given a more limited vocabulary and little experience interpreting error messages.

  1. the package contains data that let young learners replicate plots from the new ‘Daphne Draws Data’ book by Storytelling with Data Founder Cole Nussbaumer-Knaflic and illustrator John Skewes.
  2. ggkids() is an alias for ggplot(). Either can be used to initialized the plot and set global data. ggkids might be preferred if you want to more strongly signal ‘we made this just for you, kiddos!’.
  3. use is an alias for aes, since ‘aesthetic’ is unlikely to be already in the vocabulary of young learners. We think that ‘use color’ (to as the aesthetic channel to communicate about ‘var1’) or ‘use size’ might be a natural way to talk about aesthetic mapping with children.
  4. A series of use_*() functions are also available and facilitate more step-wise plot construction, e.g. use_x(), use_y(), use_size(), use_linewidth() 🚧 One thing people struggle with including me is the color aesthetic. It sometimes feels like fill might better be called color, and color something (outline_color?); If this is done using open point (shape = 21), would probably be the way to go too.
  5. More aesthetic defaults are provided, rather than required for layer rendering. e.g. to add a point layer, x and y are not required, but instead default to zero. We think inspecting a plot with a zero default might be more useful than reading an error message for very young learners. 🚧 This is done using the helper function, aes_default.
  6. New picture aesthetic will play more of a role - because kids love pictures, and faces, etc. stamp_picture() is lets you add an annotation layer in an arbitrary place on your plot, e.g. stamp_picture(🚀, x = 4, y = 10) - position default is the middle of the plot, i.e. x = I(.5), y = I(.5) and picture default is a classic smiley 🙂.
  7. We introduce chart_*() series which can be used similar to layer functions (geom_* and stat_*), however they bring defaults that are appropriate for the chart type in question, e.g. chart_plunging_bar() wraps up geom_bar(stat = "identity") with scale_y_reverse(). Available chart functions: chart_bar, chart_item_stack
  8. Some of the chart functions are supported with new Stats, which are StatItemStack, StatPointCount,
library(statexpress)
library(tidyverse)

update_geom_defaults(GeomPoint, aes(size = from_theme(pointsize * 3)))

#' @export
aes_default <- function(default = aes(x = 0)) {

  structure(
    list(
         default_spec = default), 
    class = "aes_default"
    )

}


#' @import ggplot2
#' @importFrom ggplot2 ggplot_add
#' @export
ggplot_add.aes_default <- function(object, plot, object_name) {
  
  if(is.null(plot$mapping[[names(object$default_spec)]])){
   plot <-  plot + object$default_spec
  }

  plot

}
encode <- function(color, ...){
  aes(color = {{color}}, fill = {{color}}, ...) 
}

use <- encode


use_x <- function(x){list(aes(x = {{x}}))}
use_y <- function(y){list(aes(y = {{y}}))}


plot_data <- ggplot
use_weight <- function(weight){aes(weight = {{weight}})}
use_area <- function(area){aes(weight = {{area}})}
use_rows <- function(rows, cols, ...){facet_grid(rows = vars({{rows}}), cols = vars({{cols}}), ...)}

use_columns <- function(cols, rows, ...){facet_grid(rows = vars({{rows}}), cols = vars({{cols}}), ...)}
use_rows_columns <- function(rows, cols, ...){facet_grid(rows = vars({{rows}}), cols = vars({{cols}}), ...)}
use_wrap <- function(wrap, ...){facet_wrap(facets = vars({{wrap}}), ...)}
use_size <- function(size){aes(size = {{size}})}
use_shape <- function(shape){aes(shape = {{shape}})}
use_color <- function(color){aes(fill = {{color}})}
set_color <- function(color){aes(fill = I(color))}
use_color_line <- function(color){aes(color = {{color}})}
use_chart_point <- function(...){qlayer(geom = qproto_update(GeomPoint, aes(shape = 21), 
                                                         required_aes = c()),
                                    stat = qstat(function(data, scales){data$x <- data$x %||% 0 ; data$y <- data$y %||% 0; data}), ...)}


data <- function(data){ggplot(data |> remove_missing()) + theme_classic(ink = "darkgrey", paper = "whitesmoke", base_size = 18)}
chart_jitter <- geom_jitter

chart_heat <- function(...){list(
  qlayer(geom = GeomTile, 
         stat = qproto_update(StatSum, aes(fill = after_stat(n), size = NULL)), ...),
  scale_fill_gradientn(colors = c("blue", "white", "yellow", "orange", "red")),
  theme(panel.grid.minor = element_line(color = "darkgrey")))
}



title <- function(title){labs(title = title)}
subtitle <- function(subtitle){labs(subtitle = subtitle)}
caption <- function(caption){labs(caption = caption)}
tag <- function(tag){labs(tag = tag)}

stamp_picture <- function(picture = "🙂", x = I(.5), y = I(.5), ...){
  
  annotate(geom = GeomText, label = picture, x = x, y = y, ...)
  
}

pie of pets

chart_pie <- function(...){
  
  list(
  
  geom_bar(position = "fill", width = 1, show.legend = F, ...),
  # add defaults that bar doesn't usually include
  aes_default(aes(y = .5)), 
  aes_default(aes(fill = "All")),
  aes_default(aes(weight = 1)),
  aes_default(aes(color = from_theme(paper))),
  # add labels
  stat_count(geom = GeomLabel, color = "transparent",
             position = position_fill(vjust = .5),
             aes(label = after_stat(fill), 
                 y = .9, 
                 # group = after_stat(fill)
                 ), alpha = 0,
             show.legend = F,
             size = 30
             ),
  coord_polar(),
  theme(axis.text = element_blank(),
        axis.ticks = element_blank(),
        axis.line = element_blank(),
        axis.title = element_blank()),
  labs(fill = NULL)
  
  )
  
}

theme_kids <- theme_classic(paper = "whitesmoke", 
              ink = "darkgrey", 
              base_size = 30,
              base_family = "Comic Sans MS") 

theme_set(theme_kids)




pets_data <- data.frame(pets = c("🐱", "🐶", "🦚", "🐠", "🐰"), 
                   number_of_pets = c(30, 25, 10, 15, 5)) |> 
  mutate(pets = fct_infreq(pets, number_of_pets) |> fct_rev())
pets_data
#>   pets number_of_pets
#> 1   🐱             30
#> 2   🐶             25
#> 3   🦚             10
#> 4   🐠             15
#> 5   🐰              5

ggplot(pets_data) +
  use_color(pets) + 
  use_area(number_of_pets) +
  chart_pie()

A scatter of crustaceans

library(tidyverse)
types <- c("🦐", "🦀")
set.seed(1234)
ocean_table <- cars |> 
  rename(size = dist) |>
  mutate(type = c(
    rep("🦐", 20),
    sample(types, 10, replace = T),
    rep("🦀", 20))) |>
  sample_frac()


GeomPointFill <-qproto_update(GeomPoint, aes(shape = 21),
                              required_aes = c())


library(statexpress)
chart_point <- function(...){
  list(
  qlayer(geom = GeomPointFill,
         stat = qstat(function(data, scales){
           # data$shape <- data$shape %||% data$picture
           # data$x <- data$x %||% 0 ; 
           # data$y <- data$y %||% 0; 
           data}), 
         ..., show.legend = F),
  aes_default(aes(x = 0)),
  aes_default(aes(y = 0)),
  aes_default(aes(shape = I(after_stat(picture)))),
  scale_size(range = c(2,10))
  
  )
  }


# should replace with lm xy
chart_fit_line <- function(...){
  
  geom_smooth(method = lm, ..., show.legend = F, se = F, 
              linetype = "dashed",
              aes(shape = NULL, picture = NULL))
  
}

use_picture <- function(picture){aes(shape = I({{picture}}))}
head(ocean_table)
#>   speed size type
#> 1    13   26   🦐
#> 2     7   22   🦐
#> 3    18   76   🦀
#> 4    20   32   🦀
#> 5    14   60   🦀
#> 6    15   54   🦀

ggplot(ocean_table) + 
  chart_point() +
  use_y(speed) +
  use_x(size) + 
  use_size(size) + 
  use_picture(type) + 
  chart_fit_line()

last_plot() +
  labs(x = "little                        big") +
  labs(y = "slow           fast") +
  theme(panel.background = element_rect(fill = "skyblue" |> alpha(.3))) + 
  stamp_picture("🐉", 
                size = 40, 
                x = 100,
                y = 10)

A jungle bar chart… ?

theme_chart_bar <- function(){
  theme(panel.grid.minor = element_blank(), 
        panel.grid.major.x = element_blank(),
        axis.ticks.x = element_blank())
  }

chart_bar <- function(...){
  list(geom_col(...), 
       theme_chart_bar(),
       scale_y_continuous(expand = expansion(c(0, .3))),
       labs(x = NULL))
}

compute_item_stack <- function(data, scales, width = 0.2){
               
    data$shape <- data$shape %||% data$picture

    data |> 
      uncount(y) |>
      dplyr::mutate(row = row_number()) |> 
      dplyr::mutate(y = row - 
        0.5) |>
      dplyr::mutate(width = width)
    
  }

chart_item_stack <- function(...){
  
  list(
  qlayer(
    geom = GeomPointFill, 
    stat = qstat(compute_item_stack)
  ),
  # spacing
  qlayer(
    geom = GeomTile, 
    stat = qstat(compute_item_stack), 
    alpha = 0
  ),
  scale_y_continuous(expand = expansion(c(0, .3))),
  aes_default(aes(y = 1)),
  aes_default(aes(x = "All")),
  labs(x = NULL, y = NULL),
  guides(y = "none")
  )

  
}

ggprop.test:::compute_group_bricks
#> function (data, scales, width = 0.2) 
#> {
#>     data %>% dplyr::mutate(row = row_number()) %>% dplyr::mutate(y = row - 
#>         0.5) %>% dplyr::mutate(width = width)
#> }
#> <bytecode: 0x11054d318>
#> <environment: namespace:ggprop.test>

jungle_table <- data.frame(tree = paste0("🌴#", 1:5), 
                     num_bunches = c(2, 5, 1, 2, 1), 
                     banana = "🍌")

jungle_table |> 
  select(x = tree, y = num_bunches, picture = banana) |>
  compute_item_stack()
#>       x picture shape row    y width
#> 1  🌴#1      🍌    🍌   1  0.5   0.2
#> 2  🌴#1      🍌    🍌   2  1.5   0.2
#> 3  🌴#2      🍌    🍌   3  2.5   0.2
#> 4  🌴#2      🍌    🍌   4  3.5   0.2
#> 5  🌴#2      🍌    🍌   5  4.5   0.2
#> 6  🌴#2      🍌    🍌   6  5.5   0.2
#> 7  🌴#2      🍌    🍌   7  6.5   0.2
#> 8  🌴#3      🍌    🍌   8  7.5   0.2
#> 9  🌴#4      🍌    🍌   9  8.5   0.2
#> 10 🌴#4      🍌    🍌  10  9.5   0.2
#> 11 🌴#5      🍌    🍌  11 10.5   0.2




# last_plot() + 
#   annotate(geom = GeomText,
#            x = I(.75), y = I(.72),
#            label = "🎈🎀🙏",
#            angle = -10,
#            size = 22,
#             )  
jungle_table
#>   tree num_bunches banana
#> 1 🌴#1           2     🍌
#> 2 🌴#2           5     🍌
#> 3 🌴#3           1     🍌
#> 4 🌴#4           2     🍌
#> 5 🌴#5           1     🍌

ggplot(jungle_table) + 
  use(x = tree,
      y = num_bunches, 
      picture = banana) + # you could also use "🍌"
  chart_item_stack()



last_plot() + 
  stamp_picture("🐒",
                size = 40,
                x = I(.95),
                y = I(.15)) + 
  coord_cartesian(clip = "off")



head(jungle_table)
#>   tree num_bunches banana
#> 1 🌴#1           2     🍌
#> 2 🌴#2           5     🍌
#> 3 🌴#3           1     🍌
#> 4 🌴#4           2     🍌
#> 5 🌴#5           1     🍌

ggplot(jungle_table) + 
  encode(x = tree,
         y = num_bunches) + 
  chart_bar(fill = "gold")

Polar Bears


chart_bar_plunging <- function(...){
  list(geom_col(...), 
       theme_chart_bar(),
       scale_y_reverse(expand = expansion(c(.3, 0))),
       scale_x_discrete(position = "top"),
       labs(x = NULL),
       geom_label(vjust = 1, aes(label = after_stat(y)),
                  linewidth = 0)
       )
}

bears <- paste0("🐻‍❄️ #", 1:5)
depth <- c(3,5,4,6,5)

polar_bear_table <- data_frame(bears, depth)

polar_bear_table
#> # A tibble: 5 × 2
#>   bears depth
#>   <chr> <dbl>
#> 1 🐻‍❄️ #1     3
#> 2 🐻‍❄️ #2     5
#> 3 🐻‍❄️ #3     4
#> 4 🐻‍❄️ #4     6
#> 5 🐻‍❄️ #5     5

# clearhistory()

bear_table <- tribble(~id_bear, ~depth,
            "🐻‍❄️#1",    3,
            "🐻‍❄️#2",    5,
            "🐻‍❄️#3",    4,
            "🐻‍❄️#4",    6,
            "🐻‍❄️#5",    5) 
#'
ggplot(bear_table) +
  use(x = bears, y = depth) +
  chart_bar_plunging(
    fill = "lightblue1"
    ) 


# ggram(title = "Daphne's North Pole plot")

digging

time <- c(0, 15, 30, 45, 60)
num_tunnels <- c(0, 4, 5, 5.5, 5.9)
fork_and_spoon_table <- data_frame(time, num_tunnels, type = "🍴")

shovel_and_bucket_table <- data_frame(time, num_tunnels = num_tunnels * 2, type = "🪣")

paws_table <- data_frame(time, num_tunnels = num_tunnels * 3, type = "🐾")

chart_line <- geom_line

digging_table <- fork_and_spoon_table |>
  bind_rows(shovel_and_bucket_table) |>
  bind_rows(paws_table)

ggplot(digging_table) +
  use_x(time) + 
  use_y(num_tunnels) + 
  use(picture = type) +
  chart_line() +
  chart_point() + 
  labs(x = "⏱️")

Shuttles (future work)


outer_space_data <- data.frame(shuttle = paste0("🚀 #", 1:6), fuel = c(.3,.5,.3, .8,.7, .4))

chart_part_of_full <- function(...){
  
  list(
       geom_col(fill = "transparent", aes(y = 1)),
       geom_col( ... ),
       aes_default(aes(color = from_theme(ink)))
       )
  
}

stamp_hline <- function(y = .5, linetype = "dashed", ...){
  
  geom_hline(yintercept = y, linetype = linetype, ...)
  
}
outer_space_data
#>   shuttle fuel
#> 1   🚀 #1  0.3
#> 2   🚀 #2  0.5
#> 3   🚀 #3  0.3
#> 4   🚀 #4  0.8
#> 5   🚀 #5  0.7
#> 6   🚀 #6  0.4

ggplot(outer_space_data) +
  use(x = shuttle,
      y = fuel) +
  chart_part_of_full(fill = "darkolivegreen3") +
  stamp_hline(.75)



last_plot() + 
  stamp_picture("👽",
                angle = 25,
                size = 40,
                x = I(.95),
                y = I(.15)) + 
  coord_cartesian(clip = "off")

scooter repair

weeks <- c(0,1,2)
num_coins <- c(1,2,3)
coin <- c("🪙", "🪙🪙", "🪙🪙🪙")

scooter_table <- tibble(weeks, num_coins, coin)

compute_panel_count <- function(data, scales){
            data |> 
             uncount(y, .remove = F)
           }

scooter_table |> 
  select(x = weeks, y = num_coins) |> 
  compute_panel_count()
#> # A tibble: 6 × 2
#>       x     y
#>   <dbl> <dbl>
#> 1     0     1
#> 2     1     2
#> 3     1     2
#> 4     2     3
#> 5     2     3
#> 6     2     3

chart_point_count <- function(...){
  list(
  qlayer(geom = GeomPointFill,
         stat = qstat(compute_panel = compute_panel_count), 
         position = position_jitter(width = .12, height = .12),
         ..., show.legend = F),
  aes_default(aes(x = 0)),
  aes_default(aes(y = 0)),
  aes_default(aes(shape = I(after_stat(picture)))),
  scale_size(range = c(2,10))
  
  )
  }


scooter_table
#> # A tibble: 3 × 3
#>   weeks num_coins coin  
#>   <dbl>     <dbl> <chr> 
#> 1     0         1 🪙    
#> 2     1         2 🪙🪙  
#> 3     2         3 🪙🪙🪙

set.seed(12345)
ggplot(scooter_table) +
  use_x(weeks) + 
  use_y(num_coins) + 
  use_picture("🪙") +
  chart_line() +
  chart_point_count() + 
  stamp_hline(2.25)

  

last_plot() + 
  stamp_picture("🛴",
                y = 2.45,
                x = 1.9,
                size = 45,
                angle = 10)



knitr::knit_exit()