countdown v0.4.0 – Now on CRAN!

By Garrick Aden-Buie in Blog

August 15, 2022

Posted on:
August 15, 2022
Length:
13 minute read, 2673 words
Categories:
Blog
Tags:
R R Package Slides countdown Shiny JavaScript Apps CRAN Announcement Presentations xaringan Quarto
Source:
content/blog/2022/countdown-v0-4-0/index.Rmd
See Also:
Process Profile Pictures with magick
🧼 cleanrmd
xaringanExtra v0.6.0 — Now on CRAN!

I’m stoked to announce that countdown is now available on CRAN! Countdown to something awesome in xaringan, Quarto, R Markdown, or Shiny.

In this post, I reflect a bit on the development of countdown, but you can also skip straight to the release notes!

What is countdown? #

01:30

countdown() is a simple timer you can use in presentations, documents and Shiny apps. It’s great for teaching or breakout sessions!

👈☝️ Click the timer to start it. Click again to pause. Double click to reset it. Adjust the timer on the fly with the + and buttons.

Everything you need to know about countdown, you can learn from the docs-slash-presentation at pkg.garrickadenbuie.com/countdown.

01:30
countdown::countdown(
  minutes = 1,
  seconds = 30,
  warn_when = 30
)

Installing countdown #

Installing countdown is now a whole lot easier:

install.packages("countdown")

As always, you can still get the latest and greatest in-development versions from GitHub

# install.packages("remotes")
remotes::install_github("gadenbuie/countdown")

or from gadenbuie.r-universe.dev.

options(repos = c(
  gadenbuie = 'https://gadenbuie.r-universe.dev',
  CRAN = 'https://cloud.r-project.org'
))

install.packages('countdown')

A brief history of countdown #

Before we talk about all the new things in countdown, I want to take a small minute to get nostalgic. I hope you don’t mind indulging me (or skip ahead if you’d rather get right to business).

rstudio::conf(2019) #

In 2019 I went to rstudio::conf in Austin, TX where a highlight of the conference, for me, was the Train-the-Trainer: Tidyverse Track workshop by Garrett Grolemund and Greg Wilson. That workshop specifically marked a turning point in my career and I left rstudio::conf very inspired to build and teach cool things in R.

I also walked away from rstudio::conf(2019) with another key take away: it was time to learn JavaScript. An odd thing to take away from an R conference, yes. (Although I don’t think I’m alone in this kind of realization; this year many people left rstudio::conf(2022) thinking that it’s time to learn Python.)

These two inspirations came together in my first post-conf project: a countdown timer for xaringan slides.

A slide from Garrett’s workshop materials with a 4-minute timer in the lower right corner.

Garrett used timers extensively to pace break out sessions and they worked surprisingly well to keep everyone on track. One funny thing I noticed during our workshop session was that Garrett would frequently have to switch to slide-edit mode (in Keynote, I think) to fiddle with the timer as he adjusted the length of the “your turn” session. This is pretty normal; an instructor probably has a sense of approximately how long an activity will take and we’ll often will adjust the time spent on the activity based on how the audience is doing, how well the material is working, or how close to lunch or a break we are in the session.

So my idea was to build a countdown timer that you could drop into a slide and easily use to time an event. I also wanted to make it easy to adjust the time, but my JavaScript skills were limited to what I could learn from StackOverflow, so I compromised and decided that you could only bump the timer up. After all it’s not like you have to end the timer, you can always just move on in your slides.

It becomes an R package #

I cobbled together an R package that was a fairly decent R interface around a collection of lines of JavaScript that I barely understood, that somehow assembled into an actual working timer. I made a cool intro-slash-docs presentation and would probably have sat on it for a while longer if it weren’t for Mara Averick who spotted my GitHub activity and soft-announced the package for me.

Not long after that, and slightly to my horror (please don’t go looking at my JavaScript code), Hadley submitted an issue. Actually, two issues. Obviously, that was an exciting turn of events. His suggestions were solid and helped improve the quality of the timer: he suggested a warning state and a full-screen view/app.

