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.
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
tipswith 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 |
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.
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)
Your prompt must make the AI produce:
tips, column tip in US dollars# ===== 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()
Your prompt must make the AI produce:
# ===== 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()
Your prompt must make the AI produce:
tip (y) against total_bill
(x)smoker, semi-transparent points# ===== 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()
Your prompt must make the AI produce:
day, side by side
for each sex# ===== 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()
Your prompt must make the AI produce:
time, columns = smokersize on a continuous
viridis scale# ===== 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()
Your prompt must make the AI produce:
# ===== 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()
Your prompt must make the AI produce:
total_bill,
tip, size, tip_pcttip_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()
Your prompt must make the AI produce:
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()