Data Visualisation

3. Building a StatsBomb Ready Pitch

There are lots of reasons why we might want to draw a line or circle on our charts. We could look to add an average line, highlight a key data point or even draw a picture. This article will show how to add lines, circles and arcs with the example of a football pitch map.

This example works with Statsbomb pitch dimensions, but you might want to change them according to your data/sport/needs. We will create this plot with the ggplot2 package so make sure it's installed.

Before we start, the plotting will throw up some warnings from R, I am going to cheat and turn these off. Do not get into a habit of this!

    
  options(warn=-1)
    
  

Setting a Theme


Firstly, we need to set a colour theme, I have gone with a adventurous pink scheme which I chose with the help of Adobe Color Tool.


  require(ggplot2)

  # set our colours for our pitch
  grass_colour <- "#775D6A"
  line_colour <- "#F4828C"
  background_colour <- "#775D6A"
  goal_colour <- "#7E3C5D"

Next we want to create a theme for our plots which will help us control how the plot is displayed via ggplot. We turn a lot of the plot feautres off using element_blank() and set others to be inline with our colour scheme.


  theme_blankPitch = function(size=12) {
    theme(
      #axis.line=element_blank(),
      axis.text.x=element_blank(),
      axis.text.y=element_blank(),
      #axis.ticks.y=element_text(size=size),
      axis.ticks.length=unit(0, "lines"),
      #axis.ticks.margin=unit(0, "lines"),
      axis.title.x=element_blank(),
      axis.title.y=element_blank(),
      legend.background=element_rect(fill=background_colour, colour=NA),
      legend.key=element_rect(colour=background_colour,fill=background_colour),
      legend.key.size=unit(1.2, "lines"),
      legend.text=element_text(size=size),
      legend.title=element_text(size=size, face="bold",hjust=0),
      strip.background = element_rect(colour = background_colour, fill = background_colour, size = .5),
      panel.background=element_rect(fill=background_colour,colour=background_colour),
      panel.grid.major=element_blank(),
      panel.grid.minor=element_blank(),
      panel.spacing=element_blank(),
      plot.background=element_blank(),
      plot.margin=unit(c(0, 0, 0, 0), "lines"),
      plot.title=element_text(size=size*1.2),
      strip.text.y=element_text(colour=background_colour,size=size,angle=270),
      strip.text.x=element_text(size=size*1))}

Creating the Dimension Data


Next we define the overall pitch size and store them as variables, these are taken from page 34 of the StatsBomb Data Spec document.


  ymin <- 0 # minimum width
  ymax <- 80 # maximum width
  xmin <- 0 # minimum length
  xmax <- 120 # maximum length

Our next job is to define and calculate the pitch markings we want to plot


  # Defining features along the length
  boxEdgeDef <- 18
  boxEdgeOff <- 102
  halfwayline <- 60
  sixYardDef <- 6
  sixYardOff <- 114
  penSpotDef <- 12
  penSpotOff <- 108

  # Defining features along the width
  boxEdgeLeft <- 18
  boxEdgeRight <- 62
  sixYardLeft <- 30
  sixYardRight <- 50
  goalPostLeft <- 36
  goalPostRight <- 44
  CentreSpot <- 40

  # other dimensions
  centreCirle_d <- 20

  padding = 5

Lastly we create the data for the centre cirle with code we created in the radar tutorial.


  circleFun <- function(center = c(0,0),diameter = 1, npoints = 100){
      r = diameter / 2
      tt <- seq(0,2*pi,length.out = npoints)
      xx <- center[1] + r * cos(tt)
      yy <- center[2] + r * sin(tt)
      return(data.frame(x = xx, y = yy))
    }

    #### create center circle ####
    center_circle <- circleFun(c(halfwayline,CentreSpot),centreCirle_d,npoints = 100)
 

Creating the Layers of the Pitch


ggplot2 adds layers on to of each other, from the bottom upwards. We will create the pitch from the bottom up, adding different elements as we go.

Step 1: The Base Layer
This is an exteremely simple set as we have alraedy defined all the colours and setup our theme. So we create a ggplot() object, define the x and the y limits of the plot and then apply our blankPitch theme, saving this as 'p' for plot.


  ## initiate the plot, set some boundries to the plot
  p <- ggplot() + xlim(c(xmin-padding,xmax+padding)) +
  ylim(c(ymin-padding,ymax+padding)) +
  # add the theme
  theme_blankPitch()

The result, a blank plot that is coloured as we wanted it and is ready for the next elements to be added to it.



