Building a Post-Game Pitcher Report with Statcast Data

The ‘shiny’ package in the R programming language is a powerful tool to develop interactive web applications straight from your local RStudio. The package’s ease of use has made it a popular choice among programmers looking to naively explore application development. Since we covered the basics of ‘shiny’ in my last post, this article will skip past the fundamentals. To date, we’ve covered data manipulation, data visualization, and shiny applications, which have laid a foundation to build a post-game pitcher report with Statcast data.

As we’ve done in previous posts, we will stick with the ‘NL_CY’ dataset that includes 2020 Statcast data from Trevor Bauer, Yu Darvish, and Jacob deGrom. As we dive deeper into building specific applications, the code is not as cut-and-dry to include all of it directly in this article. Instead, I’ll strongly encourage you to download the application’s code at this GitHub link and follow along as I pinpoint and describe specific chunks of code.

As always, you can view the published application through my shinyapps account here.

User Interface (UI) Code

CSS Styling

Cascading Style Sheets, otherwise known as CSS, is used alongside HTML and JavaScript as a mechanism to add style to web applications. The extent of CSS styling goes far beyond the examples provided in this post-game report. Read more about using CSS in shiny applications here and here.

wellPanel(style = "background: white; border-color: black; border-width: 2px", …)
hr(style="border-color: black;")

The wellPanel() is the border you see around the report’s information. With no styling, the well panel has a grey background with an outline that is barely visible. To remove the grey background and increase the outline effect, I’ve applied the three CSS styles you see above.

The hr() is the horizontal line placed above the outlined report (not shown above). The default color is grey, so I’ve also changed the border color to black.

Updating Select Inputs

choices = ""

You might wonder why the choices for the select game input are left as blank quotations. This is intentional, and is done this way to allow the drop-down list of games to update based on the selected pitcher. We’ll cover this more in the “Observe Event” section down below.

Text Outputs



The textOutput() function displays text generated from a specific input. This portion requires code written in the server, which is covered below in “Render Text.” I’ve also applied h2() to both the pitcher output and game output, which specifies the text as headers. h1() is the biggest header size, with the font decreasing as that number increases (h3, h4, etc.). Finally, strong() indicates that the text is bold.

Server Code


This chunk of code is actually a part of our “global code”, which is anything we execute before the ui and server sections. I’ve included it in this section of the article because it also could be placed in your server code.

NL_CY$pitch_type <- factor(NL_CY$pitch_type, levels = c("FF", "SI", "FC", "CS", "CU", "KC", "SL", "CH", "FS"))

NL_CY$pitch_name <- factor(NL_CY$pitch_name, levels = c("4-Seam Fastball", "Sinker", "Cutter", "Curveball", "Knuckle Curve", "Slider", "Changeup", "Split-Finger"))

These two lines above are establishing an order for our “pitch_type” and “pitch_name” variables. The technical terminology is we are changing the order of levels for a factor. Both pitch variables are imported as character types. When a list of pitches is displayed in a table or plot legend, the order will be alphabetical, which sometimes isn’t desired.

Observe Event

 inputId = "GameInput", 
 label = "Select Game", 
 choices = sort(unique(NL_CY$game_date[NL_CY$player_name == input$PitcherInput]))))

As I alluded to above, this observeEvent() function updates the game input list with only the games in which the chosen pitcher appears in. You’ll notice “session” is included in this code. This chunk will not work unless “session” is included in your server function, as seen below.

server <- function(input, output, session) { … }

An observeEvent() is a handy function to keep in your back pocket, especially once you start building more complex applications with more inputs. You can think of this function as a reaction between two events. If the user changes the input from the first event, there needs to be a reaction in the second input. In this case, when the pitcher changes, the dropdown list of games changes based on the new list of choices indexed in the updateSelectInput() function.

Render Text

output$selected_pitcher <- renderText({paste(input$PitcherInput)})
output$selected_game <- renderText({paste(input$GameInput)})

As mentioned above, the selected pitcher and game date are displayed on the report through the code above. The renderText() function simply takes the input and connects through textOutput() to render on the interface.

DT (Data Tables)

I have yet to introduce data tables here at Simple Sabermetrics. Using the DT library, the datatable() function is used to output a basic-styled table displaying pitcher summary statistics. The input and output interact with renderDataTable() in the server and dataTableOutput() in the ui.

output$pitcher_summary_table <- renderDataTable({
 table <- NL_CY %>%
 tableFilter <- reactive({table})
 options = list(dom = 't', 
 columnDefs = list(list(targets = 0, visible = FALSE)))) %>%
 formatStyle(c(1,2), `border-left` = "solid 1px") %>% 
 formatStyle(c(2,5,7), `border-right` = "solid 1px")

Within the renderDataTable() code above, “table” is built using dplyr code (the entirety of the code replaced with “...” to save space). To allow for reactivity, the table is transformed to “tableFilter” to update with user input.

Additional styling is applied in the “options” argument, which removes the labeling surrounding the table, removes the row numbers, and adds column borders. Read more about the DT library here.

Wrapping It Up

All together, these bits and pieces round out a post-game report that includes information on the selected pitcher’s pitch metrics and performance, and visualizes each pitch’s movement, location, and velocity. By keeping the application to the size of one page, the inputs quickly turn into outputs.

Reports can vary in size and in the information they display. Hopefully this article’s post-game pitcher report gives you the means necessary to develop your own style of report with the information you and your team care about. Post-game reporting is fundamental to the data-driven feedback loop for athletes. The ‘shiny’ package makes it possible to utilize the basics of the R programming language to develop interactive web applications built for reporting.

I’ve written more on the types of reports my team and I put together at Iowa on my personal Medium blog. If you’d like to dig further into reports built in Shiny, be sure to check out these links: Post-Action Pitcher Reports & BlastTrax Reports.

Back to blog