Intro to Data Visualization in R

A 2-hour, hands-on introduction to making plots in R with ggplot2, using the Palmer Penguins dataset. For RISE 2026 summer researchers — no prior R needed.

Set up your environment

Everything runs in the cloud with GitHub Codespaces — there is nothing to install on your laptop. You only need a web browser and a free GitHub account. No API keys required.

1. Sign in to GitHub

Go to github.com/login and sign in, or create a free account if you don’t have one.

This matters because GitHub hides the “Use this template” and “Code” buttons from signed-out visitors — if you’re not logged in, the buttons in the next steps won’t appear.

2. Make your own copy of the environment

Open the participant environment repo and click the green “Use this template” button near the top right, then choose “Create a new repository”.

GitHub repo page with the 'Use this template' button highlighted in the top right

Click “Use this template” (your screen will show the r-tutorial-rise-2026 repo name).

Use this template dropdown with 'Create a new repository' highlighted

Choose “Create a new repository”.

Give the repository a name (for example, rise-data-viz) and click the green “Create repository” button. The owner can stay as your username.

New repository form with the Repository name field highlighted

Name your repository and create it.

3. Launch your codespace

Go to your new repository’s page. Click the green “Code” button, switch to the “Codespaces” tab, and click “Create codespace on main”.

Repository page with the Code button and Create codespace on main button highlighted in the Codespaces tab

Code → Codespaces → Create codespace on main.

The first build takes a few minutes while GitHub sets up R, the tidyverse, and Quarto for you. After this first build, reopening the codespace is fast.

4. Open the first notebook

Once the codespace has finished building:

  1. In the file explorer on the left, open notebooks/00-hello.qmd.
  2. Click the green play button on the code cell. A penguin chart should appear — that means your environment is working and you are ready.
  3. When we start the session, open notebooks/01-visualization.qmd and work through it with the class.

If you get stuck, ask an instructor.

▲ Collapse these steps

Slides

Open the slides full screen

The datasets

Most of today uses palmerpenguins: measurements of 344 penguins from three species (Adelie, Chinstrap, Gentoo) across three islands. Each row is one penguin; each column is something measured about it (flipper length, body mass, species, island, …).

In the last part we switch to a small plate-reader result — four genes measured in ten samples — to see what real machine data looks like and how to reshape it for plotting.

The session runs in three short cycles of learn a little, then try it:

  • Part A — the elements of a plot: geom, mark, mapping
  • Part B — distributions: histograms and boxplots
  • Part C — tidy data: reshaping wide machine data with pivot_longer()

Hands-on worksheet

You’ll do the real work in your Codespace, in notebooks/01-visualization.qmd. This page lists the tasks. Try each one yourself first — the collapsible “Show an example” boxes are hints to peek at only if you get stuck.

Each task has a number so we can point to it out loud. The first number is the exercise and the second is the task — so 2.3 means “Exercise 2, task 3.”

Exercise 1 — The elements of a plot

Every row in a table becomes one mark on a plot. The geom decides what kind of mark; the mapping (written inside aes()) links a column to a property of the mark. We use the palmerpenguins data.

1.1 — Read a plot. Look at the plot below and answer in your own words: (a) What is the mark? (b) Which column controls x, y, and colour? (c) How many rows of the table made one mark?

  • Mark: a dot. Mappings: flipper_length_mm → x, body_mass_g → y, species → colour. Rows per mark: one row = one dot.

1.2 — Build a scatter. Make a scatter plot of flipper_length_mm (x) against body_mass_g (y) using geom_point().

ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point()

1.3 — Colour by a column. Add color = species inside aes() so each species gets its own colour.

ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g, color = species)) +
  geom_point()

1.4 — Mapping vs. setting. Make all the dots the same fixed colour (say, navy). Does color = "navy" go inside aes() or outside? Why?

It goes outside aes(), inside the geom — it’s a fixed value, not a column mapping:

ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point(color = "navy")

1.5 — Change the geom. Compare body mass across species with a boxplot: map species to x, body_mass_g to y, and use geom_boxplot().

ggplot(penguins, aes(x = species, y = body_mass_g, fill = species)) +
  geom_boxplot()

1.6 — Filter with the pipe. Use |> to keep only one species (your choice), then make a scatter of just those penguins.

penguins |>
  filter(species == "Gentoo") |>
  ggplot(aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point()

Exercise 2 — The shape of one variable

A histogram cuts one numeric column into bins and counts the rows in each. A boxplot is a compact summary, handy for comparing groups.

2.1 — Make a histogram of flipper_length_mm.

ggplot(penguins, aes(x = flipper_length_mm)) +
  geom_histogram(binwidth = 5)

2.2 — Change the binwidth. Try binwidth = 2, then binwidth = 20. In a sentence, what happens to the shape? (No new plot to hand in — just look.)

2.3 — Compare groups with a boxplot of body_mass_g for each species.

ggplot(penguins, aes(x = species, y = body_mass_g)) +
  geom_boxplot()

Exercise 3 — Tidy data: reshape, then plot

Real data often comes off a machine in wide format — one row per sample, with a separate column for each measurement. Here is a small plate-reader result for four genes in ten samples:

# A tibble: 10 × 6
   sample group   gene_A gene_B gene_C gene_D
   <chr>  <chr>    <dbl>  <dbl>  <dbl>  <dbl>
 1 s01    control    102     88     41     59
 2 s02    control     98     91     44     62
 3 s03    control    105     86     39     58
 4 s04    control    100     93     43     61
 5 s05    control     97     89     40     60
 6 s06    treated    101     90     87    108
 7 s07    treated     99     92     91    112
 8 s08    treated    104     87     85    105
 9 s09    treated     98     94     90    110
10 s10    treated    103     88     88    107

In your Codespace you’ll read this from a file with read_csv("data/qpcr_plate_wide.csv") — same data, straight off the machine.

3.1 — Read the table. What does one row represent, and how many separate measurements are packed into each row?

Each row is one sample, and it holds four measurements (gene_Agene_D). That is wide format — ggplot can’t plot it directly yet.

3.2 — Reshape to long. Use pivot_longer() to stack the four gene columns into a gene column and a signal column.

plate_long <- plate |>
  pivot_longer(gene_A:gene_D, names_to = "gene", values_to = "signal")
plate_long
# A tibble: 40 × 4
   sample group   gene   signal
   <chr>  <chr>   <chr>   <dbl>
 1 s01    control gene_A    102
 2 s01    control gene_B     88
 3 s01    control gene_C     41
 4 s01    control gene_D     59
 5 s02    control gene_A     98
 6 s02    control gene_B     91
 7 s02    control gene_C     44
 8 s02    control gene_D     62
 9 s03    control gene_A    105
10 s03    control gene_B     86
# ℹ 30 more rows

3.3 — Plot it. Make a boxplot of signal by gene, filled by group. Which genes change between the control and treated samples?

ggplot(plate_long, aes(x = gene, y = signal, fill = group)) +
  geom_boxplot()