ETC5512

Australian election data

Lecturer: Kate Saunders

Department of Econometrics and Business Statistics


  • ETC5512.Clayton-x@monash.edu
  • Wild Caught Data
  • wcd.numbat.space


Today’s Lecture

What we’ll cover

  • Learn about Australian election data
  • Look at results from the last election
  • Learn how to visualise the election results spatially in a few ways

From a coding perspective:

  • This will require learning about spatial mapping in R.

  • You will also need to learn about different mapping projections

Australian Election Data

Australian Election Data


Important Information

  • Much like the census, the election attempts to collect the data from the population.

  • In Australia, it is compulsory by law to vote in elections if you are an Australian citizen or eligible British subject aged 18 years old or over and have lived in your address for at least one month.

  • The Australian Electoral Commission (AEC) is an independent federal agency in charge of federal Australian elections and provides the geographical boundaries of the electoral divisions.

Some questions


Quick quiz

  1. When was the last federal election in Australia?
  2. How often is the federal election conducted in Australia?
  3. How many electoral divisions are there in the last federal election?
  4. What is the population for the Australian federal election?

2022 Australian Federal Election

Aussie Politics

  • Parliament of Australia comprises two houses:
    • Senate (upper house) comprising 76 senators
    • House of Representatives (lower house) comprising 151 members
  • Government is formed by the party or coalition with majority of the seats in the lower house
  • The 2022 Australian Federal Election was held on Sat 21st May 2022
  • The next federal election will likely be held in the next few months!

Parties

Two major parties: Labour and the Coalition. Coalition combines Liberals and Nationals.

There are also minor parties like the Greens and One Nation, and Independents.

Ballots

  • House of Representatives uses the instant-runoff voting system
  • Senate uses the single transferable voting system
.

Data on Voter Counts

2022 Australian Federal Election Data

Let’s download some data

Go to https://results.aec.gov.au

Download the distribution of preferences by candidate by division for the 2022 Australian Federal Election

Select:

  1. 2022 federal election
  2. Downloads
  3. Distribution of preferences by candidate by division

Voting Data

House of Representatives

library(tidyverse)
votes <- read_csv("https://results.aec.gov.au/27966/Website/Downloads/HouseDopByDivisionDownload-27966.csv", skip = 1)
glimpse(votes)
Rows: 35,096
Columns: 14
$ StateAb          <chr> "ACT", "ACT", "ACT", "ACT", "ACT", "ACT", "ACT", "ACT…
$ DivisionID       <dbl> 318, 318, 318, 318, 318, 318, 318, 318, 318, 318, 318…
$ DivisionNm       <chr> "Bean", "Bean", "Bean", "Bean", "Bean", "Bean", "Bean…
$ CountNumber      <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ BallotPosition   <dbl> 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5,…
$ CandidateID      <dbl> 36239, 36239, 36239, 36239, 37455, 37455, 37455, 3745…
$ Surname          <chr> "CONWAY", "CONWAY", "CONWAY", "CONWAY", "AMBARD", "AM…
$ GivenNm          <chr> "Sean", "Sean", "Sean", "Sean", "Benjamin", "Benjamin…
$ PartyAb          <chr> "UAPP", "UAPP", "UAPP", "UAPP", "ON", "ON", "ON", "ON…
$ PartyNm          <chr> "United Australia Party", "United Australia Party", "…
$ Elected          <chr> "N", "N", "N", "N", "N", "N", "N", "N", "Y", "Y", "Y"…
$ HistoricElected  <chr> "N", "N", "N", "N", "N", "N", "N", "N", "Y", "Y", "Y"…
$ CalculationType  <chr> "Preference Count", "Preference Percent", "Transfer C…
$ CalculationValue <dbl> 2831.00, 2.88, 0.00, 0.00, 2680.00, 2.72, 0.00, 0.00,…

Electoral district of Monash

Let’s have a look at the electoral district named “Monash”

