Assignment 4: Visualization Deep Dive

Due by 11:59 PM on Sunday, May 3, 2026

NoteAssignment Details

Assigned: Monday, April 27 (Session 9) Due: Sunday, May 3 at 11:59 PM Submit: Quarto document (.qmd) AND rendered HTML on Canvas

TipGetting started

Overview

This assignment focuses on creating publication-quality visualizations. You’ll practice using different geoms, scales, themes, and design principles.

Setup

# Assignment 4: Visualization Deep Dive
# Your Name
# Date

library(tidyverse)
library(palmerpenguins)  # install.packages("palmerpenguins") if needed

Part 1: Bar Charts with Error Bars (25 points)

Use the palmerpenguins dataset.

Task 1.1

Create a summary table showing mean flipper length, standard deviation (SD), and sample size (N) for each species.

# Hint: use group_by() + summarize() with mean(), sd(), and n().
# Don't forget na.rm = TRUE for the mean and sd.
penguins |>
  group_by(species) |>
  summarize(
    mean_flipper = mean(flipper_length_mm, na.rm = TRUE),
    sd_flipper   = sd(flipper_length_mm,   na.rm = TRUE),
    n            = n()
  )
# A tibble: 3 × 4
  species   mean_flipper sd_flipper     n
  <fct>            <dbl>      <dbl> <int>
1 Adelie            190.       6.54   152
2 Chinstrap         196.       7.13    68
3 Gentoo            217.       6.48   124

n() counts every row in the group, including rows where flipper_length_mm is NA. If you want only the rows that contributed to the mean, use sum(!is.na(flipper_length_mm)) instead.

Task 1.2

Create a bar chart showing mean flipper length by species with error bars representing the 95% confidence interval. Use stat_summary() for both the bar and the error bars — this is the pattern from the Session 8 slides.

Your plot should:

  • Have clear axis labels (not variable names)
  • Use a clean theme
  • Have a descriptive title that names what the error bars represent (or include it in a caption)
  • Use a colorblind-friendly palette (hint: scale_fill_viridis_d())
TipHint

You’ll need stat_summary() twice — once for the bar and once for the error bars:

ggplot(penguins, aes(x = species, y = flipper_length_mm, fill = species)) +
  stat_summary(fun = mean, geom = "bar") +
  stat_summary(fun.data = mean_cl_normal, geom = "errorbar", width = 0.2)

mean_cl_normal returns the 95% confidence interval (the Session 8 slides note this is the most common error bar in psychology).

