Drilldowns with Highcharter

How to create dynamic highchart on-click drilldown graphics using the R highcharter library.
R
interactive visuals
Published

February 22, 2023

Intro to Highcharter

In this tutorial, we’ll explore how to add a drilldown feature to a highcharter plot. I love working with highcharter - it is an easy to use R wrapper for the JS library, highcharts, translating a lot of its functionality into familiar R syntax.

Before digging into this tutorial, I recommend looking through some highcharter vignettes to understand some of the basic functions.

Libraries

First, let’s import our libraries. We’ll use dplyr and purrr to reshape and transform our data before plotting.

We’ll then create our plot with highcharter, and finally use htmlwidgets to inject some additional JS code for minor tweaks.

library(dplyr)
library(purrr)
library(stringr)
library(highcharter)
library(htmlwidgets)

Import Data

To explore drilldowns, I’ll be working with my own personal Peloton data (it’s no secret I have a mild obsession with Peloton).

Here I’m reading in a csv file with my workout data stored in one of my GitHub repositories.

#import csv data from GitHub
df_raw <- read.csv("https://raw.githubusercontent.com/tashapiro/peloton-stats/main/data/peloton_data.csv")

#subset columns (not all needed)
df_raw<-df_raw|>
  select(fitness_discipline,type_display_name)

#preview data
head(df_raw,5)
  fitness_discipline       type_display_name
1           strength              Upper Body
2            cycling                   Music
3         stretching Pre & Post-Ride Stretch
4            cycling       Warm Up/Cool Down
5            cycling                   Music

Notice this dataset a couple of categorical variables describing the data: fitness_discipline describes the type of workout, e.g. strength, cycling, yoga. type_display_name is a more granular classification of workout types.

Reshaping The Data

The end goal is to create a dynamic visual that shows aggregate number of workouts by workout type, an a drilldown feature to show workouts by sub type for each main category.

In this step we’ll use dplyr to clean up our data and create a couple of new variables.

df<-df_raw|>
  #remove meditation from analysis
  filter(fitness_discipline!="meditation")|>
  mutate(
    #clean up fitness discipline names
    fitness_discipline = str_to_title(str_replace(fitness_discipline,"_"," ")),
    #create new workout_type category based on fitness_disciplines
    workout_type = case_when(fitness_discipline %in% c("Running","Walking","Cycling") ~ "Cardio",
                     fitness_discipline %in% c("Stretching","Yoga")~ "Mobility",
                     fitness_discipline %in% c("Circuit", "Bike Bootcamp") ~ "Bootcamp",
                     fitness_discipline == "Strength" ~ "Strength"),
    #create a workout subtype
    workout_subtype = case_when(fitness_discipline=="Strength" ~ type_display_name,
                        TRUE ~ fitness_discipline))

head(df,5)
  fitness_discipline       type_display_name workout_type workout_subtype
1           Strength              Upper Body     Strength      Upper Body
2            Cycling                   Music       Cardio         Cycling
3         Stretching Pre & Post-Ride Stretch     Mobility      Stretching
4            Cycling       Warm Up/Cool Down       Cardio         Cycling
5            Cycling                   Music       Cardio         Cycling

Now that we’ve tidied up our data, let’s create a new data frame that aggregates total workouts by workout_type.

#aggregate # of classes by workout type
by_type<- df|>
  group_by(workout_type)|>
  summarise(workouts = n())

#preview data
head(by_type,5)
# A tibble: 4 × 2
  workout_type workouts
  <chr>           <int>
1 Bootcamp          119
2 Cardio            578
3 Mobility          262
4 Strength          404

Basic Chart

Easy as Pie

Before we get into the cool fancy drilldown part, let’s walk through how to set up a basic pie chart with highcharter.

#pass in data set with pipe
pie_chart<-by_type|>
#set up highchart object
  hchart("pie", 
         #mapping for pie chart
         hcaes(x = workout_type, y = workouts, drilldown=workout_type), 
         name="Workouts")|>
  #add title
  hc_title(text="By Workout Type")

pie_chart

Pie to Donut

To transform our pie chart into a donut chart, we can use hc_plotOptions to create a hole at the center.

Arguments for highcharter are usually followed by lists with specific parameters.

donut_chart<-pie_chart|>
  hc_plotOptions(pie = list(innerSize="70%"))

donut_chart

Prepping Data for Drilldowns

When I was first learning about drilldowns, the biggest hurdle was understanding how to format the data.Since highcharts is a JS library - it generally works well with nested data (like JSON).

To mimic this format, we need to create a new data set aggregated that contains our drilldown information in several lists with the suitable mappings and arguments for a new highcharter object.