Rows: 224
Columns: 14
$ StateAb          <chr> "VIC", "VIC", "VIC", "VIC", "VIC", "VIC", "VIC", "VIC…
$ DivisionID       <dbl> 323, 323, 323, 323, 323, 323, 323, 323, 323, 323, 323…
$ DivisionNm       <chr> "Monash", "Monash", "Monash", "Monash", "Monash", "Mo…
$ CountNumber      <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ BallotPosition   <dbl> 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5,…
$ CandidateID      <dbl> 36561, 36561, 36561, 36561, 36737, 36737, 36737, 3673…
$ Surname          <chr> "MORGAN", "MORGAN", "MORGAN", "MORGAN", "BROADBENT", …
$ GivenNm          <chr> "Mat", "Mat", "Mat", "Mat", "Russell", "Russell", "Ru…
$ PartyAb          <chr> "GVIC", "GVIC", "GVIC", "GVIC", "LP", "LP", "LP", "LP…
$ PartyNm          <chr> "The Greens", "The Greens", "The Greens", "The Greens…
$ Elected          <chr> "N", "N", "N", "N", "Y", "Y", "Y", "Y", "N", "N", "N"…
$ HistoricElected  <chr> "N", "N", "N", "N", "Y", "Y", "Y", "Y", "N", "N", "N"…
$ CalculationType  <chr> "Preference Count", "Preference Percent", "Transfer C…
$ CalculationValue <dbl> 9533.00, 9.86, 0.00, 0.00, 36546.00, 37.79, 0.00, 0.0…

District: Monash

votes_monash <- votes |>
  # get the preference count only
  filter(CalculationType == "Preference Count") |>
  # get the Monash division
  filter(DivisionNm == "Monash")
glimpse(votes_monash)
Rows: 56
Columns: 14
$ StateAb          <chr> "VIC", "VIC", "VIC", "VIC", "VIC", "VIC", "VIC", "VIC…
$ DivisionID       <dbl> 323, 323, 323, 323, 323, 323, 323, 323, 323, 323, 323…
$ DivisionNm       <chr> "Monash", "Monash", "Monash", "Monash", "Monash", "Mo…
$ CountNumber      <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2,…
$ BallotPosition   <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2,…
$ CandidateID      <dbl> 36561, 36737, 36065, 37629, 37637, 36455, 36914, 3603…
$ Surname          <chr> "MORGAN", "BROADBENT", "LEONARD", "HICKEN", "WELSH", …
$ GivenNm          <chr> "Mat", "Russell", "Deb", "Allan", "David Matthew", "J…
$ PartyAb          <chr> "GVIC", "LP", "IND", "ON", "CYA", "ALP", "LDP", "UAPP…
$ PartyNm          <chr> "The Greens", "Liberal", "Independent", "Pauline Hans…
$ Elected          <chr> "N", "Y", "N", "N", "N", "N", "N", "N", "N", "Y", "N"…
$ HistoricElected  <chr> "N", "Y", "N", "N", "N", "N", "N", "N", "N", "Y", "N"…
$ CalculationType  <chr> "Preference Count", "Preference Count", "Preference C…
$ CalculationValue <dbl> 9533, 36546, 10372, 7289, 674, 24759, 3548, 3991, 959…

Visualising the counts

Better

Order candidates by counts!!!

Winner:
Russel
Broadbent

Code

votes_monash_for_plotting |> 
  mutate(Surname = fct_reorder(Surname, CalculationValue, sum))  #<<

ggplot(votes_monahs_for_plotting) +
  geom_col(aes(x = CalculationValue, y = Surname)) +
  geom_text(aes(label = paste("Count", CountNumber + 1)),
    x = 10000, y = 3, size = 16, color = "#ee64a4", alpha = 0.4, hjust = "left"
  ) +
  facet_wrap(~CountNumber) + 
  theme_bw()

Where is the electoral district of Monash?

It doesn’t include Monash Clayton campus- Check here.

Electoral district of Hotham

Does include Monash Clayton campus - Check here.

Maps of Electoral Results

Australian Electorates Divisions

Australia electoral divisions in the 2022 election

There were 151 electorates in 2022.

The geographical boundaries of the electoral divisions are determined by the Redistribution Committee and are redrawn every so often to ensure similar number of electors in each electoral division for a given state or territory.

Electoral Boundaries

Warning!

Electoral boundaries can change across years

Break out discussion

  • Have any boundaries changed from 2022 to 2025?

  • If so which ones?

  • What would that mean for comparing election results between years?

GIS data

GIS

GIS stands for Geographic Information System.

GIS is a framework for capturing and inspecting geographical data.

