Navigation Menu

Skip to content

Commit

Permalink
Updated docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Joe Gallagher committed Jun 6, 2018
1 parent c3cde09 commit 4a43dcd
Show file tree
Hide file tree
Showing 19 changed files with 365 additions and 216 deletions.
7 changes: 4 additions & 3 deletions DESCRIPTION
@@ -1,5 +1,5 @@
Package: soccermatics
Version: 0.8.4
Version: 0.9.0
Authors@R: person("Joe", "Gallagher", email = "joedgallagher@gmail.com", role = c("aut", "cre"))
Title: Visualise spatial data from soccer matches
Description: Provides tools to visualise x,y-coordinates of soccer players in the manner presented in David Sumpter's eponymous book. Uses ggplot to draw soccer pitch and overplot player trajectories, average player positions, heatmaps of player position, flow fields to show binned player movement or passing, and more.
Expand All @@ -12,12 +12,13 @@ Collate:
'soccerPitchFG.R'
'soccerPitchBG.R'
'soccerDirection.R'
'soccerFlipHoriz.R'
'soccerHeatmap.R'
'soccerSpokes.R'
'soccerFlow.R'
'soccerPath.R'
'soccerPositions.R'
'soccerNormXY.R'
'soccerPositionMap.R'
'tromso.R'
'tromso_extra.R'
'utils.R'
RoxygenNote: 6.0.1
4 changes: 3 additions & 1 deletion NAMESPACE
@@ -1,12 +1,14 @@
# Generated by roxygen2: do not edit by hand

export(soccerDirection)
export(soccerFlipHoriz)
export(soccerFlow)
export(soccerHeatmap)
export(soccerNormXY)
export(soccerPath)
export(soccerPitchBG)
export(soccerPitchFG)
export(soccerPositions)
export(soccerPositionMap)
export(soccerSpokes)
import(dplyr)
import(ggplot2)
Expand Down
27 changes: 27 additions & 0 deletions R/soccerFlipHoriz.R
@@ -0,0 +1,27 @@
#' @import ggplot2
#' @importFrom ggforce geom_arc geom_circle
NULL
#' Flips x,y-coordinates horizontally in one half to account for changing sides at half-time
#'
#' @description Normalises direction of attack in both halves of both teams by
#' flipping x,y-coordinates horizontally in either the first or second half;
#' i.e. teams attack in the same direction all game despite changing sides at
#' half-time.
#'
#' @param dat = dataframe containing unnormalised x,y-coordinates named `x` and `y`
#' @param periodVar = name of variable containing period labels
#' @param periodToFlip = which period to flip
#' @param pitchLength,pitchWidth = length, width of pitch in metres
#' @return a dataframe
#' @examples
#' # to flip coordinates in 2nd half of a dataframe with 1st/2nd half identity labelled by variable named `period`
#' soccerFlipHoriz(df, "period", 2, 105, 68)
#'
#' @export
soccerFlipHoriz <- function(dat, periodVar = "period", periodToFlip = 1, pitchLength = 105, pitchWidth = 68) {

dat$x <- ifelse(dat[,periodVar] == periodToFlip, pitchLength - dat$x, dat$x)
dat$y <- ifelse(dat[,periodVar] == periodToFlip, pitchWidth - dat$y, dat$y)

return(dat)
}
67 changes: 67 additions & 0 deletions R/soccerNormXY.R
@@ -0,0 +1,67 @@
#' @include soccerPath.R
#' @import ggplot2
#' @importFrom ggforce geom_arc geom_circle
NULL
#' Normalises x,y-coordinates to metres units for use with soccermatics functions
#'
#' @description Normalises x,y-coordinates from between any arbitrary bounds to metre units bounded by [0 < x < pitchLength, 0 < y < pitchWidth]
#'
#' @param dat = dataframe containing unnormalised x,y-coordinates named `x` and `y`
#' @param xMin,xMax,yMin,yMax = range of x,y-coordinates possible in the raw dataset
#' @param pitchLength,pitchWidth = length, width of pitch in metres
#' @return a dataframe
#' @examples
#' # Three examples with true pitch dimesions (in metres):
#' lengthPitch <- 101
#' widthPitch <- 68
#'
#' # Example 1. Opta-style -------------------------------------------------------
#' # limits = [0 < x < 100, 0 < y < 100]
#' # centre of pitch = [50,50]
#'
#' df <- data.frame(t = 1:12,
#' x = c(50,55,61,66,62,58,51,44,45,42,41,32),
#' y = c(50,48,47,40,42,45,49,51,59,75,88,100))
#'
#' df <- soccerNormXY(df, 0, 100, 0, 100, lengthPitch, widthPitch)
#'
#' soccerPath(df, lengthPitch = lengthPitch, widthPitch = widthPitch)
#'
#'
#' # Example 2. StrataBet-style --------------------------------------------------
#' # limits = [0 < x < 420, -136 < y < 136]
#' # centre of pitch = [210,0]
#'
#' df <- data.frame(t = 1:12,
#' x = c(210,222,201,192,178,170,143,122,104,91,75,60),
#' y = c(0,-5,-20,-12,-8,-2,4,8,13,20,30,45))
#'
#' df <- soccerNormXY(df, 0, 420, -136, 136, lengthPitch, widthPitch)
#'
#' soccerPath(df, lengthPitch = lengthPitch, widthPitch = widthPitch)
#'
#'
#' # Example 3. Other ------------------------------------------------------------
#' # limits = [-5250 < x < 5250, -3400 < y < 3400]
#' # centre of pitch = [0,0]
#'
#' xMin <- -5250
#' xMax <- 5250
#' yMin <- -3400
#' yMax <- 3400
#'
#' df <- data.frame(x = c(0,-452,-982,-1099,-1586,-2088,-2422,-2999,-3200,-3857),
#' y = c(0,150,300,550,820,915,750,620,400,264))
#'
#' df <- soccerNormXY(df, -5250, 5250, -3400, 3400, lengthPitch, widthPitch)
#'
#' soccerPath(df, lengthPitch = lengthPitch, widthPitch = widthPitch)
#'
#' @export
soccerNormXY <- function(dat, xMin, xMax, yMin, yMax, pitchLength, pitchWidth) {

dat$x <- (dat$x - xMin) / diff(c(xMin,xMax)) * pitchLength
dat$y <- (dat$y - yMin) / diff(c(yMin,yMax)) * pitchWidth

return(dat)
}
42 changes: 0 additions & 42 deletions R/soccerPassmap.R

