R Tutorial: Creating an NHL rink using the Tidyverse

Hockey is officially back. The NHL has kicked off its 2020-21 regular season and there will be hockey every single day for over 100 days, knock on wood. One part of The Win Column’s offseason improvements was to generate automated plots with R to visualise Flames games.

One of the plots depict shot locations, which inherently requires an overlay of the rink in order to make the (x, y) location data mean anything. There are several NHL rinks already created with R, one coming from Andrew Thomas for War-on-Ice, another from Matthew A. from Stats With Matt, as well as one from Eric Fastner’s icescrapR package (coded by Prashanth Iyer).

R Tutorial: Creating an NHL rink using the Tidyverse, featured image, thewincolumn.ca

I set out to construct my own version using Tidyverse functions from the ggplot2 and ggforce packages. Using their functions, I should be able to plot every element independently from one another as “geoms”, which gives more flexibility to modify or outright remove each element.

The code

If you are just here for the code, let’s get that out of the way first. The R code is available on Github for your perusal. Please feel free to dive in, clone or modify whatever you need to, and make your own plots as it suits you.

You can also source it into your own code by using devtools:

devtools::source_url("https://raw.githubusercontent.com/mrbilltran/the-win-column/master/nhl_rink_plot.R")

Everything beyond this point is a bit heavy on the R code and at times a bit of math is involved too. I’m including it in hopes that the methodology can be well-documented and the work can be replicated as a learning tool.

Methodology

I followed every rule presented in the official 2019-2020 NHL rulebook. Following each description in depth, I plotted every reasonable detail in R. This includes the correct line widths, colours, and the weirdly labelled corner radii by the boards (more on that in a bit).

The idea was to create as accurate a rink as possible based on the rules, partly to present a complete product, and partly to get better with coding.

I’ll give a brief run down of each relevant rule book section as well as some code snippets using ggplot2 and ggforce. The snippets shown in this post are simplified, which includes removing aesthetics such as setting the colour on each geom.

Also note that in this post, I’m listing items in the order of the rule book and not necessarily the order of the R code. Refer to Github for the fully formatted and ordered code.

Lastly, all units in the code are in feet to match the rules, as well as allow for one-to-one plotting of (x, y) location data. In the case of rounding, two decimal points were used.

So here are all the rules and references used to create the rink. Let’s get started.

Dimensions

The official size of the rink shall be two hundred feet (200′) long and eighty-five feet (85′) wide. The corners shall be rounded in the arc of a circle with a radius of twenty-eight feet (28′). See diagram on page iv preceding the table of contents.

Rule 1.2

The rink border components can be made up of lines and arcs. In the code, these geoms are plotted last so that they’d be the top layer, covering up the ends of other lines to make the final plot neater.

# Top edge of the rink
ggplot2::geom_line(aes(x = c(-72, 72), y = c(42.5, 42.5))) + 
# Top-right corner of the rink
ggforce::geom_arc(aes(x0 = 72, y0 = 14.5, start = pi / 2, end = 0, r = 28)) +
... 

Quick side note that drove me crazy when my borders were not lining up: Recall that I said the corner radii of the boards were weirdly labelled… The following dimension outlined in red makes it look like the corner radius is supposed to be 11 feet. But as stated in Rule 1.2, the radius is actually 28 feet. So what gives?

Turns out the 11 feet value is dimensioning the distance from the end boards to the goal line, and not the radius at all. In other words, the official dimensions of the rink surface do not reflect a properly scaled rink. The more you know!

Lines

Eleven feet (11′) from each end of the rink and in the center of a red line two inches (2″) wide drawn completely across the width of the ice and continued vertically up the side of the boards, regulation goal posts and nets shall be set in such a manner as to remain stationary during the progress of a game.

The red line, two inches (2”) wide, between the goal posts on the ice and extended completely across the rink, shall be known as the “GOAL LINE.”

In front of each goal, a “GOAL CREASE” area shall be marked by a red line two inches (2”) in width.

The ice area between the two goals shall be divided into three parts by lines, twelve inches (12”) in width, and blue in color, drawn sixty-four feet (64′) out from the goal lines, and extended completely across the rink, parallel with the goal lines, and continued vertically up the side of the boards. (Paint code PMS 286.)

There shall also be a line, twelve inches (12”) in width and red in color, drawn completely across the rink in center ice, parallel with the goal lines and continued vertically up the side of the boards, known as the “CENTER LINE.” This line shall contain regular interval markings of a uniform distinctive design, which will readily distinguish it from the two blue lines, the outer edges of which must be continuous. (Paint code PMS 186.)

Rule 1.5

Instead of using line geoms, I opted to use rectangles with the tile geom so that the widths of each line can be controlled. Also, to determine the “height” of the goal line (technically the length but listed as height in the code), I calculated the mathematical intersect between the goal line and the corner radius, setting x = -89. By symmetry, the right-side lines just use positive x values.

