Intro to R Course
  • Prepare for the course
  • Copyright
  • Practical Sessions
  • Resources
  • Source Code
  • Report an issue
  1. Session 5 - ggplot2
  2. Scatterplot - your first plot
  • Welcome
  • Session 1 - Basics of R
    • Getting familiar with RStudio
    • Setting up your Workspace
    • Functions that make the work
  • Session 2 - Tidyverse
    • Data manipulation using the Tidyverse
    • Logical conditions and Tidy
    • Creating variables
    • Grouping and summarising
  • Session 3 - Data Cleaning
    • Intro to Data Cleaning
    • Variable Class
    • Recoding variables
    • Derived Variables & Export
  • Session 4 - Tables
    • Counting cases
    • Crosstabulations and richer tables
    • Tables of things you cannot count
    • The whole table in one line
  • Session 5 - ggplot2
    • Scatterplot - your first plot
    • Barplots - elemental count
    • Lines - tracking trends
    • Histograms for Epicurves
  • Session 6 - Use of AI
    • The teaching assistant
    • The design assistant

On this page

  • Before you start
  • Part 1 · The many brushes
  • Part 2 · Aesthetics for our plot
    • Static values
    • Dynamic values
  • Part 3 · A little scale
  • Part 4 · Final framing
  • Final plot
  • Saving your plot
  • Exercise Summary
  1. Session 5 - ggplot2
  2. Scatterplot - your first plot

Scatterplot - your first plot

Session 5 practical exercises

Communication in field epidemiology is crucial for sharing the results of any project. Surveillance, research, outbreaks… they all rely on our capability of explaining complex information in a simple, understandable and striking way so that our message can be remembered.

R is amazing in doing so. Probably one of its strongest features. But with great power comes… a big difficulty in understanding and using all the possibilities the grammar of graphics has to offer us. We are here to guide you through your first steps, thanks to a fundamental aspect of ggplot: its modularity. Every plot is built following the same grammar model, so for you now the most important skill is understanding how a plot is designed. You will have plenty of time later to master those skills and create the most amazing graphs.

Kassandra is very well aware of the importance of graphs and communication, of course. Yesterday’s tables were only the appetizer, and now you are staring at her mail asking you to assist in the elaboration of a formal surveillance report of the past four seasons of IMD, to set up a new baseline for comparison of future trends.

Attached to it, you find the draft of the report with the sections and baseline info they want to include. Your task is creating the supporting graphs, for which you are given precise instructions on what to create - which takes most of the headache of making such decision. We will create those graphs across the exercises, make them beautiful and save them in high quality.

Before you start

Start a new script for Session 5 named after today’s topic — keep the habit of one script per task. Today you will only have a single script, but keep in mind you may want a script per graph in the future.

Load your packages (ggplot is part of the tidyverse) and import the clean dataset, the same one you already used throughout Session 4. Notice we are importing the .rds version, because we want to preserve all the format we need for producing nice outputs.

This first exercise is deliberately light. No new concept is hiding at the bottom of it — the goal is simply to get your hands used to the grammar before Kassandra starts asking for real graphs. Let’s follow the painter metaphor

Part 1 · The many brushes

Back in the lecture we have been exploring the distribution of the age of cases across sex, as a case study. The first plot we built was a boxplot of age_years by sex. Let’s start from exactly the same place and replicate the slides, step by step:

Action — Open for first blank canvas with ggplot() and pipe in the imd data. Run and see for yourself.

Action — Add your first aes() call, define x and y axis there. Is there any consequence in placing age_years and sex on one or the other axis? Run it and see for yourself again.

Action — Choose your brush-geom, a boxplot in this case.

Try to write the code yourself. See how much of the slide you can remember and replicate on the first attempt. Don’t worry if you find it difficult, that’s the point now. It will be piece of cake in two hours, believe me.

Expand the box below to find the code you will be replicating a hundred times from now on:

Hint
# Pipe the plot data
imd %>%
  # Open white canvas
  ggplot(aes(
    # Define aesthetics: x and y axis
    x = ___,
    y = ___
  )) +
  # choose your brush
  geom_boxplot()

Action — Without touching aes() at all, swap geom_boxplot() for geom_violin(). Then swap it again for geom_point(). Then for geom_jitter().

What just happened

Look back at the four lines of aes() you wrote. Did you change a single character in any of them? The only thing moving was the name of the geom_*() function. That is the modularity of the grammar: what gets plotted and how it gets drawn are two separate decisions, and you can swap one without touching the other.

geom_point() and geom_jitter() show exactly the same data, but sex only has three possible values, so a lot of cases share the exact same age. Without jitter, those points stack perfectly on top of each other and you have no way of telling ten cases from one. From here on, we work with geom_jitter().

Missing data and plots

We know — because we cleaned it ourselves, that sex,serogroup, and death all contain missing (NA) values. Reporting and plotting are two different tasks, meaning that a table should transparently report on those values most of the times, while a plot can benefit from erasing them. These are delicate choices that have an impact, and should be agreed within the team and your supervisors beforehand.

For this exercise, we will get rid of missing values, equivalent to reporting only about known cases. In a real scenario, you would state that in a comment below each graph or inline within the main body text.

Part 2 · Aesthetics for our plot

Static values

You can tell geom_jitter() to use a fixed color, size and transparency for every single point, regardless of what that point represents. These three go outside aes(), because they are not connected to any column in your data — just a styling choice, the same for every row.

Action — Filter away missing sexvalues before piping data into the ggplot() call.

Action — Set the points to color = "steelblue", size = 2 and alpha = 0.6. Keep the vertical, one-argument-per-line style. Also, play with the 2 and 0.6 values to see how they change the plot. The alpha can range between 0-1 only.

However, it doesn’t feel natural coloring all dots the same, right? Ideally it would be one color per category

Dynamic values

Now we want the color to stop being a fixed decision and start representing something, be it another variable or the same sex to define one color per sex.

Action — Move color inside aes() and map it to sex. Leave size and alpha exactly where they are.

Action — Play around with other coloring variables to see what happens. But retain sex for continuing the exercise.

Why do size and alpha stay outside aes() while color moves inside?

Part 3 · A little scale

Let’s fix the age_years axis appearance by changing the breaks — the little numbers in the axis indicating the range of values represented, like we did in the presentation:

Action — Add scale_y_continuous() with the arguments name and breaks

Action — Use either a vector c(0, 5, 15, ...) with your breaks of choice or the seq(from, to, by) function specifying the initial and final values, and the width of each jump i.e., from 0 to 90 years with 10 years jumps.

Tell me something: is the legend in this graph really necessary? Is it contributing information that the labels of your sex axis is not providing? Not really. We only used the color aesthetic because we wanted one separate color for males and females. Can we get rid of that? Well, of course.

Action — Just add this scale to your plot code:

scale_colour_discrete(guide = "none")
<ggproto object: Class ScaleDiscrete, Scale, gg>
    aesthetics: colour
    axis_order: function
    break_info: function
    break_positions: function
    breaks: waiver
    call: call
    clone: function
    dimension: function
    drop: TRUE
    expand: waiver
    fallback_palette: function
    get_breaks: function
    get_breaks_minor: function
    get_labels: function
    get_limits: function
    get_transformation: function
    guide: none
    is_discrete: function
    is_empty: function
    labels: waiver
    limits: NULL
    make_sec_title: function
    make_title: function
    map: function
    map_df: function
    minor_breaks: waiver
    n.breaks.cache: NULL
    na.translate: TRUE
    na.value: grey50
    name: waiver
    palette: NULL
    palette.cache: NULL
    position: left
    range: environment
    rescale: function
    reset: function
    train: function
    train_df: function
    transform: function
    transform_df: function
    super:  <ggproto object: Class ScaleDiscrete, Scale, gg>
  • Modify the color scale which is a discrete (categorical) variable

  • Modify the guide (the legend)

  • Erase it guide = "none"

