{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. <blockquote>Are you looking to port a React library to Shiny? Try {shiny.react} and <a href="https://appsilon.github.io/shiny.react/articles/shiny-react.html" target="_blank" rel="noopener">port libraries like Liquid Oxygen to Shiny</a>.</blockquote> 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. <img class="alignnone size-full wp-image-17436" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01c1bd62ffcb22a8510a1_reactable-podium-leaderboard-in-R-Shiny-with-sass.webp" alt="reactable podium leaderboard built in R Shiny with sass" width="1560" height="1204" /> For full scripts and data used, head to our <a href="https://github.com/Appsilon/community-gists/tree/main/reactable/podium_style_leaderboard" target="_blank" rel="noopener">Community-Gists repo</a>. TOC: <ul><li><a href="#kickingoff">Kicking Things Off with Data</a></li><li><a href="#begins">The Game Begins - Building a {reactable} Leaderboard in Shiny</a></li><li><a href="#podium">Making the Podium</a></li><li><a href="#table">Making the Table</a></li><li><a href="#style">Finishing in Style</a></li><li><a href="#final">Final Whistle</a></li></ul> <hr /> <h2 id="kickingoff">Kicking Things Off with Data</h2> For the context of this tutorial, we will use data from the <a href="https://www.kaggle.com/datasets/sayanroy729/fifa-worldcup-2022-results" target="_blank" rel="nofollow noopener">FIFA Worldcup 2022 Results</a> 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 <b>Total Points</b> of each country. A bit of wrangling will get us something that looks like <a href="https://github.com/Appsilon/community-gists/blob/main/reactable/podium_style_leaderboard/data/total_points.csv" target="_blank" rel="noopener">this</a> .csv file. <img class="alignnone size-full wp-image-17438" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01c1c87a667cfae8d2f0a_table-data-for-reactable-leaderboard.webp" alt="table data for reactable leaderboard " width="1600" height="512" /> <h3>Using ChatGPT to find country codes</h3> 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 <a href="https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2" target="_blank" rel="nofollow noopener">Wikipedia</a>, 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: <img class="alignnone size-full wp-image-17434" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01c1de7ac33b972b46eea_country-codes-with-chatgpt.webp" alt="how to find country codes with chatGPT" width="1246" height="896" /> 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. <h2 id="begins">The Game Begins - Building a {reactable} Leaderboard in Shiny</h2> 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. <pre><code> #' @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() } </code></pre> 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. <pre><code> leaderboard_data <- reactive({ <br> 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 <br> }) </code></pre> This gives us a reactive object <i>leaderboard_data()</i>, which can then be used in a <i>reactable() </i>call. But before that, we need to separate the Top 3 to create the podium. <blockquote>Is your Shiny app performing well? Get results by <a href="https://appsilon.github.io/shiny.benchmark/" target="_blank" rel="noopener">measuring and comparing Shiny app performance with shiny.benchmark</a>.</blockquote> <h2 id="podium">Making the Podium</h2> The first half of the code below takes the Top 3 countries and creates three separate <i>div</i> elements. Each div has the class <i>podium_box </i>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. <pre><code> output$podium <- renderUI({ <br> 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 <br> countries <- top_three$Country flags <- top_three$Flag <br> 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]) ) ), ) }) </code></pre> <h2 id="table">Making the Table - reactable()</h2> 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. <pre><code> 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 } )) ) }) </code></pre> Since we can define each column using {reactable}’s colDef(), we define the Country to be a <i>div </i>with the name and the flag both, and then for the rank, we use the<i> html = TRUE </i>parameter to pass in an HTML string to create superscript for the positions, such as 4th and 7th using the <i><sup> </i>HTML tags. <h2 id="style">Finishing in Style - SASS and Shiny UI</h2> 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 <i>ui() </i>of an R/Shiny app. <pre><code> css <- sass(sass_file("style.scss")) ui <- fluidPage( tags$head(tags$style(css)), … … … ) <br></code></pre> To see the CSS formatting, check out the <a href="https://github.com/Appsilon/community-gists/blob/main/reactable/podium_style_leaderboard/style.scss" target="_blank" rel="noopener">full style.scss</a>. As you may notice, we use <i>.podium_box </i>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 <i>display</i> to <i>none</i>. Some minor styling later, we have a table that has a Top 3 podium on top. <h2 id="whistle">Final Whistle - {reactable} Podium on a Shiny Leaderboard</h2> 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. <blockquote>Looking for a way to build consistent, high-quality production Shiny apps? <a href="https://appsilon.github.io/rhino/" target="_blank" rel="noopener">Try Rhino - an R package designed to help users create Shiny apps like a fullstack software engineer.</a></blockquote>