Join the R Community at ShinyConf 2023

{reactable} Podium – How to Build a Leaderboard in R Shiny


{reactable} is pretty powerful in itself given just how many features are available in it. But its real power is in how we can tinker with it. In one of our R Shiny projects, we needed a leaderboard of sorts. So, we figured, why not make things interesting and add a podium on top? This blog post walks you through a similar example using the same technique.

Are you looking to port a React library to Shiny? Try {shiny.react} and port libraries like Liquid Oxygen to Shiny.

The idea is to understand {reactable} enough to combine it with simple HTML elements and make it all look like one cohesive table. The end result will look something like the following.

For full scripts and data used, head to our Community-Gists repo.

TOC:


Kicking Things Off with Data

For the context of this tutorial, we will use data from the FIFA Worldcup 2022 Results dataset published on Kaggle by Sayan Roy to visualize the results based on Total Points. ⚽

The overall data is a bit too much so we will manually subset it to only have the Total Points of each country.

A bit of wrangling will get us something that looks like this .csv file.

Using ChatGPT to find country codes

We need the country codes to reliably produce the flags for our leaderboard, but how do we get the ISO Alpha-2 codes? We can get them from Wikipedia, of course. But then, we have to pick out the specific countries in our data, then manually sort them out and assign the column.

If only there was a publicly available, smart, AI tool available? 🤔 Lucky for us, ChatGPT exists, and that is what we used! Here’s how:

Note: Relying blindly on any tool such as ChatGPT can introduce errors, so please make sure you always verify the data you receive.

Now that the data is ready, we can begin with the leaderboard.

The Game Begins – Building a {reactable} Leaderboard in Shiny

First, we need a function to fetch the flag emoji, which you can do by taking the ISO Alpha-2 codes for each country and applying the following function.


#' @description simple function to get country emoji
#' @param country_codes vector of alpha-2 country codes
#' @return vector with country flag emoji
#'
get_flag <- function(country_codes) { sapply(country_codes, function(country_code) { if (is.null(country_code) || is.na(country_code)) { return(NULL) } intToUtf8(127397 + strtoi(charToRaw(toupper(country_code)), 16L)) }) |>
    as.vector()
}

You can even pass a vector of country codes and it will return the flags. This is exactly what we used when we wrangled the data in the server part of our Shiny app.


leaderboard_data <- reactive({

    data <- read.csv("data/total_points.csv")
    data <- data[order(data$total_points, decreasing = TRUE), ]
    data$rank <- seq(1:nrow(data))
    data$total_points <- NULL
    data$alpha.2 <- get_flag(data$alpha.2)
    data$country <- paste(data$country, data$alpha.2, sep = "_")
    data$alpha.2 <- NULL
    data

    })

This gives us a reactive object leaderboard_data(), which can then be used in a reactable() call.

But before that, we need to separate the Top 3 to create the podium.

Is your Shiny app performing well? Get results by measuring and comparing Shiny app performance with shiny.benchmark.

Making the Podium

The first half of the code below takes the Top 3 countries and creates three separate div elements. Each div has the class podium_box and a specific id corresponding to its position. Using {sass}, we will later read in a .sass file that defines these attributes with CSS styles.


output$podium <- renderUI({

      top_three <- head(leaderboard_data(), 3)$country
      top_three <- data.frame(t(data.frame(strsplit(top_three, "_"))))
      colnames(top_three) <- c("Country", "Flag")
      rownames(top_three) <- NULL

      countries <- top_three$Country
      flags <- top_three$Flag

      div(id = "podium",
          div(id = "second_place",
              class = "podium_box",
              div(class = "podium_country",
                  p(class = "country_flag",
                    flags[2]),
                  p(class = "country_label",
                    countries[2])
              )
          ),
          div(id = "first_place",
              class = "podium_box",
              div(class = "podium_country",
                  p(class = "country_flag",
                    flags[1]),
                  p(class = "country_label",
                    countries[1])
              )
          ),
          div(id = "third_place",
              class = "podium_box",
              div(class = "podium_country",
                  p(class = "country_flag",
                    flags[3]),
                  p(class = "country_label",
                    countries[3])
              )
          ),
      )
    })

Making the Table – reactable()

The second half of the code creates a reactable() definition for the data starting from the 4th place to the 10th place. We discard the rest of the data since we do not require it for the leaderboard. 1st to 10th place seems like a good enough display of information in most contexts.


output$leaderboard <- renderReactable({
      reactable(data = leaderboard_data()[4:10, ],
                style = list(backgroundColor = "rgba(255, 255, 255, 0.4)"),
                borderless = TRUE,
                outlined = FALSE,
                striped = TRUE,
                pagination = FALSE,
                width = "100%",
                columns = list(
                  country = colDef(
                    cell = function(value) {
                      value <- strsplit(value, "_")[[1]]
                      div(class = "leaderboard_row",
                          p(class = "country_flag_row",
                            value[2]),
                          p(class = "country_label_row",
                            value[1])
                      )
                    }
                  ),
                  rank = colDef(
                    html = TRUE,
                    cell = function(value) {
                      value <- paste0("<p class = 'country_rank_row'>",
                                      value, "<sup>th</p>"
                      )
                      value
                    }
                  ))
      )
    })

Since we can define each column using {reactable}’s colDef(), we define the Country to be a div with the name and the flag both, and then for the rank, we use the html = TRUE parameter to pass in an HTML string to create superscript for the positions, such as 4th and 7th using the <sup> HTML tags.

Finishing in Style – SASS and Shiny UI

The structure is all set. Now, we need a little styling to move from messy to Messi. That’s where the {sass} package comes in. It’s a convenient way to read a .sass file into an R variable. We can then use it anywhere, even in the ui() of an R/Shiny app.


css <- sass(sass_file("style.scss"))
ui <- fluidPage(
  tags$head(tags$style(css)),
…
…
…
)

To see the CSS formatting, check out the full style.scss.

As you may notice, we use .podium_box to define the basic look and feel of the podium steps for all positions in the Top 3. Then, we use a specific rule to set the gradient, the height, and the margin of the flag from the top.

Also, since the {reactable} header is still visible, we cascade into it and set its display to none.

Some minor styling later, we have a table that has a Top 3 podium on top.

Final Whistle –  {reactable} Podium on a Shiny Leaderboard

After following the steps above and referring to the code on GitHub, you now have a {reactable} table with a podium on top. This is, of course, not a tutorial to only create a FIFA leaderboard. It is an example to illustrate how we can customize certain elements and tinker with things to create something we require specifically for our use case.

As far as the leaderboard is concerned, we’d be honored if you “borrowed” our code and used it to visualize something in your own project. Share your project output with us on LinkedIn or Mastodon!

Thank you for following along.

Looking for a way to build consistent, high-quality production Shiny apps? Try Rhino – an R package designed to help users create Shiny apps like a fullstack software engineer.