Amazingly, the package worked! People really seemed to like it, it solved a niche but useful need that many people have when teaching, and it let me learn a ton about how to build htmlwidgets in R. I’m proud of the R interface — it’s easy to use and configure — and I think the feature set hits the right balance of looking good right out of the box without doing too much.

But that JavaScript code… #

Since I wrote the first version of countdown, I’ve learned a whole lot more JavaScript and I know a whole lot more about how to build web things in R. countdown’s underlying code has always haunted me a little, but on the other hand it was chugging away, still working fine for most people in most situations.

So I left it alone…

Screenshot of the countdown GitHub repository page where the phrase “3 years ago” is highlighted. That is GitHub’s summary of the last time I updated countdown.

…for almost 3 years.

And wow how much has changed in the three plus years since rstudio::conf(2019). Not only did I lead a workshop about JavaScript for Shiny users at rstudio::conf(2020) (hashtag js4shiny), and not only do I now work for RStudio1, but I was also part of the program committee for conference planning. Which means I saw colleagues were still using my countdown timer in workshop slides.

And that old franken-JavaScript code still haunted me.

So this year, in part inspired by the return of the final rstudio::conf, I decided that finally rewriting that JavaScript would be the perfect conference side-hack project.

Which led to countdown v0.4.0 arriving on CRAN!

coundown v0.4.0 #

The Old JavaScript #

My first implementation relied heavily on the JavaScript function setTimeout, which takes a function and a delay in milliseconds: setTimeout(function, delay). When called, the browser waits until the delay is over and then calls the function.

A neat trick with setTimeout is that you can call it recursively inside a function as a way to run that function repeatedly. Below I’ve defined a function timerTick() that moves the timer forward one tick. It also uses setTimeout to schedule the next tick of the timer. And by using a delay of 1000 milliseconds, I’ve set up a function that runs once per second — just like a clock 😉.

This is, essentially, how countdown worked before. For each run of timerTick(), I would decrement the number of remaining seconds by one and update the timer. If there’s time left on the timer, then timerTick() shcedules another update for 1 second later. If there isn’t any time left, we can stop the timer by simply not scheduling any more timer updates.

function timerTick() {
  const timer = document.getElementById('timer')
  
  // update the timer
  timer.value -= 1
  console.log(`${timer.value}s remaining...`)
  
  if (timer.value > 0) {
    // there's time left, schedule next tick
    setTimeout(timerTick, 1000)
  } else {
    // time is up, reset the timer
    console.log(`Time's up!`)
    timer.classList.remove('running')
    timer.innerText = 'Start Timer'
    // notice we don't schedule another tick
  }
}

And this works*! Try it out by clicking the button below.


*Almost. This almost works. It works pretty well if you start the timer and then don’t touch the browser window or switch to another tab. So it does usually work fine when you’re presenting slides.

But it turns out that setTimeout() is more like suggestThatThisRunsLater(). There’s really no guarantee that the function you scheduled to run 1,000 milliseconds from now is actually going to run in 1,000 milliseconds.

There are many things that can get in the way of that function being run when you expect it. If you move to a different tab and come back, for example, there’s no guarantee that the background tab would keep chugging along, running my function every seconds. Browsers have better things to do and they’ll de-prioritize pages that aren’t being actively shown to users. This means that sometimes setTimeout(fn, 1000) runs fn 1 second from now, but depending on what else the browser is doing it could be a lot longer than that.

So how do we get around this? 🤔

All New JavaScript #

The new JavaScript version of countdown does something really simple. It doesn’t rely on setTimeout() directly to keep track of the time.

Yes, it still schedules the next tick on 1 second intervals, but it doesn’t trust that exactly one second has passed. Now, when the user starts the timer, countdown will note when the timer should end and recalculates the remaining time with each tick. This means the updates are always accurate, even if there happen to be 4 seconds between consecutive ticks.

It also means that we can bump a running timer up or down by moving that end time later or earlier. To stop the timer, we just note how much time is left and to restart it again we recalculate the end time based on how much time was left when we paused.

There’s a small amount of internal state to keep track of, which happens to basically cry out for a JavaScript class. So the new countdown timer is implemented via a CountdownTimer class.