Part 4 · Final framing

Now, the plot started looking quite good! Still, that grey default background is not very appalling. All the non-data details of the graph are customized through the theme() function of ggplot. But it’s a really complicated set of arguments and values to define, so we will just use the default themes that ggplot provide — and most people use for their plots, believe me.

Action — Finalise the plot code with a default call to any theme: theme_minimal(), bw or classic

From this exercise onward, we will be adding one of the themes at the end of every plot - it’s up to you which one you prefer of fits better with the graph.

Final plot

Your final plot should look like this. If you chose to map different x or y axis it’s more than ok, and also a way of realizing how choices in here don’t result in error messages but in different graphs — and yes, sometime also in errors

Saving your plot

Close the exercise the way you will close most of your plotting scripts from now on: with ggsave().

We haven’t assigned yet the plot to any object. Strictly speaking, it’s not necessary for saving, but when creating multiple plots at once it’s better practice to do like this:

Action — Assign the plot to an object jitter_plot, for example. What is its class()? How does it look in the environment? What happens when you call it again?

Action — Fill in the blanks below and save your plot in your outputs folder. You will need to play with the width and height values. Because of the vectorial nature of R, “bigger” saved plots (i.e 10 x 10) will result in “smaller” looking elements like text labels and dots. On the contrary, “smaller” saved plots (i.e., 5x5) will result in zoomed-in like output. Play around with the numbers and find out for yourself.

ggsave(
  plot     = ___,  # the plot object
  filename = ___,  # don't forget the extension .png, .jpeg, .tiff, etc.
  path     = ___,  # a call with here()
  units    = "in",
  width    = ___,
  height   = ___,
  dpi      = 300   # for a high-quality output
)
Hint

ggsave() saves the last plot you drew by default if you don’t specify the plot argument, so you can run it right after your final plot without assigning anything first. filename needs its extension (.png, for instance); path is the folder you want it saved into.


Exercise Summary

You just built your first complete ggplot2 chain from scratch — same aes(), four different brushes, then a static style turned into a data-driven one. Scales for better breaks in the age axis, and erasing the legend of the sex coloring. All finally styled with a default theme before saving.

Nothing here required a new concept; it required getting your hands used to the pattern. In E2 the geometries start to bite back, with a real error message waiting for you.

Function Package What it does
ggplot() ggplot2 Opens the canvas: sets the dataset and, optionally, the global aes()
aes() ggplot2 Declares which variables are mapped, and where
geom_jitter() ggplot2 Draws individual points, nudging apart the ones that overlap
color, size, alpha (outside aes()) ggplot2 Fixed style, identical for every point
color, size, alpha (inside aes()) ggplot2 Represent a variable from the data
scale_x_continuous(), scale_y_continuous() ggplot2 Modify x/y axis when a continuous variable is supplied
scale_color_discrete() ggplot2 Modify the color aesthetic inside aes() when a categorical variable is mapped
theme_minimal() ggplot2 Applies a predefined visual theme
ggsave() ggplot2 Saves the last plot drawn to a file
💡 Show solution — only after trying yourself!
# Load libraries
library(pacman)
p_load(rio, here, tidyverse)

# Import data
imd <- import(here("data", "clean", "IMD_Sample_Clean.rds"))

# Same aes(), different brush
imd %>%
  ggplot(aes(
    x = sex,
    y = age_years
  )) +
  geom_boxplot()

imd %>%
  ggplot(aes(
    x = sex,
    y = age_years
  )) +
  geom_violin()

imd %>%
  ggplot(aes(
    x = sex,
    y = age_years
  )) +
  geom_point()

imd %>%
  ggplot(aes(
    x = sex,
    y = age_years
  )) +
  geom_jitter()

