# Libraries----
library(shiny)
library(understatr)
library(tidyverse)
library(DT)
library(ggrepel) # include automated positioned text in ggplot
library(ggimage) # include team logo in ggplot
library(shinycssloaders) # Nice spinner to show while loading outputs
library(bslib) # to themed the shiny
library(thematic) # to pass the bslib theme to ggplots
I will document the making of a selectable and editable DT::dataTable in shiny. This will also be my submission to participate on RStudio table contest for 2021. I will use understatr
package to get_league_teams_stats
for the 2021/2022 Premier League and build a standings table. This table will be selectable in order to display cumulative xG for the selected team, and editable to add comments for the team in a Total xG plot. Visit shiny here
Packages
Getting Data
Let’s use understatr::get_league_teams_stats
to retrieve results for every team in the 2021/2022 Premier League. team_logo.csv
contains image addresses for the logo of every team
<- get_league_teams_stats(league_name = "EPL", year = 2021)
epl_2021 <- read_csv('table-contest-2021/team_logo.csv') team_logo
Let’s summarise epl_2021 and join the URL of the logos, which will be our standing table and also our dataframe to display Total xG plot. We also made the cumulative xG dataframe that will be team filtered every time we select a row in the standings table to display only the curve for the selected team
# Standings
<- epl_2021 %>%
epl_standings group_by(team_name) %>%
summarise(
Loses = sum(loses),
Draws = sum(draws),
Wins = sum(wins),
Pts = sum(pts),
GD = sum(scored)-sum(missed),
Comment = '',
xG = sum(xG),
xGA = sum(xGA),
.groups = 'drop'
%>%
) arrange(desc(Pts)) %>%
left_join(team_logo) %>%
mutate(Team = sprintf('<img src="%s" height="28"></img>', logo)) %>%
relocate(Team)
# Cumulative xG
<- epl_2021 %>%
roll_xG group_by(team_name) %>%
arrange(team_name, date) %>%
transmute(
match_day = 1:n(),
team_name,xG = cumsum(xG),
xGA = cumsum(xGA),
G = cumsum(scored),
GA = cumsum(missed)
%>%
) ungroup() %>%
pivot_longer(contains('G')) %>%
mutate(type = if_else(name %in% c('G', 'GA'), 'real', 'expected'))
Setting Reactivity and observes
The first thing we need to do is create a reactiveValues
and set an observe
to catch the value every time the source dataframe changes, in this case, it would not change since epl_standings is static, but this would be necessary if epl_standings were reactive. Also, we need to create a dataTableProxy
to store the previous value for the main dataTable -‘standings’ is our DT::dataTable
that we will create later on-
# Create Empty Reactive Value
<- reactiveValues(df = NULL)
table_reactive_value
# observe in case of epl_standings would be reactive
observe({
$df <- epl_standings
table_reactive_value
})
# Creating table proxy to store previous values
<- dataTableProxy('standings') standings_proxy
Next, we set the observeEvent
that in the event of a cell edit, will update the value of the reactiveValue
that stores the copy of the source dataframe. Note the event will always be an input of the nameOfTheDT_cell_edit
# Update table
observeEvent(input$standings_cell_edit, {
= input$standings_cell_edit
info str(info)
# Location and value of the edit
= info$row
i = info$col
j = info$value
v
# Coerce value of the same data type and replace proxy
$df[i, j] <- DT::coerceValue(v, pull(table_reactive_value$df[i, j]))
table_reactive_value<- table_reactive_value$df
aux_table replaceData(standings_proxy, aux_table, resetPaging = FALSE, rownames = T)
})
Now lets build the DT::dataTable
. The only column that will not be selectable is Comment which is column number 8, so we build a matrix of nrow(epl_standings) rows and two columns that maps the cells of epl_standings that will not be selectable. Column one and column two indicates row number and column number respectively. All values are negative, in that way we specify the no selectable parts of the table
The editable range is easier to set with the disable argument. Note that there are 3 columns that we do not want to display in the table, we do not use dplyr::select
to remove them, instead, we set columnDefs
argument otherwise would be a mismatch between the reactiveValue and the dataTable columns
$standings <- DT::renderDataTable({
output
<- ncol(epl_standings)
ncolumns <- nrow(epl_standings)
nrows <- matrix(c(-(1:nrows), rep(-8, nrows)), ncol = 2)
unselectable_matrix
%>%
epl_standings ::datatable(
DTrownames = T,
style = 'bootstrap',
selection = list(mode = 'single', target = "cell", selectable = unselectable_matrix),
editable = list(target = "cell", disable = list(columns = 1:7)),
escape = F,
options = list(
searching = F,
paging = F,
info = F,
columnDefs = list(
list(visible=FALSE, targets = c(9,10,11)),
list(className = 'dt-center', targets = '_all')
)
)
)
})
Now we use the reactiveValue to build the output with the dataframe edited, in this case, a ggplot
$total_xG <- renderPlot({
output
$df %>%
table_reactive_valueggplot(aes(x = xG, y = xGA, label = Comment)) +
geom_image(aes(image = logo), asp = 10/5, size = .04) +
geom_text_repel(box.padding = 1, max.overlaps = Inf, size = 4,
min.segment.length = 0, segment.curvature = -.5) +
labs(x = 'Expected Goal (xG)', y = 'Expected Goal Against (xGA)')
})
ShowModal of selected row
We set a reactive that activates when a _cells_selected
and stores the name of the team in the selected row. With the reactive team_selected
we filter the dataframe and build the ggplot. Finally, we set the observeEvent
that triggers the modal based on the dataTable_cells_selected
<- reactive({
team_selected <- epl_standings %>%
selected_team slice(input$standings_cells_selected[1,1]) %>%
pull(team_name)
})
# Modal to display cumulative xG
observeEvent(
$standings_cells_selected,
input
{if(nrow(input$standings_cells_selected)>0){
showModal(
modalDialog(
title = paste0(team_selected(), " Cumulative xG"),
easyClose = T,
size = 'l',
plotOutput('rolling_xG')
)
)
}
} )