Federal electoral boundary

Electoral Boundaries

“The Licensee must make End-users aware the data was sourced from the Australian Electoral Commission and is used under licence.”

Note: the federal electoral boundary is provided by Australian Electoral Commission
© Commonwealth of Australia (Australian Electoral Commission) 2025

  • Download the ESRI zip file for Victoria.

  • To work with spatial data, we use the sf R-package.

Working with spatial data

library(sf)
aec_map <- read_sf(here::here("data/vic-july-2021-esri/E_VIC21_region.shp")) |> st_simplify(dTolerance = 250)

head(aec_map)
Simple feature collection with 6 features and 9 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: 143.4412 ymin: -38.07451 xmax: 146.191 ymax: -36.38518
Geodetic CRS:  GDA94
# A tibble: 6 × 10
  E_div_numb Elect_div Numccds Actual Projected Total_Popu Australian Area_SqKm
       <dbl> <chr>       <dbl>  <dbl>     <dbl>      <dbl>      <dbl>     <dbl>
1          1 Aston         377 111098    115439          0          0      114.
2          2 Ballarat      340 106745    115696          0          0     5399.
3          3 Bendigo       351 108821    117818          0          0     5285.
4          4 Bruce         450 112619    116317          0          0      115.
5          5 Calwell       314  99781    116976          0          0      221.
6          6 Casey         425 114652    119870          0          0     2481.
# ℹ 2 more variables: Sortname <chr>, geometry <MULTIPOLYGON [°]>

Geometry object

aec_map$geometry[[1]]
MULTIPOLYGON (((145.3452 -37.85979, 145.3362 -37.85105, 145.3321 -37.85102, 145.3328 -37.84744, 145.3063 -37.84047, 145.3011 -37.83784, 145.281 -37.83314, 145.275 -37.83577, 145.2253 -37.83874, 145.2133 -37.84102, 145.2097 -37.85132, 145.2071 -37.85227, 145.2043 -37.85721, 145.1922 -37.87161, 145.1929 -37.88139, 145.2039 -37.89608, 145.2083 -37.89905, 145.2051 -37.90603, 145.2056 -37.92663, 145.2142 -37.93573, 145.2282 -37.95141, 145.2348 -37.94207, 145.2796 -37.9476, 145.2898 -37.95814, 145.289 -37.96284, 145.2934 -37.96398, 145.2999 -37.93146, 145.3038 -37.93259, 145.2964 -37.91047, 145.2928 -37.90734, 145.297 -37.90122, 145.305 -37.90571, 145.3113 -37.90091, 145.3219 -37.89818, 145.3043 -37.88546, 145.3061 -37.86902, 145.3039 -37.86285, 145.3169 -37.86321, 145.327 -37.8661, 145.3306 -37.86181, 145.3336 -37.86157, 145.339 -37.85695, 145.3426 -37.86133, 145.3452 -37.85979)))

Visualisation in ggplot

ggplot(data = aec_map) +
  geom_sf()

Combining election winners and map

winners <- votes |>
  # get the winner
  filter(Elected == "Y" & CountNumber == 0 & CalculationType == "Preference Count") |>
  # join the data
  right_join(aec_map, by = c("DivisionNm" = "Elect_div")) |>
  select(DivisionNm, PartyAb, PartyNm, geometry)

Combining election winners and map

ggplot(winners) +
  geom_sf(aes(fill = PartyAb, geometry = geometry))

Detective work

Caution

Watch out for mismatched Division Names

  • e.g. “McEwen” vs “Mcewen”

  • That caught me out last year, but it seems like its fixed.

  • For cases like these you might need fuzzy/partial string matching

  • Try agrepl() function

Using colors wisely

aus_colours <- c(
  "ALP" = "#DE3533", "LNP" = "#ADD8E6", "KAP" = "#8B0000",
  "GVIC" = "#10C25B", "XEN" = "#ff6300", "LP" = "#0047AB",
  "NP" = "#0a9cca", "IND" = "#000000", "GRN" = "#006400"
)

ggplot(winners) +
  geom_sf(aes(fill = PartyAb, geometry = geometry)) +
  scale_fill_manual(name = "Party", values = aus_colours) +
  theme_void() +
  theme(legend.position = "bottom")