Step 2: Add the Pitch Outline
The outline of the pitch is a very simple rectangle, we will simple add it to the 'p' object and then overwrite the 'p' object with the additional rectangle. Each rectangle needs 4 values, the minimum x, the maximum x, the minimum y and the maximum x. We give it no fill with 'NA' and then the lines are coloured as whatever we assigned to line_colour earlier.


  p <- p + geom_rect(aes(xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax), fill = NA, colour = line_colour)

Some progress! We have a rectangle working!



Step 3: Add the 18 Yard Boxes
The 18 yard boxes are also simple rectangles and can be added in the same way.


  p <- p + geom_rect(aes(xmin=xmin, xmax=boxEdgeDef, ymin=boxEdgeLeft, ymax=boxEdgeRight), fill = grass_colour, colour = line_colour) +
  geom_rect(aes(xmin=boxEdgeOff, xmax=xmax, ymin=boxEdgeLeft, ymax=boxEdgeRight), fill = grass_colour, colour = line_colour)

This is taking shape!



Step 4: Add the Half-Way Line
Adding a line to a ggplot2 is just as easy as a rectangle! It takes 4 values of its starting x,y and its ending x,y - super simple.


  p <- p + geom_segment(aes(x = halfwayline, y = ymin, xend = halfwayline, yend = ymax),colour = line_colour)

Nice and easy, some people like to stop here, as plotting data onto a simplfied pitch can help give more focus to the data. But let's continue.



Step 5: Add the Six Yard Boxes
Exactly the same process as the 18 yard boxes, see this is easy.


  p <- p + geom_rect(aes(xmin=xmin, xmax=sixYardDef, ymin=sixYardLeft, ymax=sixYardRight), fill = grass_colour, colour = line_colour) +
    geom_rect(aes(xmin=sixYardOff, xmax=xmax, ymin=sixYardLeft, ymax=sixYardRight), fill = grass_colour, colour = line_colour)

This is 90% finished, its just left with the small details.



Step 6: Adding those Spots
We need to add three spots; the centre-spot and the two penalty spots. This is acheived with the help of geom_point().


  p <- p +
  # add penalty spot left
  geom_point(aes(x = penSpotDef , y = CentreSpot), colour = line_colour, size = 0.75) +
  # add penalty spot right
  geom_point(aes(x = penSpotOff , y = CentreSpot), colour = line_colour, size = 0.75) +
  # add centre spot
  geom_point(aes(x = halfwayline , y = CentreSpot), colour = line_colour, size = 0.75)

asds



