├── .gitignore ├── README.md ├── app.R ├── data ├── net.RData ├── network.R └── tidytuesday_tweets.rds ├── functions.R ├── scrollytell.Rproj └── www └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scrollytell 2 | 3 | Scrollytell with R 4 | 5 | Main dependencies: 6 | 7 | - [shiny](https://shiny.rstudio.com/) 8 | - [shticky](https://github.com/JohnCoene/shticky) 9 | - [waypointer](https://github.com/RinteRface/waypointer) 10 | - [sigmajs](http://sigmajs.john-coene.com/) 11 | -------------------------------------------------------------------------------- /app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(shinyjs) 3 | library(shticky) 4 | library(sigmajs) 5 | library(waypointer) 6 | 7 | source("./data/network.R") 8 | source("functions.R") 9 | 10 | OFFSET <- "50%" 11 | ANIMATION <- "slideInUp" 12 | 13 | ui <- fluidPage( 14 | tags$head( 15 | tags$link(rel = "stylesheet", href = "style.css"), 16 | tags$link( 17 | rel = "stylesheet", 18 | href = "https://use.fontawesome.com/releases/v5.8.1/css/all.css", 19 | integrity = "sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf", 20 | crossorigin = "anonymous" 21 | ) 22 | ), 23 | use_shticky(), 24 | use_waypointer(), 25 | div( 26 | id = "bg", 27 | div( 28 | id = "stick", 29 | style = "position:fixed;width:100%;", 30 | fluidRow( 31 | column(4), 32 | column(8, sigmajsOutput("graph", width = "100%", height = "100vh")) 33 | ) 34 | ), 35 | longdiv( 36 | h1("Nine Months of #tidytuesday", class = "title"), 37 | br(), 38 | br(), 39 | h1( 40 | class = "subtitle", 41 | "Each", tags$i(class = "fas fa-circle sg"), "node is a twitter user,", 42 | "and each", tags$i(class = "fas fa-slash sg"), "is one tweet or more." 43 | ), 44 | br(), 45 | p( 46 | style = "text-align:center;", 47 | "Using the first dataset of #tidytuesday: #Rstats & #TidyTuesday Tweets", 48 | tags$a( 49 | class = "sg", 50 | tags$i(class = "fas fa-external-link-alt"), 51 | target = "_blank", 52 | href = "https://github.com/rfordatascience/tidytuesday/tree/master/data/2019/2019-01-01" 53 | ) 54 | ), 55 | br(), 56 | br(), 57 | br(), 58 | p( 59 | style = "text-align:center;", 60 | tags$i(class = "fas fa-chevron-down fa-3x") 61 | ) 62 | ), 63 | longdiv( 64 | div( 65 | id = "m1", 66 | uiOutput("1"), 67 | uiOutput("apr") 68 | ) 69 | ), 70 | longdiv( 71 | div( 72 | id = "m2", 73 | uiOutput("2"), 74 | uiOutput("may") 75 | ) 76 | ), 77 | longdiv( 78 | div( 79 | id = "m3", 80 | uiOutput("3"), 81 | uiOutput("jun") 82 | ) 83 | ), 84 | longdiv( 85 | div( 86 | id = "m4", 87 | uiOutput("4"), 88 | uiOutput("jul") 89 | ) 90 | ), 91 | longdiv( 92 | div( 93 | id = "m5", 94 | uiOutput("5"), 95 | uiOutput("aug") 96 | ) 97 | ), 98 | longdiv( 99 | div( 100 | id = "m6", 101 | uiOutput("6"), 102 | uiOutput("sep") 103 | ) 104 | ), 105 | longdiv( 106 | div( 107 | id = "m7", 108 | uiOutput("7"), 109 | uiOutput("oct") 110 | ) 111 | ), 112 | longdiv( 113 | div( 114 | id = "m8", 115 | uiOutput("8"), 116 | uiOutput("nov") 117 | ) 118 | ), 119 | longdiv( 120 | div( 121 | id = "m9", 122 | uiOutput("9"), 123 | uiOutput("dec") 124 | ) 125 | ), 126 | longdiv( 127 | id = "m10", 128 | class = "light", 129 | style = "text-align:center;", 130 | h1("Thank you", class = "title"), 131 | h1( 132 | class = "subtitle", 133 | tags$a( 134 | "Read the blog post", 135 | target = "_blank", 136 | class = "sg", 137 | href = "https://john-coene.com/post/scrollytell/" 138 | ) 139 | ) 140 | ) 141 | ) 142 | ) 143 | 144 | server <- function(input, output, session) { 145 | 146 | w1 <- Waypoint$ 147 | new("m1", offset = OFFSET, animate = TRUE, animation = ANIMATION)$ 148 | start() 149 | w2 <- Waypoint$ 150 | new("m2", offset = OFFSET, animate = TRUE, animation = ANIMATION)$ 151 | start() 152 | w3 <- Waypoint$ 153 | new("m3", offset = OFFSET, animate = TRUE, animation = ANIMATION)$ 154 | start() 155 | w4 <- Waypoint$ 156 | new("m4", offset = OFFSET, animate = TRUE, animation = ANIMATION)$ 157 | start() 158 | w5 <- Waypoint$ 159 | new("m5", offset = OFFSET, animate = TRUE, animation = ANIMATION)$ 160 | start() 161 | w6 <- Waypoint$ 162 | new("m6", offset = OFFSET, animate = TRUE, animation = ANIMATION)$ 163 | start() 164 | w7 <- Waypoint$ 165 | new("m7", offset = OFFSET, animate = TRUE, animation = ANIMATION)$ 166 | start() 167 | w8 <- Waypoint$ 168 | new("m8", offset = OFFSET, animate = TRUE, animation = ANIMATION)$ 169 | start() 170 | w9 <- Waypoint$ 171 | new("m9", offset = OFFSET, animate = TRUE, animation = ANIMATION)$ 172 | start() 173 | 174 | w10 <- Waypoint$ 175 | new("m10", offset = "80%", animate = TRUE, animation = ANIMATION)$ 176 | start() 177 | 178 | output$`1` <- renderUI({ 179 | req(w1$get_triggered()) 180 | if(w1$get_triggered() == TRUE) render_month(1) 181 | }) 182 | 183 | output$`2` <- renderUI({ 184 | req(w2$get_triggered()) 185 | if(w2$get_triggered() == TRUE) render_month(2) 186 | }) 187 | 188 | output$`3` <- renderUI({ 189 | req(w3$get_triggered()) 190 | if(w3$get_triggered() == TRUE) render_month(3) 191 | }) 192 | 193 | output$`4` <- renderUI({ 194 | req(w4$get_triggered()) 195 | if(w4$get_triggered() == TRUE) render_month(4) 196 | }) 197 | 198 | output$`5` <- renderUI({ 199 | req(w5$get_triggered()) 200 | if(w5$get_triggered() == TRUE) render_month(5) 201 | }) 202 | 203 | output$`6` <- renderUI({ 204 | req(w6$get_triggered()) 205 | if(w6$get_triggered() == TRUE) render_month(6) 206 | }) 207 | 208 | output$`7` <- renderUI({ 209 | req(w7$get_triggered()) 210 | if(w7$get_triggered() == TRUE) render_month(7) 211 | }) 212 | 213 | output$`8` <- renderUI({ 214 | req(w8$get_triggered()) 215 | if(w8$get_triggered() == TRUE) render_month(8) 216 | }) 217 | 218 | output$`9` <- renderUI({ 219 | req(w9$get_triggered()) 220 | if(w9$get_triggered() == TRUE) render_month(9) 221 | }) 222 | 223 | # Our sticky plot 224 | shtick <- Shtick$ 225 | new("#stick")$ 226 | shtick() 227 | 228 | output$graph <- renderSigmajs({ 229 | sigmajs() %>% 230 | sg_settings( 231 | edgeColor = "default", 232 | defaultEdgeColor = "#c3c3c3", 233 | font = "Raleway", 234 | fontStyle = "sans-serif", 235 | mouseWheelEnabled = FALSE, 236 | labelSize = "proportional", 237 | labelThreshold = 9999 238 | ) 239 | }) 240 | 241 | observeEvent(w1$get_direction(), { 242 | if(w1$get_direction() == "down") add_data(1) 243 | }) 244 | 245 | observeEvent(w2$get_direction(), { 246 | if(w2$get_direction() == "down") add_data(2) 247 | }) 248 | 249 | observeEvent(w3$get_direction(), { 250 | if(w3$get_direction() == "down") add_data(3) 251 | }) 252 | 253 | observeEvent(w4$get_direction(), { 254 | if(w4$get_direction() == "down") add_data(4) 255 | }) 256 | 257 | observeEvent(w5$get_direction(), { 258 | if(w5$get_direction() == "down") add_data(5) 259 | }) 260 | 261 | observeEvent(w6$get_direction(), { 262 | if(w6$get_direction() == "down") add_data(6) 263 | }) 264 | 265 | observeEvent(w7$get_direction(), { 266 | if(w7$get_direction() == "down") add_data(7) 267 | }) 268 | 269 | observeEvent(w8$get_direction(), { 270 | if(w8$get_direction() == "down") add_data(8) 271 | }) 272 | 273 | observeEvent(w9$get_direction(), { 274 | if(w9$get_direction() == "down") add_data(9) 275 | Sys.sleep(2) 276 | sigmajsProxy("graph") %>% 277 | sg_force_stop_p() 278 | }) 279 | 280 | observeEvent(w10$get_direction(), { 281 | if(w10$get_direction() == "down") shtick$unshtick() 282 | }) 283 | 284 | } 285 | 286 | shinyApp(ui, server) -------------------------------------------------------------------------------- /data/net.RData: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnCoene/scrollytell/021a3ec759be5634733a05daeab3d43edc953a3e/data/net.RData -------------------------------------------------------------------------------- /data/network.R: -------------------------------------------------------------------------------- 1 | library(dplyr) 2 | library(graphTweets) 3 | 4 | tt <- readRDS("./data/tidytuesday_tweets.rds") 5 | 6 | # convert dates to incremental m/y as integers 7 | posix2int <- function(created_at) { 8 | y <- format(created_at, "%Y") 9 | m <- format(created_at, "%m") 10 | y <- as.integer(y) 11 | m <- as.integer(m) 12 | 13 | mn <- min(y) + min(m) - 1 14 | 15 | (y + m) - mn 16 | } 17 | 18 | # convert date 19 | tt <- tt %>% 20 | mutate( 21 | created_at = posix2int(created_at) 22 | ) 23 | 24 | # build network 25 | net <- tt %>% 26 | gt_edges(screen_name, mentions_screen_name, created_at) %>% 27 | gt_nodes() %>% 28 | gt_dyn() %>% 29 | gt_collect() 30 | 31 | c(edges, nodes) %<-% net 32 | 33 | # prep for sigmajs 34 | nodes <- nodes %>% 35 | mutate( 36 | id = nodes, 37 | label = nodes, 38 | size = n, 39 | color = scales::col_numeric(c("#B1E2A3", "#98D3A5", "#328983", "#1C5C70", "#24C96B"), domain = NULL)(n) 40 | ) %>% 41 | select(id, label, size, color, appear = start) 42 | 43 | edges <- edges %>% 44 | mutate( 45 | id = 1:dplyr::n() 46 | ) %>% 47 | select(id, source, target, appear = created_at, weight = n) 48 | 49 | n_tweets <- tt %>% 50 | group_by(created_at) %>% 51 | summarise(n = n()) %>% 52 | ungroup() %>% 53 | mutate(cs = cumsum(n)) 54 | 55 | save(n_tweets, edges, nodes, file = "./data/net.RData") -------------------------------------------------------------------------------- /data/tidytuesday_tweets.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnCoene/scrollytell/021a3ec759be5634733a05daeab3d43edc953a3e/data/tidytuesday_tweets.rds -------------------------------------------------------------------------------- /functions.R: -------------------------------------------------------------------------------- 1 | # creates 100vh div 2 | longdiv <- function(...){ 3 | div( 4 | ..., 5 | class = "container", 6 | style = "height:100vh;" 7 | ) 8 | } 9 | 10 | edge_colors <- colorRampPalette(c("#575b73", "#8592b0", "#8da0bb"))(9) 11 | 12 | months <- c("April", "May", "June", "July", "August", "September", "October", "November", "December") 13 | 14 | # add data. 15 | add_data <- function(wp){ 16 | 17 | n <- nodes %>% 18 | filter(appear == wp) 19 | 20 | e <- edges %>% 21 | filter(appear == wp) 22 | 23 | ec <- edges %>% 24 | filter(appear <= wp) %>% 25 | mutate( 26 | new_color = edge_colors[wp] 27 | ) 28 | 29 | sigmajsProxy("graph") %>% 30 | sg_force_kill_p() %>% 31 | sg_read_nodes_p(n, id, label, color, size) %>% 32 | sg_read_edges_p(e, id, source, target, weight) %>% 33 | sg_read_exec_p() %>% 34 | sg_change_edges_p(ec, new_color, "color") %>% 35 | sg_refresh_p() %>% 36 | sg_force_start_p(strongGravityMode = TRUE, slowDown = 5) 37 | } 38 | 39 | render_month <- function(wp, cl = "dark"){ 40 | tagList( 41 | h1(months[wp], class = paste(cl, "big")), 42 | render_count(wp) 43 | ) 44 | } 45 | 46 | .get_tweet_count <- function(wp){ 47 | n_tweets %>% 48 | filter(created_at == wp) %>% 49 | pull(cs) %>% 50 | prettyNum(big.mark = ",") %>% 51 | span( 52 | class = "sg-dark emph" 53 | ) 54 | } 55 | 56 | render_count <- function(wp, cl = "dark"){ 57 | p( 58 | "By", months[wp], .get_tweet_count(wp), "Twitter users have tweeted about #tidytuesday.", 59 | class = cl 60 | ) 61 | } -------------------------------------------------------------------------------- /scrollytell.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | -------------------------------------------------------------------------------- /www/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Raleway'); 2 | 3 | body { 4 | color: #3a3a3a; 5 | font-family: 'Raleway', sans-serif; 6 | } 7 | 8 | #bg { 9 | background: rgb(142,162,190); 10 | background: linear-gradient(180deg, rgba(142,162,190,1) 0%, rgba(66,74,88,1) 100%); 11 | } 12 | 13 | .container-fluid{ 14 | padding: 0; 15 | } 16 | 17 | .big{ 18 | font-size: 50px; 19 | } 20 | 21 | .dark { 22 | color: black; 23 | } 24 | 25 | .light { 26 | color: white; 27 | } 28 | 29 | .title { 30 | text-align: center; 31 | margin-top: 15%; 32 | font-size: 70px; 33 | } 34 | 35 | .subtitle { 36 | margin-top: 20%; 37 | text-align: center; 38 | } 39 | 40 | .sg{ 41 | color: #328983; 42 | text-shadow: 2px 2px 14px #949494; 43 | } 44 | 45 | .sg-dark{ 46 | color: black; 47 | text-shadow: 2px 2px 14px #24C96B; 48 | } 49 | 50 | .emph{ 51 | font-size: 30px; 52 | } 53 | 54 | a:hover{ 55 | color: black; 56 | } --------------------------------------------------------------------------------