by_subtype<- df|>
  #aggregate workouts by fitness discipline
  group_by(workout_type, workout_subtype)|>
  summarise(workouts = n())|>
  #create nested data at parent level - workout type
  group_nest(workout_type) |>
  mutate(
    #id should be set to parent level
    id = workout_type,
    #type specifies chart type
    type = "column",
    #drilldown data should contain arguments for chart - use purrr to map
    data = purrr::map(data, mutate, name = workout_subtype, y  = workouts),
    data = purrr::map(data, list_parse)
  )
Warning: ... is ignored in group_nest(<grouped_df>), please use group_by(..., .add =
TRUE) %>% group_nest()
head(by_subtype,5)
# A tibble: 4 × 4
  workout_type data       id       type  
  <chr>        <list>     <chr>    <chr> 
1 Bootcamp     <list [2]> Bootcamp column
2 Cardio       <list [3]> Cardio   column
3 Mobility     <list [2]> Mobility column
4 Strength     <list [9]> Strength column

Here’s an example of what one of the nested lists looks like:

by_subtype$data[2]
[[1]]
[[1]][[1]]
[[1]][[1]]$workout_subtype
[1] "Cycling"

[[1]][[1]]$workouts
[1] 467

[[1]][[1]]$name
[1] "Cycling"

[[1]][[1]]$y
[1] 467


[[1]][[2]]
[[1]][[2]]$workout_subtype
[1] "Running"

[[1]][[2]]$workouts
[1] 66

[[1]][[2]]$name
[1] "Running"

[[1]][[2]]$y
[1] 66


[[1]][[3]]
[[1]][[3]]$workout_subtype
[1] "Walking"

[[1]][[3]]$workouts
[1] 45

[[1]][[3]]$name
[1] "Walking"

[[1]][[3]]$y
[1] 45

Adding in Drilldowns

The data transformation is 80% of the battle when setting up our highcharter plot. Now that we have a new data set with our nested drilldown data, we can layer it in to our donut chart with hc_drilldown.

With this layer added, you can now click on different slices of the donut chart to reveal a column chart with workouts at the subtype level.

drilldown_chart<-donut_chart|>
  hc_drilldown(
    #map to data
    series = list_parse(by_subtype),
    allowPointDrilldown = TRUE,
    #set stylings of data labels that offer drill down views
    activeDataLabelStyle = list(
      textDecoration="none",
      color="black"
    )
  )

drilldown_chart

Customizing Drilldown/Drillup Events

Almost there! Notice when you drillup to the donut chart again, the axes are re-drawn? In this step we’ll add a couple of tricks to format our chart using events.

Usually, to format highcharter outputs, you can use functions like hc_plotOptions and hc_xAxis to customize different chart components. I discovered with drilldown functionality, sometimes these settings re-set after drilling back up into a view.

To customize both charts independently, we can use the events argument in highcharter’s hc_chart function to specify what should happen for our drilldowns and drillups.

We can then pass in JS functions to specify what should happen for each event. To inject JS, we’ll use htmlwidgets. In this step, I want to create a function that changes the plot title for the drilldown, and removes the axes for our donut chart.

Example below:

final_chart<-drilldown_chart|>
  #relabel x Axis
  hc_xAxis(title = list(text="Type"))|>
  #relabel y Axis
  hc_yAxis(title = list(text="# of Workouts"))|>
  #reorder column charts by y Axis
  hc_plotOptions(column = list(
                   dataSorting = list(enabled=TRUE)
                   )
                 )|>
  #customize drilldown & drillup events
  hc_chart(
           events = list(
             drilldown = JS(
               "function(){
               this.title.update({text: 'By Workout Sub Type'})
               this.update({
                  xAxis:{visible:true},
                  yAxis:{visible:true}
               })
               }"
             ),
             drillup =  JS("function() {
              this.title.update({text: 'By Workout Type'})
              this.update({
                xAxis:{visible:false},
                yAxis:{visible:false}
               })
             }")
           ))

final_chart

Add in a Theme

To level up our highcharter plot, we can also add our own theme with hc_add_theme.Below I’ve created my own theme with hc_theme and added it in to the final chart.

And with that, our custom drilldown chart is finally done!

#color palette
pal = c("#FEC601","#3DA5D9","#4EE28E","#EB5017")

#create and save theme as new variable
custom_theme <- hc_theme(
  colors = pal,
  chart = list(
    backgroundColor = NULL
  ),
  title = list(
    style = list(
      color = "#333333",
      fontFamily = "Archivo",
      fontWeight="bold"
    )
  ),
  xAxis = list(
    labels=list(style = list(
      color = "#666666",
      fontFamily = "Archivo"
    ))
  ),
  yAxis = list(
    labels=list(style = list(
      color = "#666666",
      fontFamily = "Archivo"
    ))
  ),
  tooltip = list(
    style = list(
      fontFamily = "Archivo"
    )
  ),
  plotOptions = list(
    series = list(
      dataLabels = list(style=list(fontFamily = "Archivo")
      ))
  )
)


final_chart|>
  #add theme
  hc_add_theme(custom_theme)