Each challenge shows a finished chart made from the tips data. Your job is to get an AI assistant (Copilot, ChatGPT, Claudeโ€ฆ) to write the code that reproduces it.

Technique: reverse-engineer the prompt

You do not have to write the prompt from scratch. Let the AI read the chart for you, then turn what it sees into a prompt. Three steps:

Step 1. Describe. Save the target chart (right-click the image, or take a screenshot), attach it to the AI chat and send:

Describe the elements of this chart (attached).

The AI lists what it sees: chart type, what is on each axis, colours and what they encode, panels, reference lines, labels, title.

Step 2. Reverse-engineer. In the same chat, send:

Concise reverse engineering for prompt to reconstruct this chart in R (ggplot2) with my data: a data frame tips with columns total_bill (bill in $), tip (tip in $), sex (Female/Male), smoker (Yes/No), day (Thur/Fri/Sat/Sun), time (Lunch/Dinner), size (people at the table), tip_pct (tip / total_bill * 100).

Telling the AI your column names is what makes the prompt usable on your data.

Step 3. Check, then run. Read the prompt the AI wrote. Fix anything wrong (a column that does not exist, a wrong colour, a missing line). Send the prompt in a new chat, paste the code it returns into Chunk 1 and run it. A new chat tests whether the prompt works on its own, without the image.

A good chart prompt names five things. Use this to check the AIโ€™s prompt in Step 3:

Part Example
Data context โ€œa data frame tips; tip is in US dollarsโ€
Chart type โ€œhistogramโ€, โ€œ100% stacked horizontal barโ€
Mapping which column goes to x, y, colour, facets
Annotations reference lines, value labels, axis limits
Labels & style exact title, axis labels, legend, theme, palette

How each challenge works

  1. Read the requirement and look at the target chart.
  2. Chunk 1 (yours): paste the prompt from Step 3 as a comment, then the AIโ€™s code below it, and run it.
  3. Compare with the target. If something is off, send follow-up prompts and note how many you needed.
  4. Chunk 2 (reference code): one way to draw the chart. Try before you peek, and compare it with the AIโ€™s code: same idea, different syntax?

Score: 3 points if the Step 3 prompt nails it, 2 after one follow-up, 1 after two, 0 after that.

Small styling differences (font, grey shade) are fine. The structure of the chart must match.

Target charts were drawn in Python, so fonts and colours differ slightly from ggplot2 defaults. Match the structure.

0. Setup (run first)

library(ggplot2); library(dplyr)
tips <- read.csv("data/tips.csv") |>
  mutate(day = factor(day, levels = c("Thur", "Fri", "Sat", "Sun")),
         tip_pct = tip / total_bill * 100)

Challenge 1 ยท One distribution (Warm-up)

Your prompt must make the AI produce:

  • Data: tips, column tip in US dollars
  • Chart: histogram, bin width $0.50
  • A dashed vertical line at the median, plus a text label with its value
  • Exact title and axis labels (note: y is Number of tables)
# ===== Chunk 1: YOUR PROMPT + AI CODE =====
# Prompt (from Step 3):
# 
# Follow-ups used: 0

# Paste the AI's code below. Change eval=FALSE to eval=TRUE before knitting
# ===== Chunk 2: REFERENCE CODE (try first!) =====
med <- median(tips$tip)
ggplot(tips, aes(tip)) +
  geom_histogram(binwidth = 0.5, fill = "#66c2a5", color = "white") +
  geom_vline(xintercept = med, linetype = "dashed") +
  annotate("text", x = med + 0.15, y = Inf, vjust = 2, hjust = 0,
           label = sprintf("median = $%.2f", med)) +
  labs(title = "Most tips are between $2 and $4", x = "Tip ($)", y = "Number of tables") +
  theme_minimal()


Challenge 2 ยท Highlight one bar (Warm-up)

Your prompt must make the AI produce:

  • A value per group: the mean tip rate (tip รท bill ร— 100) for each day
  • Days in the order Thur, Fri, Sat, Sun
  • All bars grey except Friday, in orange
  • Value labels on top of each bar with one decimal and a % sign; y-axis from 0 to 20
