library(tidyverse)
library(reactable)
<- gt::pizzaplace |>
hawaiian_sales filter(name == 'hawaiian') |>
mutate(
month = month(
label = TRUE, abbr = FALSE,
date, locale = 'en_US.UTF-8' # English month names
),quarter = paste0('Q', quarter(date))
|>
) summarise(
sales = n(),
revenue = sum(price),
.by = c(month, quarter)
)
hawaiian_sales## # A tibble: 12 × 4
## month quarter sales revenue
## <ord> <chr> <int> <dbl>
## 1 January Q1 185 2443.
## 2 February Q1 198 2633
## 3 March Q1 217 2878.
## 4 April Q2 219 2868.
## 5 May Q2 198 2688
## 6 June Q2 189 2564.
## 7 July Q3 195 2620.
## 8 August Q3 201 2679.
## 9 September Q3 196 2616.
## 10 October Q4 188 2515.
## 11 November Q4 227 2953.
## 12 December Q4 209 2817.
Creating interactive tables with reactable
reactable
package
The R programming language has a rich ecosystem of packages that are fantastic for creating beautiful production-grade tables from within R. Today, I’m showing you that one package that makes it really easy (mostly) to create interactive tables. Namely, I’m going to show you {reactable}
. 🥳
If you want to see a video version of this blog post, you can find it on YouTube:
Fake data
Let’s first create a dummy data set using one of {gt}
’s built-in data sets. {gt}
has a lot of those so we might as well use it even if we don’t use {gt}
to create the table in the end.
Base Layer
To create a table all we have to to is to pass the data to the reactable function.
reactable(hawaiian_sales)
The nice thing is that this is interactive out of the box. By clicking onto the column names, you can sort the rows.
Use better column names
Unlike {gt}
, the {reactable}
package doesn’t allow to change the table step by step by chaining pipes. Instead, you will have to use one of the many arguments of reactable()
and helper functions to get things done. For example, to set nicer column names you can use the columns
argument with a list of column definitions (using the colDef()
helper)
reactable(
hawaiian_sales,columns = list(
quarter = colDef(name = 'Quarter'),
month = colDef(name = 'Month'),
sales = colDef(name = 'Sales'),
revenue = colDef(name = 'Revenue')
) )
Title & Subtitle
For adding a nice title and subtitle to your plot, you can either use some custom HTML & CSS tricks or you just use the {reactablefmtr}
package.
reactable(
hawaiian_sales,columns = list(
quarter = colDef(name = 'Quarter'),
month = colDef(name = 'Month'),
sales = colDef(name = 'Sales'),
revenue = colDef(name = 'Revenue')
)|>
) ::add_title(
reactablefmtrtitle = 'Hawaiian Pizza Sales in 2015'
|>
) ::add_subtitle(
reactablefmtrsubtitle = 'Based on the fake pizzaplace data from `{gt}`',
font_weight = 'normal'
)
Hawaiian Pizza Sales in 2015
Based on the fake pizzaplace data from `{gt}`
Format numbers
The numbers in the revenue column correspond to dollar amounts. We can format them by specifying a column format inside of colDef()
with help from the colFormat()
helper function.
reactable(
hawaiian_sales,columns = list(
quarter = colDef(name = 'Quarter'),
month = colDef(name = 'Month'),
sales = colDef(name = 'Sales'),
revenue = colDef(
name = 'Revenue',
format = colFormat(currency = 'USD', separators = TRUE)
)
)|>
) ::add_title(
reactablefmtrtitle = 'Hawaiian Pizza Sales in 2015'
|>
) ::add_subtitle(
reactablefmtrsubtitle = 'Based on the fake pizzaplace data from `{gt}`',
font_weight = 'normal'
)
Hawaiian Pizza Sales in 2015
Based on the fake pizzaplace data from `{gt}`
Add groups
Now, I want to structure my tables into quarters. The easiest way to do that is to use the quarter column in our data set for grouping. The cool thing about {reactable}
is that it’s really easy and the output becomes nicely interactive out of the box. All you have to do is set the groupBy
argument.
reactable(
hawaiian_sales,groupBy = 'quarter',
columns = list(
quarter = colDef(name = 'Quarter'),
month = colDef(name = 'Month'),
sales = colDef(name = 'Sales'),
revenue = colDef(
name = 'Revenue',
format = colFormat(currency = 'USD', separators = TRUE)
)
)|>
) ::add_title(
reactablefmtrtitle = 'Hawaiian Pizza Sales in 2015'
|>
) ::add_subtitle(
reactablefmtrsubtitle = 'Based on the fake pizzaplace data from `{gt}`',
font_weight = 'normal'
)
Hawaiian Pizza Sales in 2015
Based on the fake pizzaplace data from `{gt}`
Add summaries
You can add group summaries by using the aggregate
argument inside of colDef()
and setting it to one of the built-in aggregate functions like "mean"
or "sum"
. If you want to do something custom, you can do that, but then you will have to write a custom JavaScript function for that.
reactable(
hawaiian_sales,groupBy = 'quarter',
columns = list(
quarter = colDef(name = 'Quarter'),
month = colDef(name = 'Month'),
sales = colDef(
name = 'Sales',
aggregate = 'sum'
),revenue = colDef(
name = 'Revenue',
format = colFormat(currency = 'USD', separators = TRUE),
aggregate = 'sum'
)
)|>
) ::add_title(
reactablefmtrtitle = 'Hawaiian Pizza Sales in 2015'
|>
) ::add_subtitle(
reactablefmtrsubtitle = 'Based on the fake pizzaplace data from `{gt}`',
font_weight = 'normal'
)
Hawaiian Pizza Sales in 2015
Based on the fake pizzaplace data from `{gt}`
Make table searchable
If we wanted to make our table more interactive, we could make the month
column filterable. That way, we can look for particular columns. In that case, it probably makes sense to have the groups unfolded by default.
reactable(
hawaiian_sales,groupBy = 'quarter',
defaultExpanded = TRUE, # Expand rows by default
columns = list(
quarter = colDef(name = 'Quarter'),
month = colDef(
name = 'Month',
filterable = TRUE # Make column filterable
),sales = colDef(
name = 'Sales',
aggregate = 'sum'
),revenue = colDef(
name = 'Revenue',
format = colFormat(currency = 'USD', separators = TRUE),
aggregate = 'sum'
)
)|>
) ::add_title(
reactablefmtrtitle = 'Hawaiian Pizza Sales in 2015'
|>
) ::add_subtitle(
reactablefmtrsubtitle = 'Based on the fake pizzaplace data from `{gt}`',
font_weight = 'normal'
)
Hawaiian Pizza Sales in 2015
Based on the fake pizzaplace data from `{gt}`
Change row styling
Finally, to add a little bit more style and visual structure let us make the group rows blue. Let’s first try to change the theme()
argument with the reactableTheme()
helper function.
With that we can inject a bit of CSS to our table. The {htmltools}
package makes it easy to combine multiple style instructions.
reactable(
hawaiian_sales,groupBy = 'quarter',
defaultExpanded = TRUE,
columns = list(
quarter = colDef(name = 'Quarter'),
month = colDef(
name = 'Month',
filterable = TRUE
),sales = colDef(
name = 'Sales',
aggregate = 'sum',
footer = JS("function(column, state) {
let total = 0
state.sortedData.forEach(function(row) {
total += row[column.id]
})
return total
}"),
),revenue = colDef(
name = 'Revenue',
format = colFormat(currency = 'USD', separators = TRUE),
aggregate = 'sum',
footer = JS("function(column, state) {
let total = 0
state.sortedData.forEach(function(row) {
total += row[column.id]
})
return total.toLocaleString('en-US', { style: 'currency', currency: 'USD' })
}")
)
),theme = reactableTheme(
rowGroupStyle = htmltools::css(
background = '#E7EDF3',
borderLeft = '2px solid #104E8B'
)
)|>
) ::add_title(
reactablefmtrtitle = 'Hawaiian Pizza Sales in 2015'
|>
) ::add_subtitle(
reactablefmtrsubtitle = 'Based on the fake pizzaplace data from `{gt}`',
font_weight = 'normal'
)
Hawaiian Pizza Sales in 2015
Based on the fake pizzaplace data from `{gt}`
Unfortunately, this didn’t do what we want. This seems to target all cells in our table because they are all a row of some group. But to only highlight the header row of each group we will have to proceed differently.
For that, we need to write a JavaScript function that takes the rowInfo
object as an argument and returns a JSON object with camelCased style properties. Thankfully, the reactable cookbook shows you exactly what you need.
reactable(
hawaiian_sales,groupBy = 'quarter',
defaultExpanded = TRUE,
columns = list(
quarter = colDef(name = 'Quarter'),
month = colDef(
name = 'Month',
filterable = TRUE
),sales = colDef(
name = 'Sales',
aggregate = 'sum',
footer = JS("function(column, state) {
let total = 0
state.sortedData.forEach(function(row) {
total += row[column.id]
})
return total
}"),
),revenue = colDef(
name = 'Revenue',
format = colFormat(currency = 'USD', separators = TRUE),
aggregate = 'sum',
footer = JS("function(column, state) {
let total = 0
state.sortedData.forEach(function(row) {
total += row[column.id]
})
return total.toLocaleString('en-US', { style: 'currency', currency: 'USD' })
}")
)
),rowStyle = JS(
"function(rowInfo) {
if (rowInfo.level == 0) { // corresponds to row group
return {
background: '#E7EDF3',
borderLeft: '2px solid #104E8B',
fontWeight: 600
}
}
}"
),|>
) ::add_title(
reactablefmtrtitle = 'Hawaiian Pizza Sales in 2015'
|>
) ::add_subtitle(
reactablefmtrsubtitle = 'Based on the fake pizzaplace data from `{gt}`',
font_weight = 'normal'
)