Step 7: Adding the Arcs of the D
I stole some maths from someone else to calculate the arcs (if you recognise the code please let me know and I will give due credit - I can't remember where I got it from!). The annoate function will be used to draw the path of both arcs.


  p <- p + annotate("path",
           x = 12 + 10 * cos(seq(-0.3*pi, 0.3*pi, length.out = 30)),
           y = 40 + 10 * sin(seq(-0.3*pi, 0.3*pi, length.out = 30)),
           col = line_colour) +
  annotate("path",
           x = (120-12) - 10 * cos(seq(-0.3*pi, 0.3*pi, length.out = 30)),
           y = 40 + 10 * sin(seq(-0.3*pi, 0.3*pi, length.out = 30)),
           col = line_colour)

One last thing is missing.... the goals



Step 7: Adding the Goals
There are many different styles that goals can be displayed, the most basic using simple lines.


  p <- p +
  # add the goal Defensive
  geom_segment(aes(x = xmin, y = goalPostLeft, xend = xmin, yend = goalPostRight),colour = goal_colour, size = 1) +
  # add the goal offensive
  geom_segment(aes(x = xmax, y = goalPostLeft, xend = xmax, yend = goalPostRight),colour = goal_colour, size = 1)
 

There you go!



This tutorial should give you a good idea of how ggplot2 layers elements on top of each other. Try and adapt the colours to create your own styled pitches.

Full Code



  options(warn=-1)
  require(ggplot2)

  # set our colours for our pitch
  grass_colour <- "#775D6A"
  line_colour <- "#F4828C"
  background_colour <- "#775D6A"
  goal_colour <- "#7E3C5D"

  theme_blankPitch = function(size=12) {
    theme(
      #axis.line=element_blank(),
      axis.text.x=element_blank(),
      axis.text.y=element_blank(),
      #axis.ticks.y=element_text(size=size),
      axis.ticks.length=unit(0, "lines"),
      #axis.ticks.margin=unit(0, "lines"),
      axis.title.x=element_blank(),
      axis.title.y=element_blank(),
      legend.background=element_rect(fill=background_colour, colour=NA),
      legend.key=element_rect(colour=background_colour,fill=background_colour),
      legend.key.size=unit(1.2, "lines"),
      legend.text=element_text(size=size),
      legend.title=element_text(size=size, face="bold",hjust=0),
      strip.background = element_rect(colour = background_colour, fill = background_colour, size = .5),
      panel.background=element_rect(fill=background_colour,colour=background_colour),
      panel.grid.major=element_blank(),
      panel.grid.minor=element_blank(),
      panel.spacing=element_blank(),
      plot.background=element_blank(),
      plot.margin=unit(c(0, 0, 0, 0), "lines"),
      plot.title=element_text(size=size*1.2),
      strip.text.y=element_text(colour=background_colour,size=size,angle=270),
      strip.text.x=element_text(size=size*1))}


  ymin <- 0 # minimum width
  ymax <- 80 # maximum width
  xmin <- 0 # minimum length
  xmax <- 120 # maximum length


    # Defining features along the length
  boxEdgeDef <- 18
  boxEdgeOff <- 102
  halfwayline <- 60
  sixYardDef <- 6
  sixYardOff <- 114
  penSpotDef <- 12
  penSpotOff <- 108

  # Defining features along the width
  boxEdgeLeft <- 18
  boxEdgeRight <- 62
  sixYardLeft <- 30
  sixYardRight <- 50
  goalPostLeft <- 36
  goalPostRight <- 44
  CentreSpot <- 40

  # other dimensions
  centreCirle_d <- 20

  padding = 5


  circleFun <- function(center = c(0,0),diameter = 1, npoints = 100){
    r = diameter / 2
    tt <- seq(0,2*pi,length.out = npoints)
    xx <- center[1] + r * cos(tt)
    yy <- center[2] + r * sin(tt)
    return(data.frame(x = xx, y = yy))
  }

  #### create center circle ####
  center_circle <- circleFun(c(halfwayline,CentreSpot),centreCirle_d,npoints = 100)

  ### FIRST STAGE
  ## initiate the plot, set some boundries to the plot
  p <- ggplot() + xlim(c(xmin-padding,xmax+padding)) + ylim(c(ymin-padding,ymax+padding)) +
  # add the theme
  theme_blankPitch()

  p <- p +
  # add the base rectangle of the pitch
  geom_rect(aes(xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax), fill = NA, colour = line_colour) +
  # add the 18 yard box defensive
  geom_rect(aes(xmin=xmin, xmax=boxEdgeDef, ymin=boxEdgeLeft, ymax=boxEdgeRight), fill = grass_colour, colour = line_colour) +
  # add the 18 yard box offensive
  geom_rect(aes(xmin=boxEdgeOff, xmax=xmax, ymin=boxEdgeLeft, ymax=boxEdgeRight), fill = grass_colour, colour = line_colour) +
  # add halway line
  geom_segment(aes(x = halfwayline, y = ymin, xend = halfwayline, yend = ymax),colour = line_colour) +
  # add the six yard box Defensive
  geom_rect(aes(xmin=xmin, xmax=sixYardDef, ymin=sixYardLeft, ymax=sixYardRight), fill = grass_colour, colour = line_colour)  +
  # add the six yard box offensive
  geom_rect(aes(xmin=sixYardOff, xmax=xmax, ymin=sixYardLeft, ymax=sixYardRight), fill = grass_colour, colour = line_colour) +
  # add centre circle
  geom_path(data=center_circle, aes(x=x,y=y), colour = line_colour) +
    # add penalty spot left
  geom_point(aes(x = penSpotDef , y = CentreSpot), colour = line_colour, size = 0.75) +
  # add penalty spot right
  geom_point(aes(x = penSpotOff , y = CentreSpot), colour = line_colour, size = 0.75) +
  # add centre spot
  geom_point(aes(x = halfwayline , y = CentreSpot), colour = line_colour, size = 0.75) +
  # add the arcs
  annotate("path",
           x = 12 + 10 * cos(seq(-0.3*pi, 0.3*pi, length.out = 30)),
           y = 40 + 10 * sin(seq(-0.3*pi, 0.3*pi, length.out = 30)),
           col = line_colour) +
  annotate("path",
           x = (120-12) - 10 * cos(seq(-0.3*pi, 0.3*pi, length.out = 30)),
           y = 40 + 10 * sin(seq(-0.3*pi, 0.3*pi, length.out = 30)),
           col = line_colour) +
  # add the goal Defensive
  geom_segment(aes(x = xmin, y = goalPostLeft, xend = xmin, yend = goalPostRight),colour = goal_colour, size = 1) +
  # add the goal offensive
  geom_segment(aes(x = xmax, y = goalPostLeft, xend = xmax, yend = goalPostRight),colour = goal_colour, size = 1)
 

Other Data Visualisation Lessons...


  1. Scatter Plots & Crosshairs with ggPlot2
  2. Building a Radar Plot: from the ground up in ggplot2
  3. Building a StatsBomb Ready Pitch with ggplot2