Zoom

ggplot(winners) +
  geom_sf(aes(fill = PartyAb, geometry = geometry)) +
  scale_fill_manual(name = "Party", values = aus_colours) +
  theme_void() +
  theme(legend.position = "bottom") + 
  xlim(c(144.5,145.5)) + 
  ylim(c(-39,-37.5))

Traditional Maps

Reference:

Hex Maps

Reference

Visualising Maps

Watch out

  • Traditional maps can be mislead you about election results

  • Larger areas don’t mean more votes

  • Better to give equal visual weight

Coordinate reference system (CRS)

Geographic coordinate reference systems

  • Geographic CRSs identify a location on the Earth’s surface by longitude and latitude.
  • Longitude is the East-West direction in angular distance from the Prime Meridian plane.
  • Latitude is the angular distance North or South of the equatorial plane.

Projected coordinate reference systems

  • All projected CRSs are based on a geographic CRS.
  • Map projections convert the three-dimensional surface of the Earth into Easting and Northing (x and y) values (typically meters) in a projected CRS.
  • These projected CRSs are based on Cartesian coordinates on a implicitly flat surface.
  • Some deformations are introduced in the process, e.g. area, direction, distance or shape, while preserving one or two of these properties.

Well Known Text (WKT)

  • Open Geospatial Consortium (OGC) developed an open standard format for describing CRSs called WKT
aus_map <- read_sf(here::here("data/2021-Cwlth_electoral_boundaries_ESRI/2021_ELB_region.shp")) |>
  sf::st_make_valid()|>
  sf::st_simplify(dTolerance = 1000)

sf::st_crs(aus_map)
Coordinate Reference System:
  User input: GDA94 
  wkt:
GEOGCRS["GDA94",
    DATUM["Geocentric Datum of Australia 1994",
        ELLIPSOID["GRS 1980",6378137,298.257222101,
            LENGTHUNIT["metre",1]]],
    PRIMEM["Greenwich",0,
        ANGLEUNIT["degree",0.0174532925199433]],
    CS[ellipsoidal,2],
        AXIS["geodetic latitude (Lat)",north,
            ORDER[1],
            ANGLEUNIT["degree",0.0174532925199433]],
        AXIS["geodetic longitude (Lon)",east,
            ORDER[2],
            ANGLEUNIT["degree",0.0174532925199433]],
    USAGE[
        SCOPE["Horizontal component of 3D system."],
        AREA["Australia including Lord Howe Island, Macquarie Island, Ashmore and Cartier Islands, Christmas Island, Cocos (Keeling) Islands, Norfolk Island. All onshore and offshore."],
        BBOX[-60.55,93.41,-8.47,173.34]],
    ID["EPSG",4283]]

GDA94 Map Projection

ggplot(aus_map) +
  geom_sf()

Changing map projections

  • Map projections may be modified in multiple methods (it’s beyond this unit to delve deep into this).

  • Below uses the Lambert azimuthal equal-area projection centered on the longitude and latitude of (rough) Melbourne coordinates via proj4string:

aus_map_transformed = aus_map |>
  sf::st_transform(crs = "+proj=laea +x_0=0 +y_0=0 +lon_0=145 +lat_0=-38")

ggplot(aus_map_transformed) +
  geom_sf(col = "forestgreen")

Wrap Up

Summary

Australian Election Case Study

  • We went through how to find and use data from the Australian election

  • This included how to deal with preferential voting counts

  • Learnt about how to plot maps in R using the sf package

  • This required learning about coordinate reference systems

Questions

Drop In - Getting started in ggplot2

*Notes from Kate’s unit on data Visualisation and Communication ETX2250/ETF5922

Shrek

Shrek and ggplot2

Don’t be scared of ggplot2, it’s just like Shrek!

Get to know it before you judge it!

“Ogres have layers. Onions have layers. You get it? We both have layers” - Shrek

The Layers

Key layers include:

  • Data:
    • The dataset you’re visualising.
  • Aesthetic Mappings (aes() for short):
    • Map variables to visual properties like x, y, color, size, etc.
  • Geometries (geom_*):
    • Define the type of plot (e.g., bars, lines, points).
  • Scales:
    • Control how data maps to aesthetics (e.g., axis limits, color gradients).
  • Facets:
    • Split the data into multiple panels (e.g., facet_wrap()).
  • Themes:
    • Customise the non-data components (e.g., background, grid lines).