# ===== Chunk 1: YOUR PROMPT + AI CODE =====
# Prompt (from Step 3):
# 
# Follow-ups used: 0

# Paste the AI's code below. Change eval=FALSE to eval=TRUE before knitting
# ===== Chunk 2: REFERENCE CODE (try first!) =====
tips |>
  group_by(day) |>
  summarise(m = mean(tip_pct)) |>
  ggplot(aes(day, m, fill = day == "Fri")) +
  geom_col() +
  geom_text(aes(label = sprintf("%.1f%%", m)), vjust = -0.4) +
  scale_fill_manual(values = c(`FALSE` = "grey75", `TRUE` = "#fc8d62"), guide = "none") +
  coord_cartesian(ylim = c(0, 20)) +
  labs(title = "Friday has the highest average tip rate", x = NULL, y = "Mean tip (% of bill)") +
  theme_minimal()


Challenge 3 ยท Two groups, two trend lines (Core)

Your prompt must make the AI produce:

  • Scatter of tip (y) against total_bill (x)
  • Colour by smoker, semi-transparent points
  • One straight regression line per group, no confidence band
  • The title states the finding, not the variables
# ===== Chunk 1: YOUR PROMPT + AI CODE =====
# Prompt (from Step 3):
# 
# Follow-ups used: 0

# Paste the AI's code below. Change eval=FALSE to eval=TRUE before knitting
# ===== Chunk 2: REFERENCE CODE (try first!) =====
ggplot(tips, aes(total_bill, tip, color = smoker)) +
  geom_point(alpha = 0.6) +
  geom_smooth(method = "lm", se = FALSE) +
  labs(title = "Smokers' tips rise less steeply with the bill",
       x = "Total bill ($)", y = "Tip ($)") +
  theme_minimal()


Challenge 4 ยท Grouped boxplots with a reference line (Core)

Your prompt must make the AI produce:

  • Boxplots of tip rate (%) by day, side by side for each sex
  • A red dotted horizontal line at 15% (the usual US norm)
  • y-axis limited to 0โ€“45 %. โš ๏ธ One table tipped 71%: is it hidden or deleted? Ask the AI to zoom, not drop data
  • No x-axis label; legend in the top-right
# ===== Chunk 1: YOUR PROMPT + AI CODE =====
# Prompt (from Step 3):
# 
# Follow-ups used: 0

# Paste the AI's code below. Change eval=FALSE to eval=TRUE before knitting
# ===== Chunk 2: REFERENCE CODE (try first!) =====
ggplot(tips, aes(day, tip_pct, fill = sex)) +
  geom_boxplot() +
  geom_hline(yintercept = 15, color = "red", linetype = "dotted") +
  coord_cartesian(ylim = c(0, 45)) +        # zoom; ylim() would DROP the 71% table
  labs(title = "Tip rate by day and payer's sex", x = NULL,
       y = "Tip (% of bill)", fill = NULL) +
  theme_minimal()


Challenge 5 ยท A grid of small multiples (Core)

Your prompt must make the AI produce:

  • Scatter tip vs total bill, 2 ร— 2 panels: rows = time, columns = smoker
  • Colour = party size on a continuous viridis scale
  • Panel titles show both the variable and the value (e.g.ย time = Lunch | smoker = No)
  • One overall title above all panels
# ===== Chunk 1: YOUR PROMPT + AI CODE =====
# Prompt (from Step 3):
# 
# Follow-ups used: 0

# Paste the AI's code below. Change eval=FALSE to eval=TRUE before knitting
# ===== Chunk 2: REFERENCE CODE (try first!) =====
ggplot(tips, aes(total_bill, tip, color = size)) +
  geom_point() +
  scale_color_viridis_c() +
  facet_grid(time ~ smoker, labeller = label_both) +
  labs(title = "Tip vs bill by time and smoker, coloured by party size",
       x = "Total bill ($)", y = "Tip ($)") +
  theme_minimal()


