1 Introduction

In this document, we’ll learn how to draw an NBA half-court using R and ggplot2.
This exercise combines data visualisation and creativity — showing how R’s plotting system can be used to draw detailed shapes, lines, and features, not just charts.

The example below recreates a Detroit Pistons “The Palace” half-court design using geometric layers and precise coordinate placement.

2 Setup

Before creating the court, we need to load the ggplot2 library, which allows us to draw and layer shapes.

library(ggplot2)

3 Creating a Half Court

To begin drawing our NBA half-court, we first need to set up the base dimensions — this defines the coordinate space where all the court elements (lines, arcs, and key) will be added.

We’ll use the ggplot() function as our plotting base, and coord_fixed() to ensure that one unit on the x-axis equals one unit on the y-axis. This keeps the court proportions realistic.

# Create empty court with fixed dimensions
court <- ggplot() +
  coord_fixed(xlim = c(0, 50), ylim = c(0, 47)) +   # Set court width and height
  theme_void() +                                    # Remove gridlines and axes
  labs(title = "NBA Half Court Dimensions") +       
  theme(
    plot.title = element_text(hjust = 0.5, size = 18, face = "bold")
  )

3.1 Adding the Outer Boundary

Now that we’ve defined our court dimensions, the next step is to draw the outer boundary lines that form the edges of the half-court.

These are created using geom_segment(), which draws straight lines between two coordinate points — an (x, y) start and an (xend, yend) end.
We’ll use the Pistons’ team colour blue (#1D428A) for these lines.

court <- ggplot() +
  coord_fixed(xlim = c(0, 50), ylim = c(0, 47)) +
  
  # Outer Boundary Lines
geom_segment(aes(x = 0, y = 0, xend = 0, yend = 47), colour = "#1D428A", linewidth = 1.2) + # Left line
geom_segment(aes(x = 0, y = 47, xend = 50, yend = 47), colour = "#1D428A", linewidth = 1.2) + # Top line
geom_segment(aes(x = 50, y = 47, xend = 50, yend = 0), colour = "#1D428A", linewidth = 1.2) + # Right line
geom_segment(aes(x = 50, y = 0, xend = 0, yend = 0), colour = "#1D428A", linewidth = 1.2) + # Bottom line

theme_void() +
labs(title = "Detroit Pistons – The Palace") +
theme(
plot.title = element_text(hjust = 0.5, size = 16, face = "bold", colour = "#C8102E"),
plot.background = element_rect(fill = "#F5DEB3", colour = NA)
)

court

3.2 Adding the Key (Paint Area)

Next, we’ll draw the key, also known as the paint area, which sits directly under the basket.
This is the rectangular zone where players position themselves for rebounds and free throws.

We’ll again use the geom_segment() function to draw the sides and top of the key.
For styling, we’ll switch to the Pistons’ red colour (#C8102E) to highlight this section against the blue border.

court <- ggplot() +
  coord_fixed(xlim = c(0, 50), ylim = c(0, 47)) +
  

  # Outer Boundary (Royal Blue)
geom_segment(aes(x = 0, y = 0, xend = 0, yend = 47), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 0, y = 47, xend = 50, yend = 47), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 50, y = 47, xend = 50, yend = 0), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 50, y = 0, xend = 0, yend = 0), colour = "#1D428A", linewidth = 1.2) +

  # Key (Paint Area – Red)

geom_segment(aes(x = 17, y = 0.175, xend = 17, yend = 19), colour = "#C8102E", linewidth = 1.1) + # Left edge
geom_segment(aes(x = 33, y = 0.175, xend = 33, yend = 19), colour = "#C8102E", linewidth = 1.1) + # Right edge
geom_segment(aes(x = 17, y = 19, xend = 33, yend = 19), colour = "#C8102E", linewidth = 1.1) + # Top of key

theme_void() +
labs(title = "Detroit Pistons – The Palace") +
theme(
plot.title = element_text(hjust = 0.5, size = 16, face = "bold", colour = "#C8102E"),
plot.background = element_rect(fill = "#F5DEB3", colour = NA)
)

court

3.3 Adding the Free Throw Line and Circle

The next component of the half-court is the Free Throw Line and the Free Throw Circle — the curved area players stand around when shooting free throws.

We’ll draw the line and arc using geom_segment() and geom_curve().
The dashed curve represents the outer circular guideline, and the solid curve represents the actual free throw arc.

court <- ggplot() +
  coord_fixed(xlim = c(0, 50), ylim = c(0, 47)) +
  
  # Outer Boundary
geom_segment(aes(x = 0, y = 0, xend = 0, yend = 47), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 0, y = 47, xend = 50, yend = 47), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 50, y = 47, xend = 50, yend = 0), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 50, y = 0, xend = 0, yend = 0), colour = "#1D428A", linewidth = 1.2) +
  
  # Key (Paint Area)
geom_segment(aes(x = 17, y = 0.175, xend = 17, yend = 19), colour = "#C8102E", linewidth = 1.1) +
geom_segment(aes(x = 33, y = 0.175, xend = 33, yend = 19), colour = "#C8102E", linewidth = 1.1) +
geom_segment(aes(x = 17, y = 19, xend = 33, yend = 19), colour = "#C8102E", linewidth = 1.1) +
  
  # Free Throw Line and Circle