# Static style
imd %>%
  filter(!is.na(sex)) %>% 
  ggplot(aes(
    x = sex,
    y = age_years
  )) +
  geom_jitter(
    color = "steelblue",
    size = 2,
    alpha = 0.6
  )

# Dynamic style 
imd %>%
  filter(!is.na(sex)) %>% 
  ggplot(aes(
    x = sex,
    y = age_years,
    color = sex
  )) +
  geom_jitter(
    size = 2,
    alpha = 0.6
  )

# Scaling
imd %>%
  filter(!is.na(sex)) %>% 
  ggplot(aes(
    x = sex,
    y = age_years,
    color = sex
  )) +
  geom_jitter(
    size = 2,
    alpha = 0.6
  ) +
  scale_y_continuous(
    name = "Age (Years)", 
    breaks = seq(0, 90, 10)
  ) +
  scale_colour_discrete(guide = "none")


# Final plot
jitter_plot <- imd %>%
  filter(!is.na(sex)) %>% 
  ggplot(aes(
    x = sex,
    y = age_years,
    color = sex
  )) +
  geom_jitter(
    size = 2,
    alpha = 0.6
  )+
  scale_y_continuous(
    name = "Age (Years)", 
    breaks = seq(0, 90, 10)
  ) +
  scale_colour_discrete(guide = "none") +
  theme_minimal()

ggsave(
  plot     = jitter_plot,
  filename = "age_sex_jitter.png",
  path     = here("outputs"),
  units    = "in",
  width    = 7,
  height   = 5,
  dpi      = 300
)
The whole table in one line
Barplots - elemental count
Source Code
---
title: "Scatterplot - your first plot"
subtitle: "Session 5 practical exercises"
---

```{r}
#| include: false
library(webexercises)
library(pacman)
p_load(rio, here, tidyverse, janitor)

imd <- import(here("data", "clean", "IMD_Sample_Clean.rds"))
```

Communication in field epidemiology is crucial for sharing the results of any project. Surveillance, research, outbreaks... they all rely on our capability of explaining complex information in a simple, understandable and striking way so that our message can be remembered.

R is amazing in doing so. Probably one of its strongest features. But with great power comes... a big difficulty in understanding and using all the possibilities the ***grammar of graphics*** has to offer us. We are here to guide you through your first steps, thanks to a fundamental aspect of ggplot: its modularity. Every plot is built following the same grammar model, so for you now the most important skill is understanding how a plot is designed. You will have plenty of time later to master those skills and create the most amazing graphs.

Kassandra is very well aware of the importance of graphs and communication, of course. Yesterday's tables were only the appetizer, and now you are staring at her mail asking you to assist in the elaboration of a formal surveillance report of the past four seasons of IMD, to set up a new baseline for comparison of future trends.

Attached to it, you find the draft of the report with the sections and baseline info they want to include. Your task is creating the supporting graphs, for which you are given precise instructions on what to create - which takes most of the headache of making such decision. We will create those graphs across the exercises, make them beautiful and save them in high quality.

## Before you start

Start a new script for Session 5 named after today's topic — keep the habit of one script per task. Today you will only have a single script, but keep in mind you may want a script per graph in the future.

Load your packages (ggplot is part of the tidyverse) and import the clean dataset, the same one you already used throughout Session 4. Notice we are importing the `.rds` version, because we want to preserve all the format we need for producing nice outputs.

This first exercise is deliberately light. No new concept is hiding at the bottom of it — the goal is simply to get your hands used to the grammar before Kassandra starts asking for real graphs. Let's follow the painter metaphor

## Part 1 · The many brushes

![](/images/exercises/S5E1_1.png){fig-align="center" width="391"}

Back in the lecture we have been exploring the distribution of the age of cases across sex, as a case study. The first plot we built was a boxplot of `age_years` by `sex`. Let's start from exactly the same place and replicate the slides, step by step:

**Action** — Open for first blank canvas with `ggplot()` and pipe in the `imd` data. Run and see for yourself.

**Action** — Add your first `aes()` call, define `x` and `y` axis there. Is there any consequence in placing `age_years` and `sex` on one or the other axis? Run it and see for yourself again.

**Action** — Choose your brush-geom, a boxplot in this case.

Try to write the code yourself. See how much of the slide you can remember and replicate on the first attempt. Don't worry if you find it difficult, that's the point now. It will be piece of cake in two hours, believe me.

Expand the box below to find the code you will be replicating a hundred times from now on:

::: {.callout-tip collapse="true" appearance="simple" icon="false"}
## Hint

``` r
# Pipe the plot data
imd %>%
  # Open white canvas
  ggplot(aes(
    # Define aesthetics: x and y axis
    x = ___,
    y = ___
  )) +
  # choose your brush
  geom_boxplot()
```
:::

**Action** — Without touching `aes()` at all, swap `geom_boxplot()` for `geom_violin()`. Then swap it again for `geom_point()`. Then for `geom_jitter()`.

::: callout-note
## What just happened

Look back at the four lines of `aes()` you wrote. Did you change a single character in any of them? The only thing moving was the name of the `geom_*()` function. That is the modularity of the grammar: *what* gets plotted and *how* it gets drawn are two separate decisions, and you can swap one without touching the other.
:::

`geom_point()` and `geom_jitter()` show exactly the same data, but `sex` only has three possible values, so a lot of cases share the exact same age. Without jitter, those points stack perfectly on top of each other and you have no way of telling ten cases from one. From here on, we work with `geom_jitter()`.

::: callout-caution
## Missing data and plots

We know — because we cleaned it ourselves, that `sex`,`serogroup`, and `death` all contain missing (`NA`) values. Reporting and plotting are two different tasks, meaning that a table should transparently report on those values most of the times, while a plot can benefit from erasing them. These are delicate choices that have an impact, and should be agreed within the team and your supervisors beforehand.

For this exercise, we will get rid of missing values, equivalent to reporting only about known cases. In a real scenario, you would state that in a comment below each graph or inline within the main body text.
:::

## Part 2 · Aesthetics for our plot

### Static values

You can tell `geom_jitter()` to use a fixed color, size and transparency for every single point, regardless of what that point represents. These three go **outside** `aes()`, because they are not connected to any column in your data — just a styling choice, the same for every row.

::: column-margin
![](/images/exercises/S5E1_2.png){.lightbox width="95"}
:::

**Action** — Filter away missing `sex`values before piping data into the `ggplot()` call.

**Action** — Set the points to `color = "steelblue"`, `size = 2` and `alpha = 0.6`. Keep the vertical, one-argument-per-line style. Also, play with the 2 and 0.6 values to see how they change the plot. The `alpha` can range between 0-1 only.

However, it doesn't feel natural coloring all dots the same, right? Ideally it would be one color per category

### Dynamic values

Now we want the color to stop being a fixed decision and start **representing** something, be it another variable or the same `sex` to define one color per sex.

**Action** — Move `color` inside `aes()` and map it to `sex`. Leave `size` and `alpha` exactly where they are.

**Action** — Play around with other coloring variables to see what happens. But retain `sex` for continuing the exercise.

```{r}
#| echo: false
opts1 <- c(
  "Because `size` and `alpha` only accept numeric variables, and `sex` is categorical",
  answer = "Because `size` and `alpha` are still a fixed styling choice — they are not representing any variable, so they stay outside `aes()`",
  "Because a geometry can only have one dynamic aesthetic at a time",
  "Because `color` is the only aesthetic allowed inside `aes()`"
)
```

**Why do `size` and `alpha` stay outside `aes()` while `color` moves inside?**

`r longmcq(opts1)`

## Part 3 · A little scale