Base Layer

Start by creating an empty plot on which to add your layers. We’ll add layers to this plot using +.

library(ggplot2)

ggplot()

Data Layer

Note

  • First step is to add our data

  • I’m going to use this data set I prepared earlier

boston_celtics = read_csv(here::here("data/boston_celtics.csv")) |> 
   filter(season == 2025)
head(boston_celtics)
# A tibble: 6 × 57
    game_id season season_type game_date  game_date_time      team_id team_uid  
      <dbl>  <dbl>       <dbl> <date>     <dttm>                <dbl> <chr>     
1 401703390   2025           2 2024-11-19 2024-11-20 00:00:00       2 s:40~l:46…
2 401704796   2025           2 2024-11-16 2024-11-17 01:00:00       2 s:40~l:46…
3 401704784   2025           2 2024-11-13 2024-11-14 00:30:00       2 s:40~l:46…
4 401703370   2025           2 2024-11-12 2024-11-13 00:00:00       2 s:40~l:46…
5 401704768   2025           2 2024-11-10 2024-11-10 20:30:00       2 s:40~l:46…
6 401704753   2025           2 2024-11-08 2024-11-09 00:30:00       2 s:40~l:46…
# ℹ 50 more variables: team_slug <chr>, team_location <chr>, team_name <chr>,
#   team_abbreviation <chr>, team_display_name <chr>,
#   team_short_display_name <chr>, team_color <chr>,
#   team_alternate_color <chr>, team_logo <chr>, team_home_away <chr>,
#   team_score <dbl>, team_winner <lgl>, assists <dbl>, blocks <dbl>,
#   defensive_rebounds <dbl>, fast_break_points <dbl>, field_goal_pct <dbl>,
#   field_goals_made <dbl>, field_goals_attempted <dbl>, …

Reading Data

Checklist

  • Remember you need to tell R where to look for this file by setting your working directory

  • Setting up a R Project helps with this!

  • Check you current working directory with getwd()

  • Check your file is in this directory - file.exists or list.files() are helpful for this.

  • After your read in your data look at it using View() of head()

  • Make sure it looks like what you expect

  • Also check the structure of your data. Try summary or str().

  • If all that seems good - we can add it to our plot!

Add you data layer

It’s still an empty plot because we haven’t told R what to do with the data yet.

ggplot(data = boston_celtics) 

Geometry Layer (geom)

geom

  • Let’s create a coloumn plot

  • Use the geometry layer - geom_col

  • Similar to geom_bar (but does slightly different things)

  • If you type ?geom_ in your Console and hit tab to scroll through a list of all the different plot geometries

  • Think of all these types is like the Visualisation Pane in Power BI

Bar Plot

Add your geom

This is what your code should look like when you add your geom layer

ggplot(data = boston_celtics) + 
  geom_col()

Warning

  • But … this code won’t work yet, because we haven’t added our aesthetic layer

  • The aesthetic layer defines how data is mapped to visual properties in your plot

    • e.g what goes on the x/y axes

Aesthetic Layer

Common Aesthetic Mappings

Use the aes() function to map variables to aesthetics.

The common parts are:

  • x: The variable on the x-axis.

  • y: The variable on the y-axis.

  • color: The color of points, lines, or outlines.

  • fill: The fill color for bars, areas, or shapes.

  • size: The size of points or lines.

  • shape: The shape of points (e.g., circles, triangles).

  • alpha: The transparency level.

Adding the aesthetic layer

ggplot(data = boston_celtics, aes(x = game_date, y = team_score)) + 
  geom_col() 

Another Option

Important

If you are going to use multiple data types or need multiple aesthetics layers it is better to put the code about the data and the aesthetics in the geom layer directly.

ggplot() + 
  geom_col(data = boston_celtics, aes(x = game_date, y = team_score)) 

Colour and Fill

Set the bar colour to Green

ggplot() + 
  geom_col(data = boston_celtics, aes(x = game_date, y = team_score), col = "forestgreen") 

Colour and Fill

Set the bar fill to Green

ggplot() + 
  geom_col(data = boston_celtics, aes(x = game_date, y = team_score), fill = "forestgreen") 