geom_curve(aes(x = 25 - 6, y = 19, xend = 25 + 6, yend = 19),
colour = "#1D428A", linewidth = 1, linetype = "dashed", curvature = 0.8) + # Outer dashed arc
geom_curve(aes(x = 25 + 6, y = 19, xend = 25 - 6, yend = 19),
colour = "#1D428A", linewidth = 1.2, curvature = 0.8) + # Inner solid arc                     # Inner arc
  
theme_void() +
labs(title = "Detroit Pistons – The Palace") +
theme(
plot.title = element_text(hjust = 0.5, size = 16, face = "bold", colour = "#C8102E"),
plot.background = element_rect(fill = "#F5DEB3", colour = NA)
)

court

3.4 Adding the Three-Point Line

The three-point line is one of the most recognisable features of a basketball court.
It consists of two straight lines extending up from the baseline and a large curved arc that connects them.

We’ll use geom_segment() for the straight edges in each corner, and geom_curve() for the main three-point arc.

court <- ggplot() +
  coord_fixed(xlim = c(0, 50), ylim = c(0, 47)) +
  
  # Outer Boundary
geom_segment(aes(x = 0, y = 0, xend = 0, yend = 47), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 0, y = 47, xend = 50, yend = 47), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 50, y = 47, xend = 50, yend = 0), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 50, y = 0, xend = 0, yend = 0), colour = "#1D428A", linewidth = 1.2) +
  
  # Key and Free Throw Circle
geom_segment(aes(x = 17, y = 0.175, xend = 17, yend = 19), colour = "#C8102E", linewidth = 1.1) +
geom_segment(aes(x = 33, y = 0.175, xend = 33, yend = 19), colour = "#C8102E", linewidth = 1.1) +
geom_segment(aes(x = 17, y = 19, xend = 33, yend = 19), colour = "#C8102E", linewidth = 1.1) +
geom_curve(aes(x = 25 - 6, y = 19, xend = 25 + 6, yend = 19),
colour = "#1D428A", linewidth = 1, linetype = "dashed", curvature = 0.80) +
geom_curve(aes(x = 25 + 6, y = 19, xend = 25 - 6, yend = 19),
colour = "#1D428A", linewidth = 1.2, curvature = 0.80) +
  
  # Three-Point Line
geom_segment(aes(x = 3, y = 0, xend = 3, yend = 14), colour = "#1D428A", linewidth = 1.1) + # Left corner
geom_segment(aes(x = 47, y = 0, xend = 47, yend = 14), colour = "#1D428A", linewidth = 1.1) + # Right corner
geom_curve(aes(x = 3, y = 14, xend = 47, yend = 14),
colour = "#1D428A", linewidth = 1.1, curvature = -0.55) + # Arc
  
theme_void() +
labs(title = "Detroit Pistons – The Palace") +
theme(
plot.title = element_text(hjust = 0.5, size = 16, face = "bold", colour = "#C8102E"),
plot.background = element_rect(fill = "#F5DEB3", colour = NA)
)
court

3.5 Adding the Backboard, Rim, and Restricted Area

Now we’ll add the features directly under the hoop — the backboard, the rim, and the restricted area.
These are central to the scoring zone and help bring the court to life visually.

We’ll use: - geom_segment() for the backboard,
- geom_point() for the rim, and
- geom_curve() for the restricted area (the curved line under the hoop).

court <- ggplot() +
  coord_fixed(xlim = c(0, 50), ylim = c(0, 47)) +
  
  # Outer Boundary
geom_segment(aes(x = 0, y = 0, xend = 0, yend = 47), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 0, y = 47, xend = 50, yend = 47), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 50, y = 47, xend = 50, yend = 0), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 50, y = 0, xend = 0, yend = 0), colour = "#1D428A", linewidth = 1.2) +
  
  # Key and Free Throw Circle
geom_segment(aes(x = 17, y = 0.175, xend = 17, yend = 19), colour = "#C8102E", linewidth = 1.1) +
geom_segment(aes(x = 33, y = 0.175, xend = 33, yend = 19), colour = "#C8102E", linewidth = 1.1) +
geom_segment(aes(x = 17, y = 19, xend = 33, yend = 19), colour = "#C8102E", linewidth = 1.1) +
geom_curve(aes(x = 25 - 6, y = 19, xend = 25 + 6, yend = 19),
colour = "#1D428A", linewidth = 1, linetype = "dashed", curvature = 0.80) +
geom_curve(aes(x = 25 + 6, y = 19, xend = 25 - 6, yend = 19),
colour = "#1D428A", linewidth = 1.2, curvature = 0.80) +
  
  # Three-Point Line