Let's fix the `age_years` axis appearance by changing the breaks — the little numbers in the axis indicating the range of values represented, like we did in the presentation:

::: column-margin
![](/images/exercises/S5E1_3.png){.lightbox width="132"}
:::

**Action** — Add `scale_y_continuous()` with the arguments `name` and `breaks`

**Action** — Use either a vector `c(0, 5, 15, ...)` with your breaks of choice or the `seq(from, to, by)` function specifying the initial and final values, and the width of each jump i.e., from 0 to 90 years with 10 years jumps.

Tell me something: is the legend in this graph really necessary? Is it contributing information that the labels of your `sex` axis is not providing? Not really. We only used the `color` aesthetic because we wanted one separate color for males and females. Can we get rid of that? Well, of course.

**Action** — Just add this scale to your plot code:

```{r}
scale_colour_discrete(guide = "none")
```

-   Modify the `color` scale which is a discrete (categorical) variable

-   Modify the `guide` (the legend)

-   Erase it `guide = "none"`

## Part 4 · Final framing

Now, the plot started looking quite good! Still, that grey default background is not very appalling. All the non-data details of the graph are customized through the `theme()` function of ggplot. But it's a really complicated set of arguments and values to define, so we will just use the default themes that ggplot provide — and most people use for their plots, believe me.

::: column-margin
![](/images/exercises/S5E1_4.png){.lightbox width="130"}
:::

**Action** — Finalise the plot code with a default call to any theme: `theme_minimal()`, `bw` or `classic`

From this exercise onward, we will be adding one of the themes at the end of every plot - it's up to you which one you prefer of fits better with the graph.

## Final plot

Your final plot should look like this. If you chose to map different `x` or `y` axis it's more than ok, and also a way of realizing how choices in here don't result in error messages but in different graphs — and yes, sometime also in errors

```{r}
#| echo: false
#| message: false
#| warning: false
# Final plot
imd %>%
  filter(!is.na(sex)) %>% 
  ggplot(aes(
    x = sex,
    y = age_years,
    color = sex
  )) +
  geom_jitter(
    size = 2,
    alpha = 0.6
  )+
  scale_y_continuous(
    name = "Age (Years)", 
    breaks = seq(0, 90, 10)
  ) +
  scale_colour_discrete(guide = "none") +
  theme_minimal()
```

## Saving your plot

Close the exercise the way you will close most of your plotting scripts from now on: with `ggsave()`.

We haven't assigned yet the plot to any object. Strictly speaking, it's not necessary for saving, but when creating multiple plots at once it's better practice to do like this:

**Action** — Assign the plot to an object `jitter_plot`, for example. What is its `class()`? How does it look in the environment? What happens when you call it again?

**Action** — Fill in the blanks below and save your plot in your outputs folder. You will need to play with the width and height values. Because of the vectorial nature of R, "*bigger*" saved plots (i.e 10 x 10) will result in "smaller" looking elements like text labels and dots. On the contrary, "*smaller*" saved plots (i.e., 5x5) will result in zoomed-in like output. Play around with the numbers and find out for yourself.

``` r
ggsave(
  plot     = ___,  # the plot object
  filename = ___,  # don't forget the extension .png, .jpeg, .tiff, etc.
  path     = ___,  # a call with here()
  units    = "in",
  width    = ___,
  height   = ___,
  dpi      = 300   # for a high-quality output
)
```

::: {.callout-tip collapse="true" appearance="simple" icon="false"}
## Hint

`ggsave()` saves the last plot you drew by default if you don't specify the `plot` argument, so you can run it right after your final plot without assigning anything first. `filename` needs its extension (`.png`, for instance); `path` is the folder you want it saved into.
:::

------------------------------------------------------------------------

## Exercise Summary

![](/images/exercises/S5E1_5.png)

