library(dplyr)
library(purrr)
library(stringr)
library(highcharter)
library(htmlwidgets)
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.
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
<- read.csv("https://raw.githubusercontent.com/tashapiro/peloton-stats/main/data/peloton_data.csv")
df_raw
#subset columns (not all needed)
<-df_raw|>
df_rawselect(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_raw|>
df#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",
%in% c("Stretching","Yoga")~ "Mobility",
fitness_discipline %in% c("Circuit", "Bike Bootcamp") ~ "Bootcamp",
fitness_discipline == "Strength" ~ "Strength"),
fitness_discipline #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
<- df|>
by_typegroup_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
<-by_type|>
pie_chart#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.
<-pie_chart|>
donut_charthc_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.
<- df|>
by_subtype#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:
$data[2] by_subtype
[[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.
<-donut_chart|>
drilldown_charthc_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:
<-drilldown_chart|>
final_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
= c("#FEC601","#3DA5D9","#4EE28E","#EB5017")
pal
#create and save theme as new variable
<- hc_theme(
custom_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)