├── Plots ├── 3294 (20).png ├── 5555 (2).png └── ksdnls ├── README.md └── app.R /Plots/3294 (20).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshkrishna17/PlayerFinishingOverviewShiny/fe77e24e6c5b952691008b23585e8e28fdf6c974/Plots/3294 (20).png -------------------------------------------------------------------------------- /Plots/5555 (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshkrishna17/PlayerFinishingOverviewShiny/fe77e24e6c5b952691008b23585e8e28fdf6c974/Plots/5555 (2).png -------------------------------------------------------------------------------- /Plots/ksdnls: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xG-Race-Shiny 2 | A Shiny app that creates xG based finishing reports for soccer players using data from Understat. 3 | 4 | [You can find the app here.](https://harshkrishna.shinyapps.io/PlayerFinishingOverview/) 5 | 6 | ## App on Local System 7 | 8 | Here is how you can run the app on your local system :- 9 | 10 | 1. Install R and Rstudio. 11 | 2. Clone this repository. 12 | 3. In RStudio, install all the packages needed. 13 | 4. Run the entire `app.R` script. 14 | -------------------------------------------------------------------------------- /app.R: -------------------------------------------------------------------------------- 1 | # Libraries 2 | 3 | library(glue) 4 | library(tidyverse) 5 | library(ggsoccer) 6 | library(TTR) 7 | library(ggtext) 8 | library(patchwork) 9 | library(understatr) 10 | library(hexbin) 11 | library(shiny) 12 | library(shinyWidgets) 13 | library(devtools) 14 | library(ggbraid) 15 | 16 | # UI 17 | 18 | ui <- fluidPage( 19 | setBackgroundColor("#14171A"), 20 | titlePanel(div("Player Finishing Overview", style = "color:#D81B60"), windowTitle = "Player Finishing Overview"), 21 | setSliderColor("#D81B60", 1), 22 | sidebarLayout( 23 | sidebarPanel( 24 | numericInput("player", "Understat Player ID:", value = 3294), 25 | selectizeInput("situation", "Situation:", choices = c("OpenPlay", "DirectFreekick", "FromCorner", "SetPiece", "Penalty"), multiple = TRUE, selected = c("OpenPlay", "DirectFreekick", "FromCorner", "SetPiece", "Penalty")), 26 | selectizeInput("shotType", "Shot Type:", choices = c("LeftFoot", "RightFoot", "Head", "OtherBodyPart"), multiple = TRUE, selected = c("LeftFoot", "RightFoot", "Head", "OtherBodyPart")), 27 | sliderInput("year", "Year:", 28 | min = 2014, max = 2021, 29 | value = c(2014, 2021), 30 | sep = ""), 31 | numericInput("roll_avg", "Rolling Average of Line Chart:", value = 50), 32 | selectInput("shots", "Shot Map Type:", choices = c("Point", "Hexbin"), selected = "Point"), 33 | selectizeInput("plots", "Plots:", choices = c("All", "Line Chart", "Shot Map", "Histogram")), 34 | radioButtons("theme", "Background Theme:", choices = c("Dark", "Light"), selected = "Dark"), 35 | downloadButton("download", "Download Plot") 36 | ), 37 | mainPanel(h2("Introduction & Plot", align = "center", style = "color:white"), 38 | h4("This simple Shiny app generates a dashboard of visualizations that can be useful in getting an overview of a soccer player's finishing ability. Play around with the options for customizations and try to gain interesting insights!", style = "color:white"), 39 | h5("Created by Harsh Krishna (@veryharshtakes)", style = "color:white"), 40 | plotOutput("plot")) 41 | ) 42 | ) 43 | 44 | # Server 45 | 46 | server <- function(input, output, session) { 47 | 48 | plot_fun <- reactive({ 49 | 50 | # Data 51 | 52 | req(input$player) 53 | req(input$roll_avg) 54 | 55 | dataset <- reactive({ 56 | data <- get_player_shots(input$player) 57 | }) %>% 58 | bindCache(input$player) 59 | 60 | data <- dataset() 61 | 62 | # Theme 63 | 64 | if(input$theme == "Dark") { 65 | fill_b <- "#212121" 66 | colorText <- "white" 67 | colorLine <- "white" 68 | gridline <- "#525252" 69 | } 70 | else if(input$theme == "Light") { 71 | fill_b <- "floralwhite" 72 | colorText <- "black" 73 | colorLine <- "black" 74 | gridline <- "#9E9E9E" 75 | } 76 | 77 | # Data Wrangling 78 | 79 | data <- data %>% 80 | mutate(X = X * 120, 81 | Y = Y * 80) %>% 82 | mutate(result = ifelse(result == "Goal", "Goal", "No Goal")) %>% 83 | mutate(isGoal = ifelse(result == "Goal", 1, 0)) 84 | 85 | line_data <- data %>% 86 | mutate(GxG = isGoal - xG) %>% 87 | mutate(GxGSM = TTR::SMA(GxG, n = input$roll_avg)) %>% 88 | mutate(date = as.Date(date)) %>% 89 | filter(year >= input$year[1], 90 | year <= input$year[2], 91 | situation %in% input$situation, 92 | shotType %in% input$shotType) 93 | 94 | shot_data <- data %>% 95 | filter(year >= input$year[1], 96 | year <= input$year[2], 97 | situation %in% input$situation, 98 | shotType %in% input$shotType) 99 | 100 | hist_data <- data %>% 101 | filter(year >= input$year[1], 102 | year <= input$year[2], 103 | situation %in% input$situation, 104 | shotType %in% input$shotType) 105 | 106 | # Custom Theme 107 | 108 | theme_custom <- function() { 109 | theme_minimal() + 110 | theme(plot.background = element_rect(colour = fill_b, fill = fill_b), 111 | panel.background = element_rect(colour = fill_b, fill = fill_b)) + 112 | theme(plot.title = element_text(colour = colorText, size = 21, face = "bold", hjust = 0.5), 113 | plot.subtitle = element_markdown(colour = colorText, size = 16, hjust = 0.5), 114 | plot.caption = element_text(colour = colorText, size = 12, hjust = 1), 115 | axis.title.x = element_text(colour = colorText, face = "bold", size = 12), 116 | axis.title.y = element_text(colour = colorText, face = "bold", size = 12), 117 | axis.text.x = element_text(colour = colorText, size = 8), 118 | axis.text.y = element_text(colour = colorText, size = 8)) + 119 | theme(panel.grid.major = element_line(colour = gridline, size = 0.4, linetype = "dashed"), 120 | panel.grid.minor = element_line(colour = gridline, size = 0.4, linetype = "dashed")) + 121 | theme(panel.grid.major.x = element_line(colour = gridline, size = 0.4, linetype = "dashed"), 122 | panel.background = element_blank()) + 123 | theme(legend.title = element_text(colour = colorText), 124 | legend.text = element_text(colour = colorText)) 125 | } 126 | 127 | # Plot 128 | 129 | g1 <- ggplot(line_data, aes(x = date, y = GxGSM)) + 130 | geom_line(size = 2) + 131 | geom_braid(aes(ymin = 0, ymax = GxGSM, fill = GxGSM > 0)) + 132 | scale_fill_manual(values = c("#D81B60", "#3949AB")) + 133 | geom_hline(yintercept = 0, size = 1, colour = colorLine, linetype = "longdash") + 134 | labs(title = glue("{data$player}"), subtitle = glue("{input$year[1]} - {input$year[2]} | League Games Only"), x = glue("{input$roll_avg} Shot Rolling Average"), y = "G - xG") + 135 | theme_custom() + 136 | theme(legend.position = "none") 137 | 138 | if(input$shots == "Point") { 139 | g2 <- ggplot() + 140 | annotate_pitch(dimensions = pitch_statsbomb, fill = fill_b, colour = colorLine) + 141 | coord_flip(xlim = c(60,120), 142 | ylim = c(80, -2)) + 143 | theme_pitch() + 144 | geom_point(data = shot_data, aes(x = X, y = Y, fill = result, size = xG), colour = colorLine, shape = 21, show.legend = FALSE) + 145 | scale_fill_manual(values = c("#3949AB", fill_b)) + 146 | labs(y = glue("{sum(shot_data$isGoal)} Goals with {round(sum(shot_data$xG))} xG\nfrom {nrow(shot_data)} Shots."), 147 | x = glue("Data via Understat\nAccurate as per {Sys.Date()}")) + 148 | theme_custom() + 149 | theme(panel.grid.major = element_blank(), 150 | panel.grid.minor = element_blank(), 151 | axis.title.y = element_text(size = 8), 152 | axis.text.x = element_blank(), 153 | axis.text.y = element_blank(), 154 | axis.ticks.x = element_blank(), 155 | axis.ticks.y = element_blank(), 156 | aspect.ratio = 0.5) 157 | } else if(input$shots == "Hexbin") { 158 | g2 <- ggplot() + 159 | annotate_pitch(dimensions = pitch_statsbomb, fill = fill_b, colour = colorLine) + 160 | coord_flip(xlim = c(60,120), 161 | ylim = c(80, -2)) + 162 | theme_pitch() + 163 | geom_hex(data = shot_data, aes(x = X, y = Y), bins = 30, colour = colorLine, show.legend = FALSE) + 164 | scale_fill_gradient(low = "#D81B60", high = "#3949AB") + 165 | labs(y = glue("{sum(shot_data$isGoal)} Goals with {round(sum(shot_data$xG))} xG\nfrom {nrow(shot_data)} Shots."), 166 | x = glue("Data via Understat\nAccurate as per {Sys.Date()}")) + 167 | theme_custom() + 168 | theme(panel.grid.major = element_blank(), 169 | panel.grid.minor = element_blank(), 170 | axis.title.y = element_text(size = 8), 171 | axis.text.x = element_blank(), 172 | axis.text.y = element_blank(), 173 | axis.ticks.x = element_blank(), 174 | axis.ticks.y = element_blank(), 175 | aspect.ratio = 0.5) 176 | } 177 | 178 | g3 <- ggplot() + 179 | geom_histogram(data = hist_data, aes(x = xG, fill = result), bins = 20, position = position_stack(reverse = TRUE)) + 180 | scale_fill_manual(values = c("#3949AB", "#D81B60")) + 181 | labs(x = "Individual Shot xG", 182 | y = "Frequency") + 183 | geom_vline(xintercept = mean(hist_data$xG), colour = colorLine, size = 0.5, linetype = "longdash") + 184 | annotate(geom = "text", x = mean(hist_data$xG) + 0.02, y = 50, label = glue("xG/Shot=", round(mean(hist_data$xG), 2)), colour = colorText, size = 4) + 185 | theme_custom() + 186 | theme(legend.position = c(0.9, 0.9), 187 | legend.title = element_blank()) 188 | 189 | if ("All" %in% input$plots) { 190 | 191 | my_plot <- g1 / (g2 | g3) 192 | my_plot & 193 | plot_annotation(caption = "Created by @veryharshtakes", 194 | theme = theme(plot.background = element_rect(fill = fill_b, colour = fill_b), 195 | plot.caption = element_text(colour = colorText, hjust = 1, size = 10))) 196 | 197 | } else if ("Line Chart" %in% input$plots) { 198 | 199 | g1 & 200 | plot_annotation(caption = "Created by @veryharshtakes", 201 | theme = theme(plot.background = element_rect(fill = fill_b, colour = fill_b), 202 | plot.caption = element_text(colour = colorText, hjust = 1, size = 10))) 203 | 204 | } else if ("Shot Map" %in% input$plots) { 205 | 206 | g2 + 207 | theme(axis.title.y = element_blank()) & 208 | plot_annotation(caption = "Created by @veryharshtakes", 209 | title = glue("{data$player}"), subtitle = glue("{input$year[1]} - {input$year[2]} | League Games Only"), 210 | theme = theme(plot.background = element_rect(fill = fill_b, colour = fill_b), 211 | plot.caption = element_text(colour = colorText, hjust = 1, size = 10), 212 | plot.title = element_text(colour = colorText, hjust = 0.5, size = 21, face = "bold"), 213 | plot.subtitle = element_text(colour = colorText, hjust = 0.5, size = 16))) 214 | 215 | } else if ("Histogram" %in% input$plots) { 216 | 217 | g3 & 218 | plot_annotation(caption = "Created by @veryharshtakes", 219 | title = glue("{data$player}"), subtitle = glue("{input$year[1]} - {input$year[2]} | League Games Only"), 220 | theme = theme(plot.background = element_rect(fill = fill_b, colour = fill_b), 221 | plot.caption = element_text(colour = colorText, hjust = 1, size = 10), 222 | plot.title = element_text(colour = colorText, hjust = 0.5, size = 21, face = "bold"), 223 | plot.subtitle = element_text(colour = colorText, hjust = 0.5, size = 16))) 224 | 225 | } 226 | }) 227 | 228 | output$plot <- renderPlot({ 229 | 230 | plot_fun() 231 | 232 | }) 233 | 234 | # Download 235 | 236 | output$download <- downloadHandler( 237 | filename = function() { paste(input$player, ".png", sep="") }, 238 | content = function(file) { 239 | device <- function(..., width, height) grDevices::png(..., width = 10, height = height, res = 300, units = "in") 240 | ggsave(file, plot = plot_fun(), device = device, bg = "#212121") 241 | } 242 | ) 243 | } 244 | 245 | shinyApp(ui = ui, server = server) 246 | --------------------------------------------------------------------------------