# Centre line
ggplot2::geom_tile(aes(x = 0, y = 0, width = 1, height = 85)) +
# Left blue line
ggplot2::geom_tile(aes(x = -25.5, y = 0, width = 1, height = 85)) +
# Left goal line
ggplot2::geom_tile(aes(x = -89, y = 0, width = 2 / 12, height = 73.50)) +
...

Goal crease and referee crease

The goal crease shall be laid out as follows: One foot (1′) outside of each goal post a two-inch (2”) line shall be painted extending four feet, six inches (4’6″) in length. These lines shall be at right angles to the goal line. A semi-circle line six feet (6′) in radius and two inches (2″) in width shall be drawn using the center of the goal line as the center point and connecting both ends of the side of the crease. On the side of the crease lines, four feet (4′) from the goal line, extend a five-inch (5″) line into the crease. (see diagram on page iv preceding the table of contents)

The goal crease area shall include all the space outlined by the crease lines and extending vertically four feet (4′) to the level of the top of the goal frame. The area outlined by the crease line and the goal line shall be painted a light blue color. (Paint code PMS 298.)

The area inside the goal frame to the goal line shall be painted a gloss white color.

On the ice immediately in front of the Penalty Timekeeper’s seat there shall be marked in red on the ice a semi-circle of ten foot (10′) radius and two inches (2”) in width which shall be known as the “REFEREE’S CREASE.”

Rule 1.7

The creases use a combination of different geoms to make up the border and the area inside the crease. The circular arc geoms were created with a bit of trial and error to get close to the right sizing such that the borders could be plotted on top and the crease area would appear uniform.

# Light blue crease area: rectangle 
ggplot2::geom_tile(aes(x = -86.75, y = 0, width = 4.5, height = 8)) +
# Light blue crease area: circular arc
ggforce::geom_arc_bar(aes(x0 = -89, y0 = 0, start = atan(4.5/4) - 0.01, end = pi - atan(4.5 / 4) + 0.01, r0 = 4, r = 6), size = 1 / 12) +
...

The goalie crease borders were similarly created using tile and arc geoms and plotted on top of the crease area. The referee’s crease was simply a single arc geom.

# Referee's crease
ggforce::geom_arc(aes(x0 = 0, y0 = -42.5, start = -pi / 2, end = pi / 2, r = 10)) +
...

Goalkeeper nets

Nets were added into the plot as rectangular boxes for completeness.

# Left net
ggplot2::geom_tile(aes(x = -90.67, y = 0, width = 3.33, height = 6)) + 
# Right net
ggplot2::geom_tile(aes(x = 90.67, y = 0, width = 3.33, height = 6)) +
...

Goalkeeper’s restricted area

A restricted trapezoid-shaped area behind the goal will be laid out as follows: Seven feet (7′) outside of each goal crease (eight feet (8′) from each goal post), a two-inch (2″) red line shall be painted extending from the goal line to a point on the end of the rink ten feet (10′) from the goal crease (eleven feet (11′) from the goal post) and continuing vertically up the kick plate (see diagram on the page iv preceding the table of contents). (Paint code PMS 186).

Rule 1.8

The trapezoids, due to their irregular angles, were plotted with shape geoms, such that each line would be defined as a parallelogram with it one end starting within the goal line and the other ending within the end boards.

# Top component of left-side trapezoid
ggplot2::geom_polygon(aes(x = c(-100, -100, -89, -89), y = c(10.92, 11.08, 7.08, 6.92))) +
# Bottom component of left-side trapezoid
ggplot2::geom_polygon(aes(x = c(-100, -100, -89, -89), y = c(-10.92, -11.08, -7.08, -6.92))) +
...

Face-off spots and circles

A circular blue spot, twelve inches (12”) in diameter, shall be marked exactly in the center of the rink; and with this spot as a center, a circle of fifteen feet (15′) radius shall be marked with a blue line two inches (2”) in width.

Two red spots two feet (2′) in diameter shall be marked on the ice in the neutral zone five feet (5′) from each blue line. Within the face-off spot, draw two parallel lines three inches (3”) from the top and bottom of the spot. The area within the two lines shall be painted red, the remainder shall be painted white. The spots shall be forty-four feet (44′) apart and each shall be a uniform distance from the adjacent boards.

In both end zones and on both sides of each goal, red face-off spots and circles shall be marked on the ice. The face-off spots shall be two feet (2′) in diameter. Within the face-off spot, draw two parallel lines three inches (3”) from the top and bottom of the spot. The area within the two lines shall be painted red, the remainder shall be painted white.