You just built your first complete `ggplot2` chain from scratch — same `aes()`, four different brushes, then a static style turned into a data-driven one. Scales for better breaks in the age axis, and erasing the legend of the sex coloring. All finally styled with a default theme before saving.

Nothing here required a new concept; it required getting your hands used to the pattern. In E2 the geometries start to bite back, with a real error message waiting for you.

| Function | Package | What it does |
|------------------------|------------------------|------------------------|
| `ggplot()` | ggplot2 | Opens the canvas: sets the dataset and, optionally, the global `aes()` |
| `aes()` | ggplot2 | Declares which variables are mapped, and where |
| `geom_jitter()` | ggplot2 | Draws individual points, nudging apart the ones that overlap |
| `color`, `size`, `alpha` (outside `aes()`) | ggplot2 | Fixed style, identical for every point |
| `color`, `size`, `alpha` (inside `aes()`) | ggplot2 | Represent a variable from the data |
| `scale_x_continuous(), scale_y_continuous()` | ggplot2 | Modify x/y axis when a continuous variable is supplied |
| `scale_color_discrete()` | ggplot2 | Modify the color aesthetic inside `aes()` when a categorical variable is mapped |
| `theme_minimal()` | ggplot2 | Applies a predefined visual theme |
| `ggsave()` | ggplot2 | Saves the last plot drawn to a file |

::: {.callout-tip collapse="true"}
## 💡 Show solution — only after trying yourself!

``` r
# Load libraries
library(pacman)
p_load(rio, here, tidyverse)

# Import data
imd <- import(here("data", "clean", "IMD_Sample_Clean.rds"))

# Same aes(), different brush
imd %>%
  ggplot(aes(
    x = sex,
    y = age_years
  )) +
  geom_boxplot()

imd %>%
  ggplot(aes(
    x = sex,
    y = age_years
  )) +
  geom_violin()

imd %>%
  ggplot(aes(
    x = sex,
    y = age_years
  )) +
  geom_point()

imd %>%
  ggplot(aes(
    x = sex,
    y = age_years
  )) +
  geom_jitter()

# Static style
imd %>%
  filter(!is.na(sex)) %>% 
  ggplot(aes(
    x = sex,
    y = age_years
  )) +
  geom_jitter(
    color = "steelblue",
    size = 2,
    alpha = 0.6
  )

# Dynamic style 
imd %>%
  filter(!is.na(sex)) %>% 
  ggplot(aes(
    x = sex,
    y = age_years,
    color = sex
  )) +
  geom_jitter(
    size = 2,
    alpha = 0.6
  )

# Scaling
imd %>%
  filter(!is.na(sex)) %>% 
  ggplot(aes(
    x = sex,
    y = age_years,
    color = sex
  )) +
  geom_jitter(
    size = 2,
    alpha = 0.6
  ) +
  scale_y_continuous(
    name = "Age (Years)", 
    breaks = seq(0, 90, 10)
  ) +
  scale_colour_discrete(guide = "none")


# Final plot
jitter_plot <- imd %>%
  filter(!is.na(sex)) %>% 
  ggplot(aes(
    x = sex,
    y = age_years,
    color = sex
  )) +
  geom_jitter(
    size = 2,
    alpha = 0.6
  )+
  scale_y_continuous(
    name = "Age (Years)", 
    breaks = seq(0, 90, 10)
  ) +
  scale_colour_discrete(guide = "none") +
  theme_minimal()

ggsave(
  plot     = jitter_plot,
  filename = "age_sex_jitter.png",
  path     = here("outputs"),
  units    = "in",
  width    = 7,
  height   = 5,
  dpi      = 300
)
```
:::

```{=html}
<script>
document.addEventListener("DOMContentLoaded", function() {
  var radiogroups = document.getElementsByClassName("webex-radiogroup");
  for (var i = 0; i < radiogroups.length; i++) {
    radiogroups[i].onchange = radiogroups_func;
  }
});
</script>
```

© 2026 – Intro to R Course