ggplot(penguins, aes(x = species, y = flipper_length_mm, fill = species)) +
  stat_summary(fun = mean, geom = "bar") +
  stat_summary(fun.data = mean_cl_normal, geom = "errorbar", width = 0.2) +
  scale_fill_viridis_d() +
  labs(
    title   = "Mean flipper length by species",
    caption = "Error bars represent 95% confidence intervals.",
    x       = "Species",
    y       = "Flipper length (mm)"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

Bar chart of mean flipper length in millimeters by penguin species (Adelie, Chinstrap, Gentoo) with 95% confidence interval error bars. Gentoo penguins have the longest flippers by a clear margin.

The legend duplicates the x-axis labels, so it’s safe to drop it with theme(legend.position = "none"). Always say what the error bars represent — in the title, caption, or a figure note.

Part 2: Distributions (25 points)

Task 2.1

Create a histogram of penguin body mass. Try at least three different bin widths in your scratch work to see how the shape changes, then submit a single histogram with the bin width you think works best. In a comment, say what bin width you chose and why.

# Bin width of 250 g shows the bimodal shape clearly without being so jagged
# that the structure gets lost — wider bins (e.g., 500 g) smooth over the
# second mode, narrower bins (e.g., 100 g) are too noisy.
ggplot(penguins, aes(x = body_mass_g)) +
  geom_histogram(binwidth = 250, fill = "steelblue", color = "white") +
  labs(
    title = "Distribution of penguin body mass",
    x     = "Body mass (g)",
    y     = "Count"
  ) +
  theme_minimal()

Histogram of penguin body mass in grams using 250 g bins, showing a bimodal distribution.

There’s no single correct bin width — what matters is that you tried a few and made a deliberate choice. Anything in the 200–400 g range here is defensible.

Task 2.2

Create overlapping density plots of body mass, colored by species. Set transparency so all distributions are visible.

TipHint

To color the densities by species, set fill = species inside aes(), then add alpha = 0.5 (or similar) inside geom_density() to make the overlap visible.

ggplot(penguins, aes(x = body_mass_g, fill = species)) +
  geom_density(alpha = 0.5) +
  scale_fill_viridis_d() +
  labs(
    title = "Body mass by species",
    x     = "Body mass (g)",
    y     = "Density",
    fill  = "Species"
  ) +
  theme_minimal()

Overlapping density plots of body mass by penguin species. Adelie and Chinstrap distributions overlap heavily in the 3,000-4,000 g range; Gentoo sits cleanly above them around 5,000 g.

The histogram in 2.1 showed a bimodal shape; the density plot reveals why — Gentoo penguins are noticeably larger than the other two species. Adelie and Chinstrap overlap almost entirely.

Task 2.3

Create a boxplot of body mass by species. Then recreate it as a violin plot. In a comment, discuss what each shows that the other doesn’t.

ggplot(penguins, aes(x = species, y = body_mass_g, fill = species)) +
  geom_boxplot() +
  scale_fill_viridis_d() +
  labs(x = "Species", y = "Body mass (g)") +
  theme_minimal() +
  theme(legend.position = "none")

Boxplot of body mass by penguin species. Gentoo's median sits above the upper whisker of both Adelie and Chinstrap.

ggplot(penguins, aes(x = species, y = body_mass_g, fill = species)) +
  geom_violin() +
  scale_fill_viridis_d() +
  labs(x = "Species", y = "Body mass (g)") +
  theme_minimal() +
  theme(legend.position = "none")

Violin plot of body mass by penguin species, showing the full distribution shape for each.

The boxplot is great for spotting the median, IQR, and outliers at a glance — five-number summary, no shape. The violin shows the full distribution, so you can see whether the data are bimodal, skewed, or symmetric, but you lose the explicit quartile markers. Layering both (geom_violin() + geom_boxplot(width = 0.1)) is a common compromise.

Part 3: The “Bad” and “Good” Graph (25 points)

Using any dataset of your choice (mpg, penguins, diamonds, etc.):

Task 3.1: The Bad Graph

Create an intentionally bad visualization that violates at least 4 design principles. In comments, list what makes it bad.

Examples of “bad” choices (pick things you can actually do in ggplot2):

  • Rainbow color scheme (scale_color_gradientn(colors = rainbow(7)))
  • Missing or unhelpful axis labels (variable names instead of plain English)
  • Truncated y-axis that exaggerates differences (coord_cartesian(ylim = ...))
  • Way too many categories shown with the same palette
  • Overlapping text or unreadable font sizes
  • Chartjunk: garish background colors, redundant gridlines, distracting fills
  • A geom that doesn’t fit the data (e.g., a line plot connecting unordered categories)

There’s no single right answer — credit goes to plots that violate at least four principles intentionally and name each violation. Here’s one example.

# Bad choices made on purpose:
# 1. Rainbow palette (no perceptual ordering, bad for colorblind viewers)
# 2. Truncated y-axis exaggerating tiny differences
# 3. Variable names instead of plain-English labels
# 4. Garish background and 3-D-style gridlines (chartjunk)
# 5. Redundant legend (color and x-axis encode the same thing)
ggplot(mpg, aes(x = class, y = hwy, fill = class)) +
  stat_summary(fun = mean, geom = "bar") +
  scale_fill_manual(values = rainbow(7)) +
  coord_cartesian(ylim = c(15, 30)) +
  labs(title = "AVERAGE HWY MPG!!!", x = "class", y = "hwy") +
  theme(
    panel.background = element_rect(fill = "hotpink"),
    panel.grid.major = element_line(color = "yellow", linewidth = 1.2),
    axis.text.x      = element_text(angle = 0, size = 6)
  )

Intentionally bad bar chart of mean highway mpg by car class, with a rainbow palette, truncated y-axis, missing axis labels, a hot pink background, and an overly busy title.

Task 3.2: The Good Graph

Create a good version of the same data. In comments, explain your design choices.

# Good choices:
# 1. Y-axis starts at zero — no exaggeration of differences
# 2. Classes reordered by mean mpg so the ranking is the story
# 3. Single, muted fill color (no decorative palette)
# 4. Plain-English labels and a descriptive title
# 5. Minimal theme with no chartjunk
mpg |>
  group_by(class) |>
  summarize(mean_hwy = mean(hwy)) |>
  mutate(class = fct_reorder(class, mean_hwy)) |>
  ggplot(aes(x = mean_hwy, y = class)) +
  geom_col(fill = "steelblue") +
  labs(
    title = "Compact and subcompact cars get the best highway mileage",
    x     = "Mean highway mpg",
    y     = NULL
  ) +
  theme_minimal()

Bar chart of mean highway mpg by car class, reordered from highest to lowest, on a y-axis starting at zero with plain-English labels and a single muted fill color.

A horizontal bar chart with reordered categories is almost always easier to read than a vertical chart with seven labels crammed along the x-axis. Stating the takeaway in the title turns the chart from “data display” into “argument.”

Part 4: Recreate a Figure (15 points)

This task uses data from a real psychology study on partisan bias in news evaluation. Participants were assigned to one of three teams (Team Spain, Team Greece, or No Team) and read news statements slanted toward one side or the other. The outcome is an acceptance threshold (c) from signal detection theory — higher values mean the participant required stronger evidence before accepting a statement as true.

Download partisan_bias.csv from Canvas and save it in your data/raw/ folder. The file has four columns:

Column Description
participant_id Unique participant identifier
team Group assignment: 1 = Team Spain, 2 = Team Greece, 3 = No Team
SP_c Acceptance threshold for Pro-Spain statements
GR_c Acceptance threshold for Pro-Greece statements

Task 4.1

Import the data and convert team from a number to a factor with labels “Spain”, “Greece”, and “No Team” (in that order).

TipHint

The safest way to recode is to specify both levels and labels so the mapping is explicit:

mutate(team = factor(team,
                     levels = c(1, 2, 3),
                     labels = c("Spain", "Greece", "No Team")))
partisan_data <- read_csv("data/raw/partisan_bias.csv")
partisan_data <- partisan_data |>
  mutate(team = factor(team,
                       levels = c(1, 2, 3),
                       labels = c("Spain", "Greece", "No Team")))

partisan_data
# A tibble: 90 × 4
   participant_id team     SP_c    GR_c
            <int> <fct>   <dbl>   <dbl>
 1              1 Spain -0.0438 -0.288 
 2              2 Spain -0.524   0.742 
 3              3 Spain -0.158   0.158 
 4              4 Spain -0.225   0.594 
 5              5 Spain -0.400  -0.247 
 6              6 Spain -0.955   0.222 
 7              7 Spain -0.421   0.0123
 8              8 Spain -0.506   0.153 
 9              9 Spain -0.166  -0.0232
10             10 Spain -0.342   0.411 
# ℹ 80 more rows

Specifying both levels and labels is safer than relying on the default ordering — if a value of 4 or 0 ever sneaks into the data, it becomes NA rather than getting silently mis-labeled.

Task 4.2

Recreate Figure 3 from the paper (provided on Canvas) as closely as you can. Pay attention to:

  • Geom type and positioning
  • Error bars
  • Axis labels and legend title
  • Theme

You won’t match it exactly — plain text axis labels are fine.

TipHint

The data has SP_c and GR_c as two separate columns (one row per participant). To plot acceptance threshold by team and statement type, you’ll need to pivot to long format first so that statement type becomes one column and the c value becomes another. Look back at Session 5 (Data Tidying) for pivot_longer().

partisan_long <- partisan_data |>
  pivot_longer(
    cols      = c(SP_c, GR_c),
    names_to  = "statement",
    values_to = "c"
  ) |>
  mutate(statement = recode(statement,
                            SP_c = "Pro-Spain",
                            GR_c = "Pro-Greece"))

ggplot(partisan_long, aes(x = team, y = c, fill = statement)) +
  stat_summary(fun = mean, geom = "bar",
               position = position_dodge(width = 0.9)) +
  stat_summary(fun.data = mean_cl_normal, geom = "errorbar",
               width = 0.2,
               position = position_dodge(width = 0.9)) +
  labs(
    x    = "Team",
    y    = "Acceptance threshold (c)",
    fill = "Statement type"
  ) +
  theme_minimal()

Bar chart of mean acceptance threshold c by team (Spain, Greece, No Team), with separate bars for Pro-Spain and Pro-Greece statements and 95% confidence interval error bars. Team members are more accepting of statements that favor their own side.

The pivot is the critical step — until statement and c are their own columns, ggplot has no way to map statement type to fill. position_dodge() must be set to the same width on the bars and the error bars, or the error bars float to the wrong place.

Grading Rubric

Task Points
1.1: Summary table (mean, SD, N by species) 10
1.2: Bar chart with error bars 15
2.1: Histogram with bin width exploration 8
2.2: Overlapping density plots by species 8
2.3: Boxplot vs. violin plot comparison 9
3.1: Intentionally bad visualization 12
3.2: Good visualization with design justification 13
4.1: Import data and convert team to factor 5
4.2: Recreate Figure 3 10
QMD file knits without errors 10
Total 100

Each task is graded on a 4-level scale: full credit, full minus one (small slip), half credit (one major error), or none (not attempted or multiple major errors). “QMD file knits” refers to whether the Quarto document renders successfully, not whether individual code produces correct output.

TipIf a chunk won’t run

If you can’t get a particular chunk to work, set #| eval: false at the top of that chunk so the rest of the document still renders. You’ll lose points on the task itself, but you’ll keep the 10 points for the file knitting cleanly.

Submission

Submit:


NotePSY 510 (Graduate Students)

Students enrolled in PSY 510 must complete the following extension in addition to all tasks above.

Graduate Extension: Publication-Ready Figure

Creating figures for a course assignment and creating figures for a manuscript are different tasks. Journals specify file format, resolution, and dimensions, and reviewers expect a caption that stands on its own without the surrounding text.

Task G.1

Choose the figure you’re most proud of from Parts 1–3. Look up the author guidelines for one real journal in your area (Psychological Science, JPSP, Emotion, or similar) and save the figure at 300 dpi with dimensions matching their specifications.

# Example — adjust width/height to your target journal's specs:
ggsave("publication_figure.tiff",
       plot  = my_plot,
       width = 3.5, height = 3,
       units = "in", dpi = 300)

Task G.2

Write a figure caption in APA style — the kind you would include in a manuscript. A good APA caption:

  • Starts with a bold figure number and title (e.g., Figure 1. Mean Flipper Length by Species)
  • Describes what the figure shows in one or two sentences
  • Defines any abbreviations
  • States what the error bars represent (if applicable) and the N
  • Stands alone — a reader who hasn’t read the paper should understand the figure

Include the caption as a comment directly above your ggsave() call. (We’ll cover APA figure conventions in more depth in Session 16; for now, a thoughtful caption is all that’s needed.)

Task G.3

In 3–5 sentences (as a comment), justify your design choices: Why this geom? Why this color palette? What did you deliberately remove or simplify? Connect your reasoning to the perception and design principles from Session 9.

Submission: Add ggsave(), the caption, and the justification to your .qmd file under a clearly marked ## Graduate Extension section. Submit the saved figure file alongside your .qmd and HTML.