This file was deleted.

26 changes: 13 additions & 13 deletions R/soccerPath.R
Expand Up @@ -6,12 +6,12 @@ NULL
#'
#' @description Draws a path connecting consecutive x,y-coordinates of a player on a soccer pitch.
#'
#' @param df dataframe containing x,y-coordinates of player position in columns named \code{'x'} and \code{'y'}.
#' @param id_var character, the name of the column containing player identity. Only required if \code{'df'} contains multiple players.
#' @param lengthPitch,widthPitch numeric, length and width of pitch in metres.
#' @param grass if TRUE, draws pitch background in green and lines in white. If FALSE, draws pitch background in white and lines in black.
#' @param col colour of path if no \code{'id_var'} is provided. If an \code{'id_var'} is present, colours from ColorBrewer's 'Paired' palette are used.
#' @param lwd thickness of path
#' @param dat dataframe containing x,y-coordinates of player position in columns named \code{'x'} and \code{'y'}
#' @param id_var character, the name of the column containing player identity. Only required if \code{'dat'} contains multiple players
#' @param lengthPitch,widthPitch length and width of pitch in metres
#' @param grass if TRUE, draws a more realistic looking pitch
#' @param col colour of path if no \code{'id_var'} is provided. If an \code{'id_var'} is present, colours from ColorBrewer's 'Paired' palette are used
#' @param lwd player path thickness
#' @return a ggplot object
#' @examples
#' data(tromso)
Expand All @@ -25,26 +25,26 @@ NULL
#' soccerPath("id")
#'
#' @export
soccerPath <- function(df, id_var = NULL, lengthPitch = 105, widthPitch = 68, grass = FALSE, col = "black", lwd = 1, legend = TRUE, plot = NULL) {
soccerPath <- function(dat, id_var = NULL, lengthPitch = 105, widthPitch = 68, col = "black", fillPitch = "white", colPitch = "grey60", grass = FALSE, lwd = 1, legend = TRUE, plot = NULL) {

if(is.null(id_var)) {
if(missing(plot)) {
# one player
p <- soccerPitchBG(lengthPitch = 105, widthPitch = 68, grass = grass) +
geom_path(data = df, aes(x, y), colour = col, lwd = lwd)
p <- soccerPitchBG(lengthPitch = lengthPitch, widthPitch = widthPitch, fillPitch = fillPitch, colPitch = colPitch, grass = grass) +
geom_path(data = dat, aes(x, y), col = col, lwd = lwd)
} else {
p <- plot +
geom_path(data = df, aes(x, y), colour = col, lwd = lwd)
geom_path(data = dat, aes(x, y), col = col, lwd = lwd)
}
} else {
# multiple players
if(missing(plot)) {
p <- soccerPitchBG(lengthPitch = 105, widthPitch = 68, grass = grass) +
geom_path(data = df, aes_string("x", "y", group = id_var, colour = id_var), lwd = lwd) +
p <- soccerPitchBG(lengthPitch = lengthPitch, widthPitch = widthPitch, fillPitch = fillPitch, colPitch = colPitch, grass = grass) +
geom_path(data = dat, aes_string("x", "y", group = id_var, colour = id_var), lwd = lwd) +
scale_colour_brewer(type = "seq", palette = "Paired", labels = 1:12)
} else {
p <- plot +
geom_path(data = df, aes_string("x", "y", group = id_var, colour = id_var), lwd = lwd) +
geom_path(data = dat, aes_string("x", "y", group = id_var, colour = id_var), lwd = lwd) +
scale_colour_brewer(type = "seq", palette = "Paired", labels = 1:12)
}

Expand Down
63 changes: 36 additions & 27 deletions R/soccerPitchBG.R
Expand Up @@ -6,56 +6,65 @@ NULL
#'
#' @description Draws a soccer pitch as a ggplot object for the purpose of adding layers such as player positions, player trajectories, etc..
#'
#' @param lengthPitch,widthPitch numeric, length and width of pitch in metres.
#' @param grass if TRUE, draws pitch background in green and lines in white. If FALSE, draws pitch background in white and lines in black.
#' @param line_col colour of pitch lines
#' @param lengthPitch,widthPitch length and width of pitch in metres
#' @param fillPitch pitch fill colour
#' @param colPitch pitch line colour
#' @param grass if TRUE, draws a more realistic looking pitch
#' @param lwd pitch line width
#' @param border size of border drawn around pitch perimeter (t,r,b,l)
#' @return a ggplot object
#' @examples
#' # get x,y-coords of player #8 during first 10 minutes
#' data(tromso)
#' dd <- subset(tromso, id == 9)[1:1200,]
#' # draw player path on pitch
#' soccerPitchBG(lengthPitch = 105, widthPitch = 68, grass = TRUE) +
#' geom_path(data = dd, aes(x, y), lwd = 2)
#' geom_path(data = dd, aes(x, y), lwd = 1.2)
#'
#' @seealso \code{\link{soccerPitchFG}} for drawing a soccer pitch as foreground over an existing ggplot object
#' @export
soccerPitchBG <- function(lengthPitch = 105, widthPitch = 68, grass = FALSE, line_col = "black") {
# set draw colours
soccerPitchBG <- function(lengthPitch = 105, widthPitch = 68, fillPitch = "white", colPitch = "grey60", grass = FALSE, lwd = 1, border = c(4, 4, 4, 4)) {

if(grass) {
bg_col = "#008000"
line_col = "white"
fill1 <- "#008000"
fill2 <- "#328422"
colPitch <- "grey85"
} else {
bg_col = "white"
fill1 <- fillPitch
fill2 <- fillPitch
}

#draw
lines <- (lengthPitch + border[2] + border[1]) / 13
boxes <- data.frame(start = lines * 0:12 - border[1], end = lines * 1:13 - border[2])[seq(2, 12, 2),]

ggplot() +
# pitch
geom_rect(aes(xmin = -4, xmax = lengthPitch + 4, ymin = -4, ymax = widthPitch + 4), fill = bg_col) +
# outer lines
geom_rect(aes(xmin = 0, xmax = lengthPitch, ymin = 0, ymax = widthPitch), fill = bg_col, col = line_col, lwd = 1.2) +
# background
geom_rect(aes(xmin = -border[1], xmax = lengthPitch + border[2], ymin = -border[3], ymax = widthPitch + border[4]), fill = fill1) +
# mowed pitch lines
geom_rect(data = boxes, aes(xmin = start, xmax = end, ymin = -border[3], ymax = widthPitch + border[4]), fill = fill2) +
# perimeter line
geom_rect(aes(xmin = 0, xmax = lengthPitch, ymin = 0, ymax = widthPitch), fill = NA, col = colPitch, lwd = lwd) +
# centre circle
geom_circle(aes(x0 = lengthPitch / 2, y0 = widthPitch / 2, r = 9.15), fill = bg_col, col = line_col, lwd = 1.2) +
geom_circle(aes(x0 = lengthPitch / 2, y0 = widthPitch / 2, r = 9.15), col = colPitch, lwd = lwd) +
# kick off spot
geom_circle(aes(x0 = lengthPitch / 2, y0 = widthPitch / 2, r = 0.5), fill = line_col, col = line_col, lwd = 1.2) +
geom_circle(aes(x0 = lengthPitch / 2, y0 = widthPitch / 2, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) +
# halfway line
geom_segment(aes(x = lengthPitch / 2, y = 0, xend = lengthPitch / 2, yend = widthPitch), col = line_col, lwd = 1.2) +
geom_segment(aes(x = lengthPitch / 2, y = 0, xend = lengthPitch / 2, yend = widthPitch), col = colPitch, lwd = lwd) +
# penalty arcs
geom_arc(aes(x0= 11, y0 = widthPitch / 2, r = 9.15, start = 0.65, end = 2.49), col= line_col, lwd = 1.2) +
geom_arc(aes(x0 = lengthPitch - 11, y0 = widthPitch / 2, r = 9.15, start = 3.79, end = 5.63), col= line_col, lwd = 1.2) +
geom_arc(aes(x0= 11, y0 = widthPitch / 2, r = 9.15, start = 0.65, end = 2.49), col = colPitch, lwd = lwd) +
geom_arc(aes(x0 = lengthPitch - 11, y0 = widthPitch / 2, r = 9.15, start = 3.79, end = 5.63), col = colPitch, lwd = lwd) +
# penalty areas
geom_rect(aes(xmin = 0, xmax = 16.5, ymin = widthPitch / 2 - (40.3 / 2), ymax = widthPitch / 2 + (40.3 / 2)), fill = bg_col, col = line_col, lwd = 1.2) +
geom_rect(aes(xmin = lengthPitch - 16.5, xmax = lengthPitch, ymin = widthPitch / 2 - (40.3 / 2), ymax = widthPitch / 2 + (40.3 / 2)), fill = bg_col, col = line_col, lwd = 1.2) +
geom_rect(aes(xmin = 0, xmax = 16.5, ymin = widthPitch / 2 - (40.3 / 2), ymax = widthPitch / 2 + (40.3 / 2)), fill = NA, col = colPitch, lwd = lwd) +
geom_rect(aes(xmin = lengthPitch - 16.5, xmax = lengthPitch, ymin = widthPitch / 2 - (40.3 / 2), ymax = widthPitch / 2 + (40.3 / 2)), fill = NA, col = colPitch, lwd = lwd) +
# penalty spots
geom_circle(aes(x0 = 11, y0 = widthPitch / 2, r = 0.5), fill = line_col, col = line_col, lwd = 1.2) +
geom_circle(aes(x0 = lengthPitch - 11, y0 = widthPitch / 2, r = 0.5), fill = line_col, col = line_col, lwd = 1.2) +
geom_circle(aes(x0 = 11, y0 = widthPitch / 2, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) +
geom_circle(aes(x0 = lengthPitch - 11, y0 = widthPitch / 2, r = 0.25), fill = colPitch, col = colPitch, lwd = lwd) +
# six yard boxes
geom_rect(aes(xmin = 0, xmax = 5.5, ymin = (widthPitch / 2) - 9.16, ymax = (widthPitch / 2) + 9.16), fill = bg_col, col = line_col, lwd = 1.2) +
geom_rect(aes(xmin = lengthPitch - 5.5, xmax = lengthPitch, ymin = (widthPitch / 2) - 9.16, ymax = (widthPitch / 2) + 9.16), fill = bg_col, col = line_col, lwd = 1.2) +
geom_rect(aes(xmin = 0, xmax = 5.5, ymin = (widthPitch / 2) - 9.16, ymax = (widthPitch / 2) + 9.16), fill = NA, col = colPitch, lwd = lwd) +
geom_rect(aes(xmin = lengthPitch - 5.5, xmax = lengthPitch, ymin = (widthPitch / 2) - 9.16, ymax = (widthPitch / 2) + 9.16), fill = NA, col = colPitch, lwd = lwd) +
# goals
geom_rect(aes(xmin = -2, xmax = 0, ymin = (widthPitch / 2) - 3.66, ymax = (widthPitch / 2) + 3.66), fill = bg_col, col = line_col, lwd = 1.2) +
geom_rect(aes(xmin = lengthPitch, xmax = lengthPitch + 2, ymin = (widthPitch / 2) - 3.66, ymax = (widthPitch / 2) + 3.66), fill = bg_col, col = line_col, lwd = 1.2) +
geom_rect(aes(xmin = -2, xmax = 0, ymin = (widthPitch / 2) - 3.66, ymax = (widthPitch / 2) + 3.66), fill = NA, col = colPitch, lwd = lwd) +
geom_rect(aes(xmin = lengthPitch, xmax = lengthPitch + 2, ymin = (widthPitch / 2) - 3.66, ymax = (widthPitch / 2) + 3.66), fill = NA, col = colPitch, lwd = lwd) +
coord_fixed() +
xlab("") +
ylab("") +
Expand Down

0 comments on commit 4a43dcd

Please sign in to comment.