Simple Reactable Timeline in R

- reactable R timeline

In this post, I will demonstrate how to create the below timeline using reactable.


Motivation

I’ve wanted to create an interactive timeline for awhile. There are a lot of great R timeline packages out there, but none of them really fit my needs.

I’m a huge fan of the reactable package, mainly for its sleek look and customizability. As a result, I had already invested a lot of time integrating “reactables” into my reports. Rather than annihilating my work hitherto, I needed a solution to augment my existing reactables with a graphical timeline.

The beauty of this solution is that it preserves the tabular framework of reactable.

First, let’s load the data.

sales <- readr::read_csv("https://raw.githubusercontent.com/sccmckenzie/reactable-timeline/master/sales.csv")

This is a simple dataset that I created for demonstration. It contains store records of customer revenue.

  • c_id: Unique customer ID
  • revenue: Total revenue from items purchased by customer
  • enter: Time when customer enters store
  • exit: Time when customer exits store
sales
## # A tibble: 10 x 4
##     c_id revenue enter  exit  
##    <dbl>   <dbl> <time> <time>
##  1     1  23.3   18:41  19:44 
##  2     2  21.4   08:55  09:49 
##  3     3  44.9   17:37  19:07 
##  4     4  57.2   11:37  13:28 
##  5     5  27.6   10:27  11:28 
##  6     6  21.5   12:25  13:20 
##  7     7  50.8   08:48  10:29 
##  8     8  28.9   08:02  09:00 
##  9     9   0.610 09:30  10:02 
## 10    10   5.44  10:07  10:30

Our goal is to project enter and exit into a “timeline column”.

Step 0: Libraries

This solution requires the below packages:

library(tidyverse)
library(lubridate) # needed for time_length()
library(htmltools) # needed for div()
library(reactable)

Step 1: Define time transform function

convert_timestamps will take enter and exit, normalize, merge, then return as list.

convert_timestamps <- function(t1, t2) {
  # extract timestamp range
  t01 <- min(t1)
  t02 <- max(t2)

  # normalize event timestamps within total range
  left <- time_length(t1 - t01)/time_length(t02 - t01)
  width <- time_length(t2 - t1)/time_length(t02 - t01)

  # splice values into list
  out <- list()

  for (i in 1:length(left)) {
    out[[i]] <- list(left[i], width[i])
  }

  out
}

Example:

sales %>%
  mutate(timeline = convert_timestamps(enter, exit))
## # A tibble: 10 x 5
##     c_id revenue enter  exit   timeline  
##    <dbl>   <dbl> <time> <time> <list>    
##  1     1  23.3   18:41  19:44  <list [2]>
##  2     2  21.4   08:55  09:49  <list [2]>
##  3     3  44.9   17:37  19:07  <list [2]>
##  4     4  57.2   11:37  13:28  <list [2]>
##  5     5  27.6   10:27  11:28  <list [2]>
##  6     6  21.5   12:25  13:20  <list [2]>
##  7     7  50.8   08:48  10:29  <list [2]>
##  8     8  28.9   08:02  09:00  <list [2]>
##  9     9   0.610 09:30  10:02  <list [2]>
## 10    10   5.44  10:07  10:30  <list [2]>

Step 2: Define html helper

reactable() will feed output from convert_timestamps into create_timeline_bar, producing our final output.

create_timeline_bar <- function(left = 0, width = "100%", fill = "#00bfc4") {
  left <- scales::percent(left)
  width <- scales::percent(width)

  bar <- div(style = list(
    position = "absolute",
    left = left,
    background = fill,
    width = width,
    height = "140%")
  )

  chart <- div(style = list(
    flexGrow = 1,
    position = "relative",
    display = "flex",
    alignItems = "center",
    height = "100%"
  ),
  bar)

  div(style = list(
    height = "100%"
  ),
  chart)
}

Final product

sales %>%
  mutate(timeline = convert_timestamps(enter, exit)) %>%
  # ^ created in Step 1
  reactable(fullWidth = FALSE,
            compact = TRUE,
            bordered = TRUE,
            columns = list(
              c_id = colDef(width = 55),
              revenue = colDef(width = 90, show = FALSE, format = colFormat(digits = 1)),
              enter = colDef(width = 80),
              exit = colDef(width = 80),
              timeline = colDef(
                width = 90,
                cell = function(value) {
                  create_timeline_bar(left = value[[1]], width = value[[2]])
                  # ^ created in Step 2
                }
              )
            )
  )