geom_segment(aes(x = 3, y = 0, xend = 3, yend = 14), colour = "#1D428A", linewidth = 1.1) + # Left corner
geom_segment(aes(x = 47, y = 0, xend = 47, yend = 14), colour = "#1D428A", linewidth = 1.1) + # Right corner
geom_curve(aes(x = 3, y = 14, xend = 47, yend = 14),
colour = "#1D428A", linewidth = 1.1, curvature = -0.55) + # Arc
  
  # Backboard
geom_segment(aes(x = 25 - 3, y = 4, xend = 25 + 3, yend = 4),
colour = "black", linewidth = 1.5) +
  
  # Rim
geom_point(aes(x = 25, y = 4.5), colour = "orange", size = 8) +
geom_point(aes(x = 25, y = 4.5), colour = "#F5DEB3", size = 5.5) +
  
  # Restricted Area
geom_curve(aes(x = 25 - 4, y = 4.5, xend = 25 + 4, yend = 4.5),
colour = "#C8102E", linewidth = 1.2, curvature = -1) +
  
theme_void() +
labs(title = "Detroit Pistons – The Palace") +
theme(
plot.title = element_text(hjust = 0.5, size = 16, face = "bold", colour = "#C8102E"),
plot.background = element_rect(fill = "#F5DEB3", colour = NA)
)

court

3.6 Adding the Half-Court Circle and Final Formatting

The last visual element to complete our NBA half-court is the Half-Court Circle — the large arc seen at centre court.
This helps balance the layout and signals where jump balls occur at the start of the game.

We’ll use geom_curve() again to draw two arcs:
- one large outer arc, and
- one smaller inner arc, to give a clean, layered effect.

We’ll also finalise the overall styling using theme_void() and element_rect() to set the background colour.

court <- ggplot() +
  coord_fixed(xlim = c(0, 50), ylim = c(0, 47)) +
  
  # Outer Boundary
geom_segment(aes(x = 0, y = 0, xend = 0, yend = 47), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 0, y = 47, xend = 50, yend = 47), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 50, y = 47, xend = 50, yend = 0), colour = "#1D428A", linewidth = 1.2) +
geom_segment(aes(x = 50, y = 0, xend = 0, yend = 0), colour = "#1D428A", linewidth = 1.2) +
  
  # Key and Free Throw Circle
geom_segment(aes(x = 17, y = 0.175, xend = 17, yend = 19), colour = "#C8102E", linewidth = 1.1) +
geom_segment(aes(x = 33, y = 0.175, xend = 33, yend = 19), colour = "#C8102E", linewidth = 1.1) +
geom_segment(aes(x = 17, y = 19, xend = 33, yend = 19), colour = "#C8102E", linewidth = 1.1) +
geom_curve(aes(x = 25 - 6, y = 19, xend = 25 + 6, yend = 19),
colour = "#1D428A", linewidth = 1, linetype = "dashed", curvature = 0.80) +
geom_curve(aes(x = 25 + 6, y = 19, xend = 25 - 6, yend = 19),
colour = "#1D428A", linewidth = 1.2, curvature = 0.80) +
  
  # Three-Point Line
geom_segment(aes(x = 3, y = 0, xend = 3, yend = 14), colour = "#1D428A", linewidth = 1.1) + # Left corner
geom_segment(aes(x = 47, y = 0, xend = 47, yend = 14), colour = "#1D428A", linewidth = 1.1) + # Right corner
geom_curve(aes(x = 3, y = 14, xend = 47, yend = 14),
colour = "#1D428A", linewidth = 1.1, curvature = -0.55) + # Arc
  
  # Backboard
geom_segment(aes(x = 25 - 3, y = 4, xend = 25 + 3, yend = 4),
colour = "black", linewidth = 1.5) +
  
  # Rim
geom_point(aes(x = 25, y = 4.5), colour = "orange", size = 8) +
geom_point(aes(x = 25, y = 4.5), colour = "#F5DEB3", size = 5.5) +
  
  # Restricted Area
geom_curve(aes(x = 25 - 4, y = 4.5, xend = 25 + 4, yend = 4.5),
colour = "#C8102E", linewidth = 1.2, curvature = -1) +

  #Final Touches  
labs(title = "Detroit Pistons – The Palace") +
theme_void() +
theme(
plot.title = element_text(hjust = 0.5, size = 18, face = "bold", colour = "#C8102E"),
plot.background = element_rect(fill = "#F5DEB3", colour = NA)
)

court

4 Conclusion

In this project, we learned how to create a fully customised NBA half-court using R and the ggplot2 package.
By layering geometric shapes such as geom_segment(), geom_curve(), and geom_point(), we were able to draw realistic features including the key, three-point line, rim, and half-court circle.

This exercise demonstrates how R’s visualisation tools extend beyond traditional charts and graphs. Allowing us to create precise, scalable, and data-driven graphics entirely through code.

Now that the court layout is complete, this base can serve as a powerful visual foundation for further basketball analytics work.

For example, we could: - Overlay shot location data to create a heatmap of player performance,
- Highlight specific zones such as mid-range or three-point areas, or
- Animate player movements over time for advanced visual storytelling.

Overall, building this court reinforces key ggplot2 concepts like layering, coordinate scaling, and shape customisation — while combining creativity and data visualisation into one powerful workflow.