Embedding tables into your analytical HTML document
Author
Alex Zajichek
Published
July 31, 2022
In my last post we demonstrated how to build a filterable map widget into an HTML output that was free of R runtime dependency. In continuation of that theme, this article focuses on the reactable package, which enables you to embed interactive data tables into your document. Specifically, we’ll look at how we can use its built-in JavaScript interface to define custom functions for commonly-desired aggregation and formatting.
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr 1.1.4 ✔ readr 2.1.5
✔ forcats 1.0.0 ✔ stringr 1.5.1
✔ ggplot2 3.5.1 ✔ tibble 3.2.1
✔ lubridate 1.9.3 ✔ tidyr 1.3.1
✔ purrr 1.0.2
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag() masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
Make a summary dataset
First we’ll need to create a data frame with group-level summary statistics that we want to display. For this example, we’ll look at the 30-day hospital readmission rate for heart failure patients on Medicare at the top five (5) most voluminous hospitals in a handful of Midwest states.
Code
# Import datasetreadmission_rates <-read_csv(file ="/Users/alexzajichek/Documents/GitHub/Unplanned_Hospital_Visits-Hospital.csv",na =c("", " ", "NA", "N/A", "Not Available") ) %>%# Filter to states with non-null valuesfilter( State %in%c("WI", "MN", "MI", "IL"),`Measure ID`=="READM_30_HF",!is.na(Denominator),!is.na(Score) ) %>%# Convert to proportionmutate(Score = Score /100 ) %>%# Keep a few columnsselect( State,Hospital =`Facility Name`,Cases = Denominator,`30-Day Readmission Rate`= Score ) %>%# For each state, keep the top 5 most voluminous hospitalsgroup_by(State) %>%slice_max(n =5,order_by = Cases,with_ties =FALSE ) %>%ungroup()
Warning: One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
dat <- vroom(...)
problems(dat)
Rows: 66906 Columns: 20
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (15): Facility ID, Facility Name, Address, City/Town, State, ZIP Code, C...
dbl (5): Denominator, Score, Lower Estimate, Higher Estimate, Footnote
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
Code
readmission_rates
# A tibble: 20 × 4
State Hospital Cases 30-Day Readmission R…¹
<chr> <chr> <dbl> <dbl>
1 IL NORTHSHORE UNIVERSITY HEALTHSYSTEM - EVAN… 2294 0.212
2 IL PALOS COMMUNITY HOSPITAL 1436 0.214
3 IL NORTHWESTERN MEMORIAL HOSPITAL 1315 0.187
4 IL ADVOCATE CHRIST HOSPITAL & MEDICAL CENTER 1287 0.202
5 IL NORTHWESTERN MEDICINE MCHENRY 1286 0.177
6 MI BEAUMONT HOSPITAL, TROY 1683 0.21
7 MI BEAUMONT HOSPITAL ROYAL OAK 1611 0.2
8 MI ASCENSION PROVIDENCE HOSPITAL, SOUTHFIELD… 1191 0.206
9 MI TRINITY HEALTH ANN ARBOR HOSPITAL 1121 0.191
10 MI SPECTRUM HEALTH 1065 0.172
11 MN MAYO CLINIC HOSPITAL ROCHESTER 1551 0.173
12 MN ABBOTT NORTHWESTERN HOSPITAL 778 0.189
13 MN PARK NICOLLET METHODIST HOSPITAL 747 0.213
14 MN MERCY HOSPITAL 742 0.176
15 MN ST CLOUD HOSPITAL 623 0.18
16 WI AURORA ST LUKES MEDICAL CENTER 1362 0.188
17 WI FROEDTERT MEMORIAL LUTHERAN HOSPITAL 621 0.195
18 WI UNIVERSITY OF WI HOSPITALS & CLINICS AUT… 609 0.219
19 WI WAUKESHA MEMORIAL HOSPITAL 605 0.171
20 WI MILWAUKEE VA MEDICAL CENTER 604 0.222
# ℹ abbreviated name: ¹`30-Day Readmission Rate`
The default table
As a starting point, let’s see what we get when we call the reactable function without any additional arguments.
Code
readmission_rates %>%reactable()
We get about what we’d expect: a basic, paginated table where each row from our dataset is represented verbatim. There are many things we could clean up here such as rounding, number representations, formatting, and, of course, aggregating the statistics to get state-specific readmission rates.
The problem with this table is that the displayed state-level readmission rates represent the averaged rates across the individual hospitals. What we really want in the aggregation is for the hospital-specific rates to be weighted by their respective case volumes so that the state-level readmission rates are correct. This is where JavaScript comes in.
Using the JavaScript Interface
During the call to reactable, our table is accessible through a JavaScript interface which makes it easy for us to customize the aggregation and formatting behavior when the default options won’t suffice. We can use the JS function from the htmlwidgets package to:
Define a JavaScript function as a text string
Supply that function as the argument to colDef applicable to the function’s context
Function 1: Weighted mean
Referring back to our example, we want to average the 30-day readmission rates over the hospitals within each state, but we need to weight them by their respective case volume. To do this, we can supply a custom JavaScript function to the aggregate argument. This function takes the entire vector of values within the group as its argument, as well as the group’s rows, and returns a scalar value. We can specify the column containing the case weights directly by name within the row.
In our implementation, we encoded the JS function into an R function that calls for the weight column name so we can use it in any reactable in which we want this functionality applied. Let’s see what our table looks when we plug it in:
Now we have correctly calculated the state-level 30-day readmission rates.
Function 2: Conditional coloring
Suppose we would like to differentiate readmission rates in our table that are above or below the national average. To do this, we can define another JavaScript function and supply it to the style argument within colDef. First, lets pull in the comparison value.
Code
national_rate <-read_csv(file ="/Users/alexzajichek/Documents/GitHub/Unplanned_Hospital_Visits-National.csv",na =c("Not Available", "Not Applicable") ) %>%# Filter to the measurefilter(`Measure ID`=="READM_30_HF" ) %>%# Pull the ratepull(`National Rate`)
Warning: One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
dat <- vroom(...)
problems(dat)
Rows: 14 Columns: 14
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (4): Measure ID, Measure Name, Start Date, End Date
dbl (9): National Rate, Number of Hospitals Worse, Number of Hospitals Same,...
lgl (1): Footnote
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
Code
national_rate <- national_rate /100national_rate
[1] 0.198
Next, lets define another JS function wrapped in an R function that takes the column to evaluate, the comparison value, and the colors to fill in the table cell when it is above or below (or the same as) that threshold. These arguments will feed into the JavaScript function that applies the specified HTML styling to each value in the column.
Code
conditional_coloring <-function(column, threshold, color_above, color_below, color_same ="#fcfffd") {JS(paste0("function(rowInfo) { var value = rowInfo.row['", column, "'] if(value > ", threshold, ") { var color = '", color_above, "' } else if(value < ", threshold, ") { var color = '", color_below, "' } else { var color = '", color_same, "' } return {background:color} }" ) ) }
---title: "A couple useful JavaScript aggregation and formatting functions for {reactable}"description: "Embedding tables into your analytical HTML document"author: "Alex Zajichek"date: "7/31/2022"image: "feature.png"categories: - Healthcare - Tables - Javascriptformat: html: code-fold: true code-tools: true---In my [last post](https://www.zajichekstats.com/post/filterable-maps/) we demonstrated how to build a filterable map widget into an HTML output that was free of `R` runtime dependency. In continuation of that theme, this article focuses on the [`reactable`](https://glin.github.io/reactable/) package, which enables you to embed interactive data tables into your document. Specifically, we'll look at how we can use its built-in JavaScript interface to define custom functions for commonly-desired aggregation and formatting. # Table of Contents* [The Basics](#thebasics) + [Make a summary dataset](#summarydata) + [The default table](#defaulttable) + [Built-in aggregation](#builtinaggregation)* [Using the JavaScript Interface](#jsinterface) + [Function 1: Weighted mean](#function1) + [Function 2: Conditional coloring](#function2) + [Final touches](#finaltouches)# The BasicsLet's start with loading some packages.```{r}library(reactable)library(tidyverse)```## Make a summary dataset {#summarydata}First we'll need to create a data frame with group-level summary statistics that we want to display. For this example, we'll look at the 30-day hospital readmission rate for heart failure patients on Medicare at the top five (5) most voluminous hospitals in a handful of Midwest states.```{r}# Import datasetreadmission_rates <-read_csv(file ="/Users/alexzajichek/Documents/GitHub/Unplanned_Hospital_Visits-Hospital.csv",na =c("", " ", "NA", "N/A", "Not Available") ) %>%# Filter to states with non-null valuesfilter( State %in%c("WI", "MN", "MI", "IL"),`Measure ID`=="READM_30_HF",!is.na(Denominator),!is.na(Score) ) %>%# Convert to proportionmutate(Score = Score /100 ) %>%# Keep a few columnsselect( State,Hospital =`Facility Name`,Cases = Denominator,`30-Day Readmission Rate`= Score ) %>%# For each state, keep the top 5 most voluminous hospitalsgroup_by(State) %>%slice_max(n =5,order_by = Cases,with_ties =FALSE ) %>%ungroup()readmission_rates```## The default table {#defaulttable}As a starting point, let's see what we get when we call the `reactable` function without any additional arguments.```{r}readmission_rates %>%reactable()```We get about what we'd expect: a basic, paginated table where each row from our dataset is represented verbatim. There are many things we could clean up here such as rounding, number representations, formatting, and, of course, aggregating the statistics to get state-specific readmission rates.## Built-in aggregation {#builtinaggregation}There are a number of [built-in aggregation functions](#https://glin.github.io/reactable/articles/examples.html#grouping-and-aggregation) available to us by default. We just need to specify:1. The groups we want the aggregation applied within using the `groupBy` argument2. The columns we want to aggregate, and how, using `colDef` within the `columns` argumentLet's add functionality to the table above to aggregate the _total_ case count and the _average_ 30-day readmission rate within each state.```{r}readmission_rates %>%reactable(groupBy ="State",columns =list(Cases =colDef(aggregate ="sum"),`30-Day Readmission Rate`=colDef(aggregate ="mean") ) )```The problem with this table is that the displayed state-level readmission rates represent the _averaged_ rates across the individual hospitals. What we really want in the aggregation is for the hospital-specific rates to be weighted by their respective case volumes so that the state-level readmission rates are correct. This is where JavaScript comes in.# Using the JavaScript Interface {#jsinterface}During the call to `reactable`, our table is accessible through a JavaScript interface which makes it easy for us to customize the aggregation and formatting behavior when the default options won't suffice. We can use the `JS` function from the [`htmlwidgets`](https://www.htmlwidgets.org/) package to:1. Define a JavaScript function as a text string2. Supply that function as the argument to `colDef` applicable to the function's context## Function 1: Weighted mean {#function1}Referring back to our [example](#builtinaggregation), we want to average the 30-day readmission rates over the hospitals within each state, but we need to weight them by their respective case volume. To do this, we can supply a custom JavaScript function to the `aggregate` argument. This function takes the entire vector of values within the group as its argument, as well as the group's rows, and returns a scalar value. We can specify the column containing the case weights directly by name within the row.```{r}weighted_mean <-function(weight) {JS(paste0("function(values, rows) { var numerator = 0 var denominator = 0 rows.forEach(function(row, index) { numerator += row['", weight, "'] * values[index] denominator += row['", weight, "'] }) return numerator / denominator }" ) ) }```In our implementation, we encoded the `JS` function into an `R` function that calls for the weight column name so we can use it in any `reactable` in which we want this functionality applied. Let's see what our table looks when we plug it in:```{r}readmission_rates %>%reactable(groupBy ="State",columns =list(Cases =colDef(aggregate ="sum"),`30-Day Readmission Rate`=colDef(aggregate =weighted_mean(weight ="Cases")) ) )```Now we have correctly calculated the state-level 30-day readmission rates.## Function 2: Conditional coloring {#function2}Suppose we would like to differentiate readmission rates in our table that are above or below the national average. To do this, we can define another JavaScript function and supply it to the `style` argument within `colDef`. First, lets pull in the comparison value.```{r}national_rate <-read_csv(file ="/Users/alexzajichek/Documents/GitHub/Unplanned_Hospital_Visits-National.csv",na =c("Not Available", "Not Applicable") ) %>%# Filter to the measurefilter(`Measure ID`=="READM_30_HF" ) %>%# Pull the ratepull(`National Rate`)national_rate <- national_rate /100national_rate```Next, lets define another `JS` function wrapped in an `R` function that takes the column to evaluate, the comparison value, and the colors to fill in the table cell when it is above or below (or the same as) that threshold. These arguments will feed into the JavaScript function that applies the specified HTML styling to each value in the column.```{r}conditional_coloring <-function(column, threshold, color_above, color_below, color_same ="#fcfffd") {JS(paste0("function(rowInfo) { var value = rowInfo.row['", column, "'] if(value > ", threshold, ") { var color = '", color_above, "' } else if(value < ", threshold, ") { var color = '", color_below, "' } else { var color = '", color_same, "' } return {background:color} }" ) ) }```We can now add it to the table:```{r}readmission_rates %>%reactable(groupBy ="State",columns =list(Cases =colDef(aggregate ="sum"),`30-Day Readmission Rate`=colDef(aggregate =weighted_mean(weight ="Cases"),style =conditional_coloring(column ="30-Day Readmission Rate",threshold = national_rate,color_above ="#eb7554",color_below ="#54a637" ) ) ) )```Note that the national 30-day readmission rate for heart failure patients on Medicare was `r paste0(national_rate*100, "%")`.## Final touches {#finaltouches}Finally, let's add a few finishing touches to really make the table pop.```{r}readmission_rates %>%reactable(groupBy ="State",columns =list(Cases =colDef(aggregate ="sum"),`30-Day Readmission Rate`=colDef(aggregate =weighted_mean(weight ="Cases"),style =conditional_coloring(column ="30-Day Readmission Rate",threshold = national_rate,color_above ="#eb7554",color_below ="#54a637" ),format =colFormat(digits =2, percent =TRUE) ) ),striped =TRUE,highlight =TRUE,bordered =TRUE,resizable =TRUE,theme = reactablefmtr::sandstone() )```