Challenge 6 ยท 100% stacked horizontal bars (Stretch)

Your prompt must make the AI produce:

  • Horizontal bars, one per day, Thur at the top
  • Each bar is the share of tables by party size (1 to 6), adding to 100%
  • Discrete viridis colours, legend Party size outside on the right
  • x-axis 0 to 100 labelled % of tables
# ===== Chunk 1: YOUR PROMPT + AI CODE =====
# Prompt (from Step 3):
# 
# Follow-ups used: 0

# Paste the AI's code below. Change eval=FALSE to eval=TRUE before knitting
# ===== Chunk 2: REFERENCE CODE (try first!) =====
ggplot(tips, aes(y = factor(day, levels = rev(levels(day))), fill = factor(size))) +
  geom_bar(position = position_fill(reverse = TRUE), width = 0.7) +
  scale_x_continuous(labels = scales::percent) +
  scale_fill_viridis_d(name = "Party size") +
  labs(title = "Weekend tables are larger", x = "% of tables", y = NULL) +
  theme_minimal()


Challenge 7 ยท Correlation heatmap (Stretch)

Your prompt must make the AI produce:

  • Correlations between the 4 numeric columns: total_bill, tip, size, tip_pct
  • Square tiles with the value printed, 2 decimals
  • Diverging redโ€“blue palette fixed from โˆ’1 to +1 (red = positive)
  • Why is tip_pct negatively related to total_bill? Discuss after drawing
# ===== Chunk 1: YOUR PROMPT + AI CODE =====
# Prompt (from Step 3):
# 
# Follow-ups used: 0

# Paste the AI's code below. Change eval=FALSE to eval=TRUE before knitting
# ===== Chunk 2: REFERENCE CODE (try first!) =====
vars <- c("total_bill", "tip", "size", "tip_pct")
cm <- cor(tips[, vars])
as.data.frame(as.table(cm)) |>
  mutate(Var1 = factor(Var1, levels = vars), Var2 = factor(Var2, levels = rev(vars))) |>
  ggplot(aes(Var1, Var2, fill = Freq)) +
  geom_tile(color = "white") +
  geom_text(aes(label = sprintf("%.2f", Freq))) +
  scale_fill_gradient2(low = "#2166ac", mid = "white", high = "#b2182b", limits = c(-1, 1), name = "r") +
  coord_fixed() +
  labs(title = "Correlation between numeric variables", x = NULL, y = NULL) +
  theme_minimal()


Challenge 8 ยท Split violins with raw points (Boss level)

Your prompt must make the AI produce:

  • Tip rate (%) by day; each violin is split in half: left = non-smoker, right = smoker
  • Dashed quartile lines inside each half
  • Every table shown as a small, faint black dot on top
  • y-axis 0 to 50. โš ๏ธ Split violins are one argument in seaborn but not built into ggplot2: R users need to tell the AI which package to use (e.g.ย see::geom_violinhalf) or accept dodged violins
# ===== Chunk 1: YOUR PROMPT + AI CODE =====
# Prompt (from Step 3):
# 
# Follow-ups used: 0

# Paste the AI's code below. Change eval=FALSE to eval=TRUE before knitting
# ===== Chunk 2: REFERENCE CODE (try first!) =====
# Closest ggplot2 version without extra packages: dodged (not split) violins
ggplot(tips, aes(day, tip_pct, fill = smoker)) +
  geom_violin(position = position_dodge(0.8), draw_quantiles = c(0.25, 0.5, 0.75),
              linetype = "dashed") +
  geom_point(position = position_jitterdodge(jitter.width = 0.15, dodge.width = 0.8),
             size = 0.6, alpha = 0.4, show.legend = FALSE) +
  coord_cartesian(ylim = c(0, 50)) +
  labs(title = "Smokers' tip rates are more spread out, every day",
       x = NULL, y = "Tip (% of bill)", fill = "Smoker") +
  theme_minimal()