Site icon R-bloggers

Creating interactive tables with reactable

[This article was first published on Albert Rapp, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

  • 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:

    < section id="fake-data" class="level2">

    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.

    library(tidyverse)
    library(reactable)
    hawaiian_sales <- gt::pizzaplace |> 
      filter(name == 'hawaiian') |> 
      mutate(
        month = month(
          date, label = TRUE, abbr = FALSE,
          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.
    < section id="base-layer" class="level2">

    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.

    < section id="use-better-column-names" class="level2">

    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')
      )
    )
    < section id="title-subtitle" class="level2">

    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')
      )
    ) |> 
      reactablefmtr::add_title(
        title = 'Hawaiian Pizza Sales in 2015'
      ) |> 
      reactablefmtr::add_subtitle(
        subtitle = 'Based on the fake pizzaplace data from `{gt}`',
        _weight = 'normal'
      )

    Hawaiian Pizza Sales in 2015

    Based on the fake pizzaplace data from `{gt}`

    < section id="format-numbers" class="level2">

    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)
        )
      )
    ) |> 
      reactablefmtr::add_title(
        title = 'Hawaiian Pizza Sales in 2015'
      ) |> 
      reactablefmtr::add_subtitle(
        subtitle = 'Based on the fake pizzaplace data from `{gt}`',
        _weight = 'normal'
      )

    Hawaiian Pizza Sales in 2015

    Based on the fake pizzaplace data from `{gt}`

    < section id="add-groups" class="level2">

    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)
        )
      )
    ) |> 
      reactablefmtr::add_title(
        title = 'Hawaiian Pizza Sales in 2015'
      ) |> 
      reactablefmtr::add_subtitle(
        subtitle = 'Based on the fake pizzaplace data from `{gt}`',
        _weight = 'normal'
      )

    Hawaiian Pizza Sales in 2015

    Based on the fake pizzaplace data from `{gt}`

    < section id="add-summaries" class="level2">

    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'
        )
      )
    ) |> 
      reactablefmtr::add_title(
        title = 'Hawaiian Pizza Sales in 2015'
      ) |> 
      reactablefmtr::add_subtitle(
        subtitle = 'Based on the fake pizzaplace data from `{gt}`',
        _weight = 'normal'
      )

    Hawaiian Pizza Sales in 2015

    Based on the fake pizzaplace data from `{gt}`

    < section id="make-table-searchable" class="level2">

    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'
        )
      )
    ) |> 
      reactablefmtr::add_title(
        title = 'Hawaiian Pizza Sales in 2015'
      ) |> 
      reactablefmtr::add_subtitle(
        subtitle = 'Based on the fake pizzaplace data from `{gt}`',
        _weight = 'normal'
      )

    Hawaiian Pizza Sales in 2015

    Based on the fake pizzaplace data from `{gt}`

    < section id="add-a-total-footer" class="level2">

    Add a total footer

    There are two ways to add a total footer to a column. One, you can use an R function that takes the values and column name as an argument and calculates the result from that. Or two, you can use a JavaScript function to the same thing but dynamically. Let’s first start with the R-approach and then see its drawback.

    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 = function(values, name) {
            sum(values)
          }
        ),
        revenue = colDef(
          name = 'Revenue',
          format = colFormat(currency = 'USD', separators = TRUE),
          aggregate = 'sum',
          footer = function(values, name) {
            sum(values) |> scales::dollar()
          }
        )
      )
    ) |> 
      reactablefmtr::add_title(
        title = 'Hawaiian Pizza Sales in 2015'
      ) |> 
      reactablefmtr::add_subtitle(
        subtitle = 'Based on the fake pizzaplace data from `{gt}`',
        _weight = 'normal'
      )

    Hawaiian Pizza Sales in 2015

    Based on the fake pizzaplace data from `{gt}`

    But if you filter for January now, you will see that the overall total doesn’t update. That’s not something that R can do for you. That’s where JavaScript comes in. Thankfully, the reactable cookbook gives you the JS code 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' })
          }")
        )
      )
    ) |> 
      reactablefmtr::add_title(
        title = 'Hawaiian Pizza Sales in 2015'
      ) |> 
      reactablefmtr::add_subtitle(
        subtitle = 'Based on the fake pizzaplace data from `{gt}`',
        _weight = 'normal'
      )

    Hawaiian Pizza Sales in 2015

    Based on the fake pizzaplace data from `{gt}`

    < section id="change-row-styling" class="level2">

    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' 
        )
      )
    ) |> 
      reactablefmtr::add_title(
        title = 'Hawaiian Pizza Sales in 2015'
      ) |> 
      reactablefmtr::add_subtitle(
        subtitle = 'Based on the fake pizzaplace data from `{gt}`',
        _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',
              Weight: 600
            }
          } 
        }"
      ),
    ) |> 
      reactablefmtr::add_title(
        title = 'Hawaiian Pizza Sales in 2015'
      ) |> 
      reactablefmtr::add_subtitle(
        subtitle = 'Based on the fake pizzaplace data from `{gt}`',
        _weight = 'normal'
      )

    Hawaiian Pizza Sales in 2015

    Based on the fake pizzaplace data from `{gt}`

    < section id="change-footer-style" class="level2">

    Change footer style

    But defining plain CSS without having to wrap it in JS code can still be useful. For example, you could make the footer a bit nicer.

    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
          }"),
          footerStyle = htmltools::css(
            _weight = 600,
            border_top = '2px solid black'
          )
        ),
        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' })
          }"),
          footerStyle = htmltools::css(
            _weight = 600,
            border_top = '2px solid black'
          )
        )
      ),
      rowStyle = JS(
        "function(rowInfo) {
          if (rowInfo.level == 0) { // corresponds to row group
            return { 
              background: '#E7EDF3', 
              borderLeft: '2px solid #104E8B',
              Weight: 600
            }
          } 
        }"
      ),
    ) |> 
      reactablefmtr::add_title(
        title = 'Hawaiian Pizza Sales in 2015'
      ) |> 
      reactablefmtr::add_subtitle(
        subtitle = 'Based on the fake pizzaplace data from `{gt}`',
        _weight = 'normal'
      )

    Hawaiian Pizza Sales in 2015

    Based on the fake pizzaplace data from `{gt}`

    To leave a comment for the author, please follow the link and comment on their blog: Albert Rapp.

    R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
    Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
  • Exit mobile version