Building a Small-Sided Games (SSG) Pitch Dimensions Calculator App
This application is designed to help sports scientists, coaches, and practitioners optimize theArea perPlayer (ApP)and calculate the ideal pitch dimensions for training and matches.
Here is the link to the ApP Calculator.
Building a Small-Sided Games (SSG) Pitch Dimensions Calculator App for Sport Scientists
Welcome to this tutorial on how to replicate a small-sided games (SSG) pitch dimensions calculator app using R Shiny. This application is designed to help sports scientists, coaches, and practitioners optimize the Area per Player (ApP) and calculate the ideal pitch dimensions for training and matches.
In this tutorial, we will learn how to use R to build a simple, interactive web application using the Shiny framework. We'll walk through each part of the code and explain its purpose. Additionally, we will discuss how the app is tied to the research of Andrea Riboli et al. (2020), who explored how the ApP in small-sided games can affect the performance and physiological demands of players.
Research Context:
In soccer, small-sided games (SSGs) are a common training method to replicate the conditions of a real match, improving players' technical skills, fitness, and tactical understanding. The Area per Player (ApP) is a critical variable in designing SSGs. Research by Riboli et al. (2020) showed that adjusting ApP can influence players' physical exertion, including their high speed running, acceleration, deceleration, and overall distance covered.
By the end of this tutorial, we will have built an interactive Shiny app that allows us to:
Input the number of players and the pitch dimensions to calculate the Area per Player.
Calculate the required pitch dimensions based on a specified area per player.
This tutorial is targeted at beginner R programmers with a focus on sport science, and we will explain every line of code to help us understand the key concepts.
Step 1: Setting Up Our R Environment
To start, we'll need to install some R packages that will enable us to build this app. The Shiny framework allows us to create interactive web applications in R, and the shinydashboard package provides an easy way to create structured, dashboard-style interfaces.
Installing Required Packages
Before we begin coding, we need to install the necessary packages by running the following commands in our R console:
install.packages("shiny") # Core package for building Shiny apps
install.packages("shinydashboard") # Dashboard layout for Shiny apps
install.packages("shinythemes") # Pre-built themes for Shiny apps
After installation, we can load these packages into our R script:
library(shiny)
library(shinydashboard)
library(shinythemes)
What Does Each Package Do?
shiny
: The core package that powers the app's interactivity. It allows us to interact with inputs (e.g., text boxes, sliders) and display outputs (e.g., calculated results) dynamically.shinydashboard
: Provides a simple layout to create dashboards with a header, sidebar, and body. It's especially useful for organizing content in Shiny apps.shinythemes
: Offers pre-designed themes that make our Shiny app look polished and professional without needing to write custom CSS.
Step 2: Defining the Core Calculation Functions
The app will need two main calculation functions:
calculate_pitch_area_per_player
: This function will calculate the area available for each player based on the number of players and pitch dimensions (length and width).pitch_dimensions
: This function will calculate the required pitch dimensions (length and width) to achieve a specific area per player.
2.1 Function to Calculate Area per Player
This function takes the total number of players, pitch length, and pitch width as inputs.
It calculates the total pitch area (length × width) and divides it by the number of players to determine how much area each player will have.
calculate_pitch_area_per_player <- function(total_players, pitch_length, pitch_width) {
# Calculate the total pitch area
pitch_area <- pitch_length * pitch_width
# Calculate area per player by dividing the total pitch area by the number of players
area_per_player <- pitch_area / total_players
return(list(
area_per_player = round(area_per_player, 0), # Round the result for better presentation
pitch_area = round(pitch_area, 0) # Round the total pitch area
))
}
Breakdown of the Function
Input Parameters:
total_players
: The total number of players on the pitch.pitch_length
: The length of the pitch in meters.pitch_width
: The width of the pitch in meters.
Steps:
Calculate the total pitch area by multiplying the pitch length by the pitch width.
Calculate the area per player by dividing the total pitch area by the number of players.
Return the results as a list, where the values are rounded for clarity.
2.2 Function to Calculate Pitch Dimensions from Area per Player
This second function works in reverse.
Given the area per player and the total number of players, it calculates the required pitch length and width.
pitch_dimensions <- function(area_per_player, num_players) {
# Define the ratio of pitch width to length (standard in soccer fields)
width_length_ratio = 0.647 # This is based on typical soccer field proportions
# Calculate the total pitch area required to achieve the specified area per player
pitch_area_m2 <- area_per_player * num_players
# Calculate the length and width of the pitch based on the ratio
pitch_length_m <- sqrt(pitch_area_m2 / width_length_ratio)
pitch_width_m <- pitch_area_m2 / pitch_length_m
return(list(
pitch_length_m = round(pitch_length_m, 0), # Round for clarity
pitch_width_m = round(pitch_width_m, 0),
pitch_area_m2 = round(pitch_area_m2, 0)
))
}
Breakdown of the Function
Input Parameters:
area_per_player
: The area each player will have in square meters.num_players
: The number of players on the pitch.
Steps:
Calculate the total pitch area required to achieve the specified area per player by multiplying the area per player by the number of players.
Using the predefined ratio of width to length (based on soccer field proportions), calculate the pitch dimensions.
Return the results (pitch length, width, and total area).
Step 3: Building the User Interface (UI)
Now that we have the functions that will perform the necessary calculations, we can build the User Interface (UI) of the app.
The UI will include:
Input fields for the number of players and pitch dimensions.
Dropdown menus for selecting the type of calculation.
Output boxes to display the results of the calculations.
3.1 The UI Layout with shinydashboard
The UI will be built using the shinydashboard package, which allows us to create a clean and organized layout with a header, sidebar, and main body.
ui <- dashboardPage(
dashboardHeader(title = "Small Sided Games Pitch Dimensions Calculator"),
dashboardSidebar(
sidebarMenu(
menuItem("Calculator", tabName = "calculator", icon = icon("calculator")),
menuItem("SSG Mind Map Download", tabName = "images", icon = icon("image")),
menuItem("How to Guide", tabName = "guide", icon = icon("info-circle"))
)
),
dashboardBody(
theme = shinytheme("united"), # Apply a theme for better aesthetics
tabItems(
tabItem(tabName = "calculator",
fluidRow(
column(width = 12, box(
title = "Input Parameters", status = "primary", solidHeader = TRUE,
numericInput("total_players", "Total Players:", value = 22), # Default value
numericInput("pitch_length", "Pitch Length (m):", value = 110),
numericInput("pitch_width", "Pitch Width (m):", value = 65)
))
),
fluidRow(
valueBoxOutput("area_per_player_box"),
valueBoxOutput("pitch_area_box")
)
)
)
)
)
Breakdown of the UI Code
dashboardPage()
: The main function for creating the layout. It has three parts: the header, sidebar, and body.dashboardHeader()
: The top header of the page, where we can add the title or logo.dashboardSidebar()
: The sidebar containing navigation items (such as links to different sections of the app).dashboardBody()
: The main content area where we will place the input fields, buttons, and output boxes.
Input Fields:
numericInput()
: Creates a numeric input field where users can enter values for the number of players and the pitch dimensions.
Output Boxes:
valueBoxOutput()
: Displays calculated values in a visually distinct box (e.g., showing the area per player and pitch area).
Step 4: Building the Server Logic
The server function defines the behavior of the app.
It specifies how the app responds to user inputs and updates the output dynamically.
4.1 Server Logic for Calculating Results
The server code listens for changes in the input fields and updates the outputs accordingly.
server <- function(input, output) {
# Create a reactive expression that listens to changes in inputs
results <- reactive({
# Check which function to use based on user selection
if (input$function_choice == "calculate_pitch_area_per_player") {
calculate_pitch_area_per_player(input$total_players, input$pitch_length, input$pitch_width)
} else if (input$function_choice == "pitch_dimensions") {
pitch_dimensions(input$area_per_player, input$num_players)
} else {
NULL # Return NULL if no valid choice
}
})
# Render output values
output$area_per_player_box <- renderValueBox({
res <- results() # Get the calculated results
valueBox(
value = res$area_per_player, # Display the area per player
subtitle = "Area per Player (m²/player)",
icon = icon("running"),
color = "purple"
)
})
output$pitch_area_box <- renderValueBox({
res <- results() # Get the calculated results
valueBox(
value = res$pitch_area, # Display the total pitch area
subtitle = "Pitch Area (m²)",
icon = icon("street-view"),
color = "purple"
)
})
}
Breakdown of Server Logic
reactive()
: This function creates a reactive expression that updates automatically whenever its inputs change (e.g., when the number of players or pitch dimensions are modified).renderValueBox()
: These functions display the results of the calculations in value boxes.
When the input values change, the app automatically re-renders the outputs based on the new calculations.
Step 5: Running the Shiny App
Now that we've defined both the UI and server components, we can run the app using the shinyApp()
function:
shinyApp(ui = ui, server = server)
Running this function will launch the Shiny app in our default web browser.
We'll be able to interact with it by entering different values for the number of players and pitch dimensions.
Step 6: Enhancing the App and Adding Research Context
Incorporating research context is essential to providing users with a deeper understanding of the calculations and their practical implications.
As mentioned earlier, the Area per Player (ApP) is critical for replicating match demands in small-sided games.
You can add text, references, and explanations to your app to explain the underlying research.
For instance, the following HTML code can be added to our app's UI to provide users with a link to the study by Andrea Riboli et al. (2020).
HTML(
"<p>For more information on the concept of Area per Player, check out the research by Riboli et al. (2020):
<a href='https://doi.org/10.1371/journal.pone.0229194'>Area per Player in Small-Sided Games</a></p>"
)
This provides context to users and helps them understand why the calculations are important for their training purposes.
Sign Up below for the full r script for the shiny app!!
Practical Applications of the App
The ability to adjust Area per Player and calculate the corresponding pitch dimensions is invaluable for designing training sessions.
For example:
Adjusting Intensity: Coaches can use the app to design small-sided games with varying intensities by reducing the ApP to increase the acceleration and deceleration demands of the game (e.g., more pressing) or increasing the ApP to focus on high-speed running demands (e.g., more sprinting).
Player Load Management: The app can help coaches manage player load by adjusting the pitch size to either challenge players or give them more recovery time during training (e.g., more players in a smaller area).
Simulation of Match Conditions: By adjusting the pitch size based on the number of players, coaches can replicate the match environment (e.g., 7v7 on a smaller pitch to simulate match intensity and tactical conditions based on Andrea's recommendations).
Conclusion
In this tutorial, we've learned how to replicate an R Shiny app for calculating the Area per Player and Pitch Dimensions in small-sided games.
We’ve broken down each step of the app-building process, from setting up our R environment to defining the core functions and building an interactive UI.
By integrating the findings of Andrea Riboli et al. (2020) on the physiological implications of small-sided games, this app can help coaches, sport scientists, and practitioners design training sessions that replicate the match demands for soccer players.
Whether we're working with elite athletes or beginners, understanding how to adjust pitch size and player numbers can be an invaluable tool for optimizing training and improving player performance.
Feel free to experiment with the app, adjust its features, or expand it to include additional metrics and calculations.
With R Shiny, the possibilities are limitless for building dynamic and interactive tools in sport science!
References
Riboli, A., Coratella, G., Rampichini, S., Cé, E., and Esposito, F.
(2020) ‘Area per player in small-sided games to replicate the external load and estimated physiological match demands in elite soccer players’, PLOS ONE, 15(9), e0229194, available: https://doi.org/10.1371/journal.pone.0229194.
Full Code
Here is the full code for the Small-Sided Games (SSG) Pitch Dimensions Calculator App:
library(shiny)
library(shinydashboard)
library(shinythemes)
if (!require("devtools")) install.packages("devtools")
devtools::install_github("jogall/soccermatics")
library(soccermatics)
library(ggplot2)
# Define the functions
calculate_pitch_area_per_player <- function(total_players, pitch_length, pitch_width) {
# Input validation: Check for numeric inputs and positive values
if (!is.numeric(total_players) ||
!is.numeric(pitch_length) || !is.numeric(pitch_width)) {
stop("All inputs must be numeric values.")
}
if (total_players <= 0 || pitch_length <= 0 || pitch_width <= 0) {
stop("All inputs must be positive values.")
}
# Calculate pitch area
pitch_area <- pitch_length * pitch_width
# Calculate area per player
area_per_player <- pitch_area / total_players
# Return the results as a list
return(list(
area_per_player = round(area_per_player, 0),
pitch_area = round(pitch_area, 0)
))
}
pitch_dimensions <- function(area_per_player, num_players) {
width_length_ratio = 0.647
# Input validation: Robust error handling to ensure valid inputs.
if (!is.numeric(area_per_player) || area_per_player <= 0) {
stop("area_per_player must be a positive numeric value.")
}
if (!is.numeric(num_players) ||
num_players <= 0 || num_players != round(num_players)) {
stop("num_players must be a positive integer value.")
}
# Calculate the total pitch area
pitch_area_m2 <- area_per_player * num_players
# Calculate the pitch length
pitch_length_m <- sqrt(pitch_area_m2 / width_length_ratio)
# Calculate the pitch width
pitch_width_m <- pitch_area_m2 / pitch_length_m
# Return the results as a list
return(list(
pitch_length_m = round(pitch_length_m, 0),
pitch_width_m = round(pitch_width_m, 0),
pitch_area_m2 = round(pitch_area_m2, 0)
))
}
plot_soccer_pitch <- function(length, width, area) {
ggplot() +
# Pitch Static Outline
geom_rect(aes(xmin = 0, xmax = 108, ymin = 0, ymax = 70),
fill = "forestgreen", color = "black", linetype = 1) +
# Centre Line
geom_segment(aes(x = 108 / 2, y = 0, xend = 108 / 2, yend = 70), color = "white") +
# Penalty Boxes
geom_rect(aes(xmin = 0, xmax = 108 * 0.16, ymin = 70 * 0.16, ymax = 70 * 0.84), color = "white", fill = NA) +
geom_rect(aes(xmin = 108 * 0.84, xmax = 108, ymin = 70 * 0.16, ymax = 70 * 0.84), color = "white", fill = NA) +
# Goal Areas
geom_rect(aes(xmin = 108 * 0, xmax = 108 * 0.05, ymin = 70 * 0.38, ymax = 70 * 0.62), color = "white", fill = NA) +
geom_rect(aes(xmin = 108 * 0.95, xmax = 108 , ymin = 70 * 0.38, ymax = 70 * 0.62), color = "white", fill = NA) +
# Pitch Dynamic Outline (Centered)
geom_rect(aes(xmin = (108 - length) / 2, xmax = (108 + length) / 2,
ymin = (70 - width) / 2, ymax = (70 + width) / 2),
fill = "yellow", alpha = 0.25, color = "yellow", linetype = 2) +
# Length Arrow and Text (Aligned to left of dynamic outline)
geom_segment(aes(x = (108 - length) / 2, y = (70 + width) / 2 + 2,
xend = (108 + length) / 2, yend = (70 + width) / 2 + 2),
arrow = arrow(length = unit(0.1, "inches")), color = "black") +
annotate("text", x = 108 / 2, y = (70 + width) / 2 + 3.5,
label = paste("Length:", length, "m"), size = 4, color = "black", hjust = 0.5)+
# Width Arrow and Text (Aligned to left of dynamic outline)
geom_segment(aes(x = (108 - length) / 2 - 2, y = (70 - width) / 2,
xend = (108 - length) / 2 - 2, yend = (70 + width) / 2),
arrow = arrow(length = unit(0.1, "inches")), color = "black") +
annotate("text", x = (108 - length) / 2 - 3.5, y = 70 / 2,
label = paste("Width:", width, "m"), size = 4, color = "black", vjust = 0.5, angle = 90) +
# Pitch Area Text (Highlighted in the center of dynamic outline)
annotate("text", x = 108 / 2, y = 70 / 2,
label = paste("Pitch Area:", area, "m²"), size = 5, color = "black", fontface = "bold", hjust = 0.5) +
coord_fixed(ratio = 1) + # Ensure correct aspect ratio
theme_minimal() +
theme(
panel.background = element_rect(fill = "white", color = NA),
plot.background = element_rect(fill = "white", color = NA),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
axis.text = element_blank(),
axis.title = element_blank(),
axis.ticks = element_blank()
)
}
# UI definition
ui <- dashboardPage(
dashboardHeader(
title = tags$div(
style = "display: flex; align-items: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;",
tags$img(
src = "Logo_Use.png",
height = "50px",
style = "margin-right: 10px;"
),
tags$span("Small Sided Games Pitch Dimensions Calculator", style = "font-size: 18px; color: white;")
),
titleWidth = 600 # Adjust this value to make sure the title fits
),
dashboardSidebar(
tags$style(
HTML(
"
/* Ensure text in the sidebar wraps and does not overflow */
.sidebar .menu-item-custom {
white-space: normal !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
font-size: 14px; /* Adjust font size if necessary */
padding: 10px;
}
"
)
),
sidebarMenu(
menuItem(
"Calculator",
tabName = "calculator",
icon = icon("calculator")
),
menuItem("SSG Mind Map Download", tabName = "images", icon = icon("image")),
# New menu item for images
menuItem(
"How to Guide",
icon = icon("info-circle"),
startExpanded = TRUE,
tags$div(
class = "menu-item-custom",
tags$h4("How to Use the App:"),
tags$ol(
tags$li("Select a function from the dropdown menu:"),
tags$ul(
tags$li(
"Calculate Area per Player: Use this to calculate the area available per player based on pitch dimensions and player count."
),
tags$li(
"Calculate Pitch Dimensions: Use this to determine the pitch dimensions based on desired area per player and total number of players."
)
),
tags$li("Enter the required input values in the fields provided."),
tags$li("View the results displayed in the value boxes."),
tags$li("Use the references (Riboli et al., 2020) and article link at the bottom for more context."),
tags$li("Match Demands Replication: The area per player in official competition is approximately equivalent to ~340m^2 (Riboli et al., 2020)."),
tags$li("Intensive Focus: With GK's = minimum ~121 ± 47 m^2; Without GK's = minimum ~77 ± 26 m^2 (Riboli et al., 2020)."),
tags$li("Extensive Focus: With GK's = minimum ~350 ± 129 m^2; Without GK's = minimum ~143 ± 34 m^2 (Riboli et al., 2020).")
)
)
)
)
),
dashboardBody(
theme = shinytheme("united"),
# a stylish and readable theme
tags$head(tags$style(
HTML(
"
/* Purple Value Box */
.box.value-box.bg-aqua {
background-color: #702963 !important;
color: white !important;
}
.value-box-title {
color: white !important;
}
.value-box-content {
color: white !important;
}
/* Header */
.skin-#702963 .main-header .logo {
height: 50px;
}
.skin-#702963 .main-header .navbar {
height: 50px;
}
/* General */
body {
background-color: #EDE3FF; /* Light Purple Background */
}
"
)
)),
# Added styling
tags$style(
HTML(
"
/* Adjust main body padding to accommodate horizontal sidebar */
.main-content {
padding-top: 50px; /* Adjust this value as needed based on header/sidebar height */
}
p {
font-size: 14px;
}
"
)
),
# Value boxes to display results
tabItems(
tabItem(
tabName = "calculator",
fluidRow(column(
width = 12,
box(
width = NULL,
title = "Input Parameters",
status = "primary",
solidHeader = TRUE,
collapsible = TRUE,
fluidRow(
column(
width = 6,
selectInput(
"function_choice",
"Choose a Function:",
choices = c(
"Calculate Area per Player" = "calculate_pitch_area_per_player",
"Calculate Pitch Dimensions" = "pitch_dimensions"
)
)
),
column(
width = 6,
conditionalPanel(
condition = "input.function_choice == 'calculate_pitch_area_per_player'",
numericInput("total_players1", "Total Players:", value = 22),
uiOutput("total_players1_error_msg"),
numericInput("pitch_length", "Pitch Length (m):", value = 108),
uiOutput("pitch_length_error_msg"),
numericInput("pitch_width", "Pitch Width (m):", value = 70),
uiOutput("pitch_width_error_msg")
),
conditionalPanel(
condition = "input.function_choice == 'pitch_dimensions'",
numericInput("area_per_player", "Area per Player (m²):", value = 340),
uiOutput("area_per_player_error_msg"),
numericInput("num_players", "Number of Players:", value = 22),
uiOutput("num_players_error_msg")
)
)
)
)
)),
fluidRow(
conditionalPanel(
condition = "input.function_choice == 'calculate_pitch_area_per_player'",
valueBoxOutput("area_per_player_box"),
valueBoxOutput("pitch_area_box")
),
conditionalPanel(
condition = "input.function_choice == 'pitch_dimensions'",
valueBoxOutput("pitch_length_m_box"),
valueBoxOutput("pitch_width_m_box"),
valueBoxOutput("pitch_area_m2_box")
)
),
fluidRow(
box(
width = 12,
title = "Pitch Plot",
status = "primary",
solidHeader = TRUE,
collapsible = TRUE,
plotOutput("pitch_plot")
)
),
fluidRow(
box(
width = 12,
status = "info",
solidHeader = FALSE,
collapsible = TRUE,
HTML(
"<p>Area per Player can be defined as the total pitch area divided by the total number of players within the given area (Hill-Haas et al., 2011). This allows practitioners to calculate the theoretical space each player has available around them at a given instance on the field of play. In official competition the area per player is approximately equivalent to ~340m^2 (Riboli et al., 2020), however it should be noted that this is dictated by the size of the field of play. In essence, pitch size and player numbers can be controlled independently of each other to create a high number of SSG formats thus, increasing pitch size or reducing the number of players within a given area increases multiple training load metrics such as total distance, total high-intensity distance, and total sprinting distance (Lacome et al., 2018b; Lacome et al., 2018a). Conversely, decreasing pitch size or increasing the number of players within the area decreases the space available to cover high speed distances, however, increases the acceleration, deceleration, and technical demands (Lacome et al., 2018b; Gaudino, Alberti and Iaia, 2014). Therefore, the manipulation of ApP through these means will bring about different physical and physiological responses (Castagna et al., 2019; Riboli, Esposito and Coratella, 2022; Riboli et al., 2020), while also affecting the technical and tactical behaviours of the players (Kelly and Drust, 2009; Olthof, Frencken and Lemmink, 2018).Clarifying the ApP that is needed to reproduce specific match day requirements may help practitioners and the coaching staff to properly plan SSGs for specific performance objectives (Lacome et al., 2018b). For more information, you can read my article: <a href='https://lorcanmason.substack.com/p/building-a-small-sided-games-ssg-pitch-dimensions-calculator-app'>https://lorcanmason.substack.com/p/building-a-small-sided-games-ssg-pitch-dimensions-calculator-app/</a></p>"
)
)
),
fluidRow(column(
width = 12,
HTML(
"<p style='font-size:14px;'>Reference: Riboli, A., Coratella, G., Rampichini, S., Cé, E., & Esposito, F. (2020). Area per player in small-sided games to replicate the external load and estimated physiological match demands in elite soccer players. PloS one, 15(9), e0229194. <a href='https://doi.org/10.1371/journal.pone.0229194'>https://doi.org/10.1371/journal.pone.0229194</a>.</p>"
)
))
),
tabItem(tabName = "images", fluidRow(column(
width = 12,
box(
width = NULL,
title = "Image References",
status = "primary",
solidHeader = TRUE,
collapsible = TRUE,
uiOutput("image_gallery")
)
)))
)
),
skin = "green"
)
# Server logic
server <- function(input, output) {
# Reactive values to store input errors
input_errors <- reactiveValues(
total_players1_error = NULL,
pitch_length_error = NULL,
pitch_width_error = NULL,
area_per_player_error = NULL,
num_players_error = NULL
)
# Reactive expression to call the selected function and handle errors
results <- reactive({
# Reset input errors
input_errors$total_players1_error <- NULL
input_errors$pitch_length_error <- NULL
input_errors$pitch_width_error <- NULL
input_errors$area_per_player_error <- NULL
input_errors$num_players_error <- NULL
if (input$function_choice == "calculate_pitch_area_per_player") {
# Validation for calculate_pitch_area_per_player inputs
if (!is.numeric(input$total_players1) ||
input$total_players1 <= 0) {
input_errors$total_players1_error <- "Please enter a valid number of players."
return(NULL)
}
if (!is.numeric(input$pitch_length) ||
input$pitch_length <= 0) {
input_errors$pitch_length_error <- "Please enter a valid pitch length."
return(NULL)
}
if (!is.numeric(input$pitch_width) ||
input$pitch_width <= 0) {
input_errors$pitch_width_error <- "Please enter a valid pitch width."
return(NULL)
}
calculate_pitch_area_per_player(input$total_players1,
input$pitch_length,
input$pitch_width)
} else if (input$function_choice == "pitch_dimensions") {
# Validation for pitch_dimensions inputs
if (!is.numeric(input$area_per_player) ||
input$area_per_player <= 0) {
input_errors$area_per_player_error <- "Please enter a valid area per player."
return(NULL)
}
if (!is.numeric(input$num_players) ||
input$num_players <= 0 ||
input$num_players != round(input$num_players)) {
input_errors$num_players_error <- "Please enter a valid number of players."
return(NULL)
}
pitch_dimensions(input$area_per_player, input$num_players)
} else {
NULL # No function selected
}
})
# Dynamically render input validation error messages
output$total_players1_error_msg <- renderUI({
if (!is.null(input_errors$total_players1_error)) {
tags$div(input_errors$total_players1_error, style = "color:red;")
}
})
output$pitch_length_error_msg <- renderUI({
if (!is.null(input_errors$pitch_length_error)) {
tags$div(input_errors$pitch_length_error, style = "color:red;")
}
})
output$pitch_width_error_msg <- renderUI({
if (!is.null(input_errors$pitch_width_error)) {
tags$div(input_errors$pitch_width_error, style = "color:red;")
}
})
output$area_per_player_error_msg <- renderUI({
if (!is.null(input_errors$area_per_player_error)) {
tags$div(input_errors$area_per_player_error, style = "color:red;")
}
})
output$num_players_error_msg <- renderUI({
if (!is.null(input_errors$num_players_error)) {
tags$div(input_errors$num_players_error, style = "color:red;")
}
})
# Output value boxes (dynamically updated based on function choice and results)
output$area_per_player_box <- renderValueBox({
# Check if area_per_player_box should be displayed
if (input$function_choice == "calculate_pitch_area_per_player") {
# Get reactive results
res <- results()
# Check if results has data or error
if (!is.null(res)) {
valueBox(
value = res$area_per_player,
subtitle = "Area per Player (m²/player)",
icon = icon("running", lib = "font-awesome"),
# Soccer-specific icon
color = "purple"
)
} else {
valueBox(
value = "Error",
subtitle = "Invalid Input",
icon = icon("exclamation-triangle"),
color = "red"
)
}
} else {
NULL
}
})
output$pitch_area_box <- renderValueBox({
if (input$function_choice == "calculate_pitch_area_per_player") {
res <- results()
if (!is.null(res)) {
valueBox(
value = res$pitch_area,
subtitle = "Pitch Area (m²)",
icon = icon("street-view", lib = "font-awesome"),
# Soccer-specific icon
color = "purple"
)
} else {
NULL
}
} else {
NULL
}
})
output$pitch_length_m_box <- renderValueBox({
if (input$function_choice == "pitch_dimensions") {
res <- results()
if (!is.null(res)) {
valueBox(
value = res$pitch_length_m,
subtitle = "Pitch Length (m)",
icon = icon("ruler-vertical"),
color = "purple"
)
} else {
valueBox(
value = "Error",
subtitle = "Invalid Input",
icon = icon("exclamation-triangle"),
color = "red"
)
}
} else {
NULL
}
})
output$pitch_width_m_box <- renderValueBox({
if (input$function_choice == "pitch_dimensions") {
res <- results()
if (!is.null(res)) {
valueBox(
value = res$pitch_width_m,
subtitle = "Pitch Width (m)",
icon = icon("ruler-horizontal"),
color = "purple"
)
} else {
valueBox(
value = "Error",
subtitle = "Invalid Input",
icon = icon("exclamation-triangle"),
color = "red"
)
}
} else {
NULL
}
})
output$pitch_area_m2_box <- renderValueBox({
if (input$function_choice == "pitch_dimensions") {
res <- results()
if (!is.null(res)) {
valueBox(
value = res$pitch_area_m2,
subtitle = "Pitch Area (m²)",
icon = icon("expand"),
color = "purple"
)
} else {
valueBox(
value = "Error",
subtitle = "Invalid Input",
icon = icon("exclamation-triangle"),
color = "red"
)
}
} else {
NULL
}
})
# Image Handling Logic
output$image_gallery <- renderUI({
# List of specific image names you want to include
desired_images <- c(
"Small-Sided-Games-Definitions-2.png",
"Area-per-Player-with-Goalkeepers.png",
"Area-per-Player-without-Goalkeepers.png",
"Practical-Conditioning.png"
) # Replace with your image file names
# List all files in 'www' folder
image_files <- list.files(
path = "www",
pattern = "\\.(png|jpg|jpeg)$",
ignore.case = TRUE,
full.names = TRUE
)
# Extract just file names
image_names <- basename(image_files)
# Filter image_files based on desired_images
image_files <- image_files[image_names %in% desired_images]
if (length(image_files) == 0) {
return(HTML("No specified images found in the 'www' folder."))
}
image_tags <- lapply(image_files, function(img_path) {
img_name <- basename(img_path)
img_tag <- tags$div(
style = "display: inline-block; margin: 10px;",
tags$img(
src = img_name,
alt = img_name,
style = "max-width: 100%; height: auto;"
),
tags$br(),
downloadButton(
outputId = paste0("download_", gsub("[^a-zA-Z0-9]", "_", img_name)),
label = paste0("Download ", img_name)
)
)
# Dynamically render the download handlers
local({
img_name_local <- img_name
output[[paste0("download_",
gsub("[^a-zA-Z0-9]", "_", img_name_local))]] <- downloadHandler(
filename = function() {
img_name_local
},
content = function(file) {
file.copy(file.path("www", img_name_local), file)
}
)
})
img_tag
})
do.call(tagList, image_tags)
})
# Reactive expression for generating the soccer pitch plot
pitch_plot_data <- reactive({
res <- results()
if (!is.null(res) && is.null(res$error)) {
if (input$function_choice == "calculate_pitch_area_per_player") {
pitch_length_m = input$pitch_length
pitch_width_m = input$pitch_width
pitch_area_m = res$pitch_area
} else if (input$function_choice == "pitch_dimensions"){
pitch_length_m <- res$pitch_length_m
pitch_width_m <- res$pitch_width_m
pitch_area_m <- res$pitch_area_m2
} else {
return(NULL)
}
if (!is.null(pitch_length_m) && !is.null(pitch_width_m) && !is.null(pitch_area_m)){
plot_soccer_pitch(length = pitch_length_m, width = pitch_width_m, area = pitch_area_m)
} else {
return(NULL)
}
} else {
NULL # Return NULL if calculation failed or no function was selected
}
})
# Render the pitch plot
output$pitch_plot <- renderPlot({
pitch_plot_data()
})
}
# Run the app
shinyApp(ui = ui, server = server)