The circles shall be two inches (2”) wide with a radius of fifteen feet (15′) from the center of the face-off spots. At the outer edge of both sides of each face-off circle and parallel to the goal line shall be marked two red lines, two inches (2”) wide and two feet (2′) in length and five feet seven inches (5’7”) apart.

One foot away from the outer edge of the face-off spot, two lines shall be drawn parallel with the side boards that shall be four feet (4′)in length and eighteen inches (18″) apart. Parallel to the end boards, commencing at the end of the line nearest to the face-off spot, a line shall extend two feet ten inches (2’10”) in length. All lines shall be two inches (2″) in width. See diagram on page v preceding the table of contents.

The location of the face-off spots shall be fixed in the following manner:

Along a line twenty feet (20′) from each goal line and parallel to it, mark two points twenty-two feet (22′) on both sides of the straight line joining the center of the two goals. Each such point shall be the center of a face-off spot and circle.

Rule 1.9

The face-off spots and circles were by far the most involved components, as they included the circles and spots themselves, hash marks, and the inner “ell” markings that surround the face-off spots inside face-off circles. The face-off spots are fully filled in as well, so there is no red versus white areas inside the face-off spots.

Each of these components included some form of math to get the correct positioning. The math involved finding intersects between lines and circles as well as finding the centres of each geom. I’ll include the code for the top right faceoff circle to keep all the values positive.

# Outer circle
ggforce::geom_circle(aes(x0 = 69, y0 = 22, r = 15), size = 2 / 12) + 
# Hash marks
ggplot2::geom_tile(aes(x = 66.125, y = 37.77, width = 2 / 12, height = 2)) +
ggplot2::geom_tile(aes(x = 66.125, y = 6.23, width = 2 / 12, height = 2)) +
ggplot2::geom_tile(aes(x = 71.875, y = 37.77, width = 2 / 12, height = 2)) +
ggplot2::geom_tile(aes(x = 71.875, y = 6.23, width = 2 / 12, height = 2)) +
# Face-off spot
ggforce::geom_circle(aes(x0 = 69, y0 = 22, r = 1), size = 0) +
# Eight components to form the four ells
ggplot2::geom_tile(aes(x = 65, y = 22.83, width = 4, height = 2 / 12)) + 
ggplot2::geom_tile(aes(x = 73, y = 22.83, width = 4, height = 2 / 12)) +
ggplot2::geom_tile(aes(x = 65, y = 21.17, width = 4, height = 2 / 12)) +
ggplot2::geom_tile(aes(x = 73, y = 21.17, width = 4, height = 2 / 12)) +
ggplot2::geom_tile(aes(x = 66.92, y = 24.25, width = 2 / 12, height = 3)) +
ggplot2::geom_tile(aes(x = 71.08, y = 24.25, width = 2 / 12, height = 3)) +
ggplot2::geom_tile(aes(x = 66.92, y = 19.75, width = 2 / 12, height = 3)) +
ggplot2::geom_tile(aes(x = 71.08, y = 19.75, width = 2 / 12, height = 3)) +
...

Coordinate system and plot background

The last bit of code sets the coordinate system and plot aesthetics. The coordinates need to have the same aspect ratio in order for the x and y coordinates to scale properly. This is controlled by adding a fixed scale at the end of the code.

Using the “void” theme when creating the plot will keep the plot free of elements like axes and gridlines. The theme isn’t directly included in the code as uploaded in Github, as it would likely be easier to control that within individual plots that call on the rink function. A reminder is commented at the end of the code as well.

... +
coord_fixed() +
theme_void()

Colours

Depending on the geom, some colours are controlled using the “fill” versus the “colour” aesthetics. This is all included for each individual geom so that customisation can be independently controlled for every single element. You might have noticed in some of the rules, there are paint codes listed with its Pantone designation (e.g. PMS 186 for the red lines).

I converted this values into hex codes to plot, and also included lightened versions in case the full saturated colours are too dark. The top row shows the Pantone colours with no modification, the middle row boosts the colour brightness to full and reduces the saturation to 20%, and the bottom row further reduces the saturation to just 10%. I did this rather than changing the transparency (alpha) levels of the colours, as layers can build up and darken, which was undesirable.

To change the colours, you may want to either clone the code off of Github and modify it to preserve the function, or copy and paste the code without the function and using it in a script in full. Currently, the colour values in the code are taken from the middle row, which makes plotting location-based events clearer with lesser distraction from the markings.

Final rink output

After all of this, you get an NHL rink created using ggplot2 and ggforce, ready for use with more geoms as you wish.

matching rule book colours

Lightened to plot more items on top

So there you have it, an NHL rink made entirely with the Tidyverse and R. If you have any feedback or questions, please let me know either in the comments or on Twitter: @mrbilltran.

I hope this was useful or insightful!

4 thoughts on “R Tutorial: Creating an NHL rink using the Tidyverse

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s