Here’s a sketch of the class containing three core methods:

  1. tick() runs the timer, like above, except this time we calculate the remaining number of seconds on each tick. When there are less than 0 seconds left, we call the finish() method.

  2. start() gets things started by calculating when the timer should end and kicking off the tick() method.

  3. finish() wraps up by resetting the timer.

class Timer {
  constructor(el, duration) {
    // The timer's attached to a button
    this.element = el
    // it has a duration that's set when initiated
    this.duration = duration
    // and it will have an end when running
    this.end = null
  }
  
  tick () {
    // decide and report how much time is left
    const remaining = this.end - Date.now()
    console.log(`${remaining / 1000}s remaining...`)
    
    // and then schedule the next tick or finish up
    if (remaining > 0) {
      setTimeout(this.tick.bind(this), 1000)
    } else {
      this.finish()
    }
  }
  
  start () {
    if (this.end) return
    
    console.clear()
    this.element.innerText = 'Timer is running...'
    // the timer ends duration (s) * 1000 (ms) from now
    this.end = Date.now() + this.duration * 1000
    this.tick()
  }
  
  finish () {
    this.end = null
    this.element.innerText = 'Start Timer'
    console.log(`Time's up!`)
  }
}


Run the 5-second timer by clicking the button above. Notice that even though we used the same setTimeout(code, 1000) as before to schedule each tick for one second later, because this version precisely reports how much time is left you can see that our timer drifts a bit away from running perfectly once per second.

New buttons and keyboard interactions #

Beyond the improved timer, the new CountdownTimer class makes it a whole lot easier to add additional features that need to build on the timer’s internal state.

For example, you can now

  • Click to start or stop the timer

  • Double click to reset

  • Bump the timer up or down using the + and buttons

  • Do all of the above with keyboard shortcus:

    • Space or Enter to start or stop the timer

    • Esc to reset

    • or to bump up or down

Shiny! #

The shiny new countdown package also has plenty of Shiny features. Countdown timers can be controlled directly from Shiny with countdown_action() or countdown_update() and timers are now also inputs that report their state!

You can find an example Shiny app with a timer, plus an explanation of how it all works, by running

to launch an example app. The example app is also available on my website at apps.garrickadenbuie.com/countdown-shiny-example.

In a nutshell, the timer will report its state using its input id. For example, countdown(id = "timer") will report its state to Shiny via input$timer. The input reports the event that caused the state to change and the state of the timer:

Here’s another small app that demonstrate how you could use a button to toggle the state of the timer.

Improved countdown App #

A screenshot of the full screen countdown timer app.

All of the Shiny updates mentioned above are used to power countdown_app(), a full screen Shiny app for running timers. These work really well for timing speakers at conferences or for a quick way to keep track of a break out session in workshops or meetings.

The app itself received a few upgrades, most importantly is the ability to share a timer with the settings you want using the URL. This uses Shiny’s Bookmarking state features to save your settings in the URL and restore them when you load that link.

For example, this timer is a 20 minute timer with a warning at 5 minutes that updates every 10 seconds.

New Options #

Finally, countdown gained a new option. You can now start the timer as soon as it is visible by setting start_immediately = TRUE. The “as as soon as it’s visible” works pretty well: in xaringan and Quarto slides it starts when you land on the slide and in regular HTML documents the timer starts when you scroll the timer into view.

It’s also worth mentioning that countdown now uses prismatic for color calculations. I was really happy to see that Emil added best_contrast() and switching to use that function cleaned up a lot of internal code for me!

Thank you! #

Huge thanks to the many people who opened issues or contributed code to countdown over these years. You all rock 🧡

@andrewpbray, @apreshill, @ConnorJPSmith, @csgillespie, @Dr-Joe-Roberts, @fvitalini, @hadley, @HaoZeke, @jhelvy, @jvcasillas, @moshpirit, @rtheodoro, @sje30, @spcanelon,and @thiyangt.

If you’ve read this far, thank you! Thanks for using countdown and making developing R packages fun. Reach out in the comments or on Twitter (I’m @grrrck) with any questions or thoughts ☺️



  1. At least until October: RStudio is becoming Posit↩︎