Scale Layer

Next layer is the visual elements is scale. e.g. axis limits and color scales

ggplot(data = boston_celtics) + 
  geom_col(aes(x = game_date, y = team_score, fill = team_winner))

Colour and Fill In Code

Common misunderstandings

  • If it the asethetic mapping is the name of a variable then you need to put it in the the aes() brackets

  • If it is fixed, e.g. you want to colour everything black, then it is just in the geom_*() bracket.

  • Depending on what geom you use, there may be a difference between colour and fill

  • Both spellings of colour and color will work

Using In Built Fill/Colour Scales

You can use the inbuilt palettes from RColourBrewer

ggplot(data = boston_celtics) + 
  geom_col(aes(x = game_date, y = team_score, fill = team_winner), col = "gray") + 
  scale_fill_brewer(palette = "Greens") 

Using Manual Fill/Colour Scales

You can also change fill/colour scales manually using scale_colour_manual or scale_fill_manual.

ggplot(data = boston_celtics) + 
  geom_col(aes(x = game_date, y = team_score, fill = team_winner), col = "gray") +
  scale_fill_manual(label = c("Loss", "Win"), values = c("TRUE" = "forestgreen", "FALSE" = "lightgreen"))

Colour Scales

Colour Scales

IMO: Colour scales are on the hardest parts about learning ggplot2

  • To change colour scale, use scale_colour_*

  • To change fill scale, use scale_fill_*

  • Check out all the different types of scales using the help menu ?scale_colour and hit tab.

  • Note for discrete variables needing distinct colours, such as categorical variables, you can use scale_*_brewer

  • For variables needing a smooth gradient use scale_*_distiller

  • You can also set colours manually using scale_*_manual

Note * here is like a blank space and it means there are multiple things that could be inserted here

Themes

Themes

  • Here is a list of the themes.

  • My favourite is theme_bw().

Default Theme

Grey Background

  • The default background for ggplot2 is arguably chartjunk.

  • But - There are good reasons for using it.

“We can still see the gridlines to aid in the judgement of position (Cleveland, 1993b), but they have little visual impact and we can easily”tune” them out… Finally, the grey background creates a continuous field of colour which ensures that the plot is perceived as a single visual entity.” -Wickham on the grey background
Source: ggplot2: Elegant Graphics for Data Analysis.

Changing Theme Background

Here I change the theme background to theme_bw().

ggplot(data = boston_celtics) + 
  geom_col(aes(x = game_date, y = team_score, fill = team_winner), col = "gray") +
  scale_fill_manual(label = c("Loss", "Win"), values = c("TRUE" = "forestgreen", "FALSE" = "lightgreen")) + 
  theme_bw() 

Plot Theme Specifics

Plot Theme Specifics

  • To tune the more specific aspects of your theme, we use the theme() layer.

  • Look up ?theme there are a lot of options!

Changing Theme Specifics

Here I move the legend to the bottom and remove the legend label.

ggplot(data = boston_celtics) + 
  geom_col(aes(x = game_date, y = team_score, fill = team_winner), col = "gray") +
  scale_fill_manual(label = c("Loss", "Win"), values = c("TRUE" = "forestgreen", "FALSE" = "lightgreen")) + 
  theme_bw() + 
  theme(legend.position = "bottom", legend.title = element_blank())

Polising your plot

The theme() layer is also were you can specifics about titles, text and axes. You could also change label names in the theme, but xlab, ylab and ggtitle are easier to use.

celtics_plot <- ggplot(data = boston_celtics) + 
  geom_col(aes(x = game_date, y = team_score, fill = team_winner), col = "gray") +
  scale_fill_manual(label = c("Loss", "Win"), values = c("TRUE" = "forestgreen", "FALSE" = "lightgreen")) + 
  theme_bw() + 
  theme(
    legend.position = "bottom", 
    legend.title = element_blank(),
    plot.title = element_text(size = 20), 
    axis.title.x = element_text(size = 15),  
    axis.title.y = element_text(size = 15),  
    axis.text = element_text(size = 12),      
    legend.text = element_text(size = 12)) + 
  xlab("Game Date") + 
  ylab("Team Score") + 
  ggtitle("Boston Celtics 2025 Season so far")

Final plot

celtics_plot