This page gives one weak and one stronger ggplot2
example for each of the 25 GoldenVizR rules. The goal is not to
prescribe one chart type, but to show the kind of practical correction
each rule encourages.
Use analyze_plot() on any example to inspect the
structured rule results. Use render_report(),
save_report(), or view_report() when you want
the same result as a formatted HTML report.
report <- analyze_plot(good_plot)
render_report(report)
save_report(report, "goldenviz-report.html")
view_report(report)Example HTML report output:
Annotated report layout:
Needs improvement the chart has no title, so the reader has to infer the question.
library(ggplot2)
ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
labs(x = "Weight", y = "Miles per gallon") +
theme_minimal()Better chart the title states what the chart is about.
library(ggplot2)
ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
labs(
title = "Fuel economy falls as vehicle weight increases",
x = "Weight",
y = "Miles per gallon"
) +
theme_minimal()Needs improvement raw variable names make the axes harder to understand.
library(ggplot2)
ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
labs(title = "Fuel economy by weight") +
theme_minimal()Better chart labels use readable language and units.
library(ggplot2)
ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
labs(
title = "Fuel economy by vehicle weight",
x = "Weight in 1000 pounds",
y = "Miles per gallon"
) +
theme_minimal()Needs improvement missing months vanish without explanation.
library(ggplot2)
sales <- data.frame(
month = as.Date(c("2026-01-01", "2026-02-01", "2026-04-01", "2026-05-01")),
revenue = c(12, 14, 18, 17)
)
ggplot(sales, aes(month, revenue)) +
geom_line() +
geom_point() +
labs(title = "Monthly revenue", x = "Month", y = "Revenue") +
theme_minimal()Better chart the chart makes the missing March value explicit.
library(ggplot2)
sales <- data.frame(
month = as.Date(c("2026-01-01", "2026-02-01", "2026-03-01", "2026-04-01", "2026-05-01")),
revenue = c(12, 14, NA, 18, 17)
)
ggplot(sales, aes(month, revenue)) +
geom_line(na.rm = TRUE) +
geom_point(size = 2, na.rm = TRUE) +
annotate("text", x = as.Date("2026-03-01"), y = 15.5, label = "March missing", size = 3) +
labs(
title = "Monthly revenue with missing March data shown",
x = "Month",
y = "Revenue",
caption = "March was not reported."
) +
theme_minimal()Needs improvement the color legend uses raw category codes.
library(ggplot2)
ggplot(mtcars, aes(wt, mpg, colour = factor(cyl))) +
geom_point(size = 2.5) +
labs(title = "Fuel economy by weight", x = "Weight", y = "Miles per gallon") +
theme_minimal()Better chart the legend title and labels explain the grouping.
library(ggplot2)
ggplot(mtcars, aes(wt, mpg, colour = factor(cyl))) +
geom_point(size = 2.5) +
scale_colour_discrete(
name = "Cylinder count",
labels = c("4 cylinders", "6 cylinders", "8 cylinders")
) +
labs(title = "Fuel economy by weight and cylinder count", x = "Weight", y = "Miles per gallon") +
theme_minimal()Needs improvement the chart gives no source, scope, or time context.
library(ggplot2)
ggplot(mtcars, aes(factor(cyl), mpg)) +
geom_boxplot() +
labs(title = "Fuel economy by cylinders", x = "Cylinders", y = "Miles per gallon") +
theme_minimal()Better chart the caption tells the reader where the data came from.
library(ggplot2)
ggplot(mtcars, aes(factor(cyl), mpg)) +
geom_boxplot() +
labs(
title = "Fuel economy by cylinder count",
subtitle = "Motor Trend road tests",
x = "Cylinders",
y = "Miles per gallon",
caption = "Source: mtcars dataset."
) +
theme_minimal()Needs improvement the important peak is left for the reader to discover.
library(ggplot2)
traffic <- data.frame(day = 1:10, visits = c(40, 42, 45, 47, 52, 88, 56, 54, 53, 51))
ggplot(traffic, aes(day, visits)) +
geom_line() +
geom_point() +
labs(title = "Daily visits", x = "Day", y = "Visits") +
theme_minimal()Better chart annotation explains the key event.
library(ggplot2)
traffic <- data.frame(day = 1:10, visits = c(40, 42, 45, 47, 52, 88, 56, 54, 53, 51))
ggplot(traffic, aes(day, visits)) +
geom_line() +
geom_point() +
annotate("label", x = 6.8, y = 82, label = "Campaign launch", size = 3) +
coord_cartesian(ylim = c(35, 94), clip = "off") +
labs(title = "Daily visits increased during campaign launch", x = "Day", y = "Visits") +
theme_minimal()Needs improvement small text makes the chart difficult to read.
library(ggplot2)
ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
labs(title = "Fuel economy by weight", x = "Weight", y = "Miles per gallon") +
theme_minimal(base_size = 6)Better chart the base text size is readable.
library(ggplot2)
ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
labs(title = "Fuel economy by weight", x = "Weight", y = "Miles per gallon") +
theme_minimal(base_size = 12)Needs improvement too many similar colors make categories hard to separate.
library(ggplot2)
segments <- data.frame(
segment = c("Home", "Food", "Bills", "Health", "Auto", "Shopping"),
value = c(32, 16, 21, 11, 15, 5)
)
ggplot(segments, aes(segment, value, fill = segment)) +
geom_col() +
scale_fill_manual(values = c("#d95f02", "#e66101", "#fdb863", "#b35806", "#fdae61", "#fee0b6")) +
labs(title = "Monthly spending", x = "Category", y = "Share") +
theme_minimal()Better chart direct labels and a restrained palette reduce dependence on color alone.
library(ggplot2)
segments <- data.frame(
segment = c("Home", "Food", "Bills", "Health", "Auto", "Shopping"),
value = c(32, 16, 21, 11, 15, 5)
)
ggplot(segments, aes(reorder(segment, value), value)) +
geom_col(fill = "#4c78a8") +
geom_text(aes(label = paste0(value, "%")), hjust = -0.15, size = 3.3) +
coord_flip() +
labs(title = "Monthly spending by category", x = NULL, y = "Share of spending") +
theme_minimal() +
theme(legend.position = "none")Needs improvement redundant layers and heavy styling compete with the data.
library(ggplot2)
ggplot(mtcars, aes(wt, mpg, colour = factor(cyl), shape = factor(cyl))) +
geom_point(size = 4) +
geom_line() +
geom_smooth(se = FALSE) +
labs(title = "Fuel economy", x = "Weight", y = "Miles per gallon") +
theme_bw() +
theme(panel.grid.minor = element_line(colour = "grey70"))Better chart the chart keeps only the marks needed for the comparison.
library(ggplot2)
ggplot(mtcars, aes(wt, mpg, colour = factor(cyl))) +
geom_point(size = 2.4, alpha = 0.85) +
labs(
title = "Fuel economy by vehicle weight",
x = "Weight",
y = "Miles per gallon",
colour = "Cylinders"
) +
theme_minimal()Needs improvement heavy grids dominate the visual field.
library(ggplot2)
ggplot(mtcars, aes(wt, mpg)) +
geom_point(size = 2.2) +
labs(title = "Fuel economy by weight", x = "Weight", y = "Miles per gallon") +
theme_minimal() +
theme(
panel.grid.major = element_line(colour = "grey25", linewidth = 0.8),
panel.grid.minor = element_line(colour = "grey45", linewidth = 0.6)
)Better chart subtle major guides support reading without taking over.
library(ggplot2)
ggplot(mtcars, aes(wt, mpg)) +
geom_point(size = 2.2) +
labs(title = "Fuel economy by weight", x = "Weight", y = "Miles per gallon") +
theme_minimal() +
theme(
panel.grid.major = element_line(colour = "grey88", linewidth = 0.4),
panel.grid.minor = element_blank()
)Needs improvement inconsistent labels and colors make the categories look unrelated.
library(ggplot2)
region <- data.frame(group = c("north", "SOUTH", "East", "west"), value = c(18, 24, 20, 16))
ggplot(region, aes(group, value, fill = group)) +
geom_col() +
scale_fill_manual(values = c("red", "grey50", "navy", "orange")) +
labs(title = "Regional performance", x = "region", y = "VALUE") +
theme_minimal()Better chart labels and styling follow one convention.
library(ggplot2)
region <- data.frame(group = c("North", "South", "East", "West"), value = c(18, 24, 20, 16))
ggplot(region, aes(group, value)) +
geom_col(fill = "#4c78a8") +
labs(title = "Regional performance", x = "Region", y = "Score") +
theme_minimal()Needs improvement a line implies order and continuity between unordered categories.
library(ggplot2)
region <- data.frame(region = c("North", "South", "East", "West"), value = c(18, 24, 20, 16))
ggplot(region, aes(region, value, group = 1)) +
geom_line() +
geom_point(size = 2.5) +
labs(title = "Regional performance", x = "Region", y = "Score") +
theme_minimal()Better chart bars suit unordered category comparisons.
library(ggplot2)
region <- data.frame(region = c("North", "South", "East", "West"), value = c(18, 24, 20, 16))
ggplot(region, aes(reorder(region, value), value)) +
geom_col(fill = "#4c78a8") +
coord_flip() +
labs(title = "Regional performance", x = "Region", y = "Score") +
theme_minimal()Needs improvement long numeric labels are harder to scan.
library(ggplot2)
revenue <- data.frame(year = 2021:2025, amount = c(1200000, 1450000, 1380000, 1700000, 1850000))
ggplot(revenue, aes(year, amount)) +
geom_line() +
geom_point() +
labs(title = "Revenue trend", x = "Year", y = "Revenue") +
theme_minimal()Better chart the scale communicates units directly.
library(ggplot2)
revenue <- data.frame(year = 2021:2025, amount = c(1200000, 1450000, 1380000, 1700000, 1850000))
ggplot(revenue, aes(year, amount / 1e6)) +
geom_line() +
geom_point() +
scale_x_continuous(breaks = revenue$year) +
labs(title = "Revenue trend", x = "Year", y = "Revenue, millions") +
theme_minimal()Needs improvement many overlapping points hide density.
library(ggplot2)
set.seed(7)
dense <- data.frame(x = rnorm(900), y = rnorm(900))
ggplot(dense, aes(x, y)) +
geom_point() +
labs(title = "Customer scores", x = "Score A", y = "Score B") +
theme_minimal()Better chart transparency reveals the concentration of observations.
library(ggplot2)
set.seed(7)
dense <- data.frame(x = rnorm(900), y = rnorm(900))
ggplot(dense, aes(x, y)) +
geom_point(alpha = 0.25, size = 1.6) +
labs(title = "Customer scores with point density visible", x = "Score A", y = "Score B") +
theme_minimal()Needs improvement the reader has to move between the legend and the lines.
library(ggplot2)
trend <- data.frame(
year = rep(2021:2025, 3),
product = rep(c("Basic", "Plus", "Pro"), each = 5),
revenue = c(10, 12, 14, 15, 17, 8, 11, 13, 16, 20, 6, 8, 12, 18, 24)
)
ggplot(trend, aes(year, revenue, colour = product)) +
geom_line(linewidth = 1) +
labs(title = "Revenue by product", x = "Year", y = "Revenue") +
theme_minimal()Better chart direct labels reduce legend lookup.
library(ggplot2)
trend <- data.frame(
year = rep(2021:2025, 3),
product = rep(c("Basic", "Plus", "Pro"), each = 5),
revenue = c(10, 12, 14, 15, 17, 8, 11, 13, 16, 20, 6, 8, 12, 18, 24)
)
end_labels <- trend[trend$year == 2025, ]
ggplot(trend, aes(year, revenue, colour = product)) +
geom_line(linewidth = 1) +
geom_text(data = end_labels, aes(label = product), hjust = -0.1, show.legend = FALSE) +
scale_x_continuous(breaks = 2021:2025, limits = c(2021, 2025.8)) +
labs(title = "Revenue by product", x = "Year", y = "Revenue") +
theme_minimal() +
theme(legend.position = "none")Needs improvement everything has the same visual weight.
library(ggplot2)
trend <- data.frame(year = rep(2021:2025, 3), product = rep(c("Basic", "Plus", "Pro"), each = 5),
revenue = c(10, 12, 14, 15, 17, 8, 11, 13, 16, 20, 6, 8, 12, 18, 24))
ggplot(trend, aes(year, revenue, colour = product)) +
geom_line(linewidth = 1.1) +
labs(title = "Revenue by product", x = "Year", y = "Revenue") +
theme_minimal()Better chart the main series is emphasized and supporting series recede.
library(ggplot2)
trend <- data.frame(year = rep(2021:2025, 3), product = rep(c("Basic", "Plus", "Pro"), each = 5),
revenue = c(10, 12, 14, 15, 17, 8, 11, 13, 16, 20, 6, 8, 12, 18, 24))
ggplot(trend, aes(year, revenue, group = product)) +
geom_line(data = subset(trend, product != "Pro"), colour = "grey70", linewidth = 0.8) +
geom_line(data = subset(trend, product == "Pro"), colour = "#8a3f24", linewidth = 1.4) +
labs(title = "Pro revenue is accelerating", x = "Year", y = "Revenue") +
theme_minimal()Needs improvement a narrow y-axis range exaggerates a modest change.
library(ggplot2)
approval <- data.frame(month = 1:6, score = c(71, 72, 72, 73, 74, 75))
ggplot(approval, aes(month, score)) +
geom_line() +
geom_point() +
coord_cartesian(ylim = c(70, 76)) +
labs(title = "Approval score", x = "Month", y = "Score") +
theme_minimal()Better chart the scale gives enough context for the magnitude.
library(ggplot2)
approval <- data.frame(month = 1:6, score = c(71, 72, 72, 73, 74, 75))
ggplot(approval, aes(month, score)) +
geom_line() +
geom_point() +
coord_cartesian(ylim = c(0, 100)) +
labs(title = "Approval score rose modestly", x = "Month", y = "Score out of 100") +
theme_minimal()Needs improvement bars start away from zero, inflating differences.
library(ggplot2)
teams <- data.frame(team = c("A", "B", "C"), score = c(92, 95, 97))
ggplot(teams, aes(team, score)) +
geom_col(fill = "#4c78a8") +
coord_cartesian(ylim = c(90, 98)) +
labs(title = "Team scores", x = "Team", y = "Score") +
theme_minimal()Better chart bars use a zero baseline.
library(ggplot2)
teams <- data.frame(team = c("A", "B", "C"), score = c(92, 95, 97))
ggplot(teams, aes(team, score)) +
geom_col(fill = "#4c78a8") +
coord_cartesian(ylim = c(0, 100)) +
labs(title = "Team scores", x = "Team", y = "Score out of 100") +
theme_minimal()Needs improvement area-like bubbles can make differences look larger than the values.
library(ggplot2)
markets <- data.frame(market = c("A", "B", "C", "D"), share = c(12, 18, 24, 30))
ggplot(markets, aes(market, 1, size = share)) +
geom_point(colour = "#8a3f24", alpha = 0.7) +
scale_size(range = c(6, 28)) +
labs(title = "Market share", x = "Market", y = NULL, size = "Share") +
theme_minimal()Better chart bar length supports more accurate comparison.
library(ggplot2)
markets <- data.frame(market = c("A", "B", "C", "D"), share = c(12, 18, 24, 30))
ggplot(markets, aes(reorder(market, share), share)) +
geom_col(fill = "#4c78a8") +
coord_flip() +
labs(title = "Market share", x = "Market", y = "Share (%)") +
theme_minimal()Needs improvement filtering removes the outlier without saying so.
library(ggplot2)
orders <- data.frame(order = 1:12, value = c(22, 24, 20, 23, 25, 21, 24, 26, 22, 23, 25, 80))
ggplot(subset(orders, value < 50), aes(order, value)) +
geom_point(size = 2.5) +
labs(title = "Order values", x = "Order", y = "Value") +
theme_minimal()Better chart show the outlier and explain it.
library(ggplot2)
orders <- data.frame(order = 1:12, value = c(22, 24, 20, 23, 25, 21, 24, 26, 22, 23, 25, 80))
ggplot(orders, aes(order, value)) +
geom_point(size = 2.5) +
annotate("label", x = 12, y = 80, label = "Bulk order", hjust = 1.05, size = 3) +
labs(title = "Order values with bulk order shown", x = "Order", y = "Value") +
theme_minimal()Needs improvement icon sizes are not proportional to the data.
library(ggplot2)
share <- data.frame(group = c("Small", "Medium", "Large"), value = c(10, 20, 40))
ggplot(share, aes(group, value, size = group)) +
geom_point(colour = "#8a3f24") +
scale_size_manual(values = c(8, 18, 30)) +
labs(title = "Group size", x = "Group", y = "Value") +
theme_minimal()Better chart use bar length for proportional comparison.
library(ggplot2)
share <- data.frame(group = c("Small", "Medium", "Large"), value = c(10, 20, 40))
ggplot(share, aes(group, value)) +
geom_col(fill = "#4c78a8") +
labs(title = "Group size", x = "Group", y = "Value") +
theme_minimal()Needs improvement averages are shown without uncertainty.
library(ggplot2)
estimate <- data.frame(group = c("A", "B", "C"), mean = c(10, 13, 15), low = c(8, 10, 12), high = c(12, 16, 18))
ggplot(estimate, aes(group, mean)) +
geom_point(size = 3) +
labs(title = "Average outcome by group", x = "Group", y = "Mean outcome") +
theme_minimal()Better chart intervals show uncertainty around the estimates.
library(ggplot2)
estimate <- data.frame(group = c("A", "B", "C"), mean = c(10, 13, 15), low = c(8, 10, 12), high = c(12, 16, 18))
ggplot(estimate, aes(group, mean)) +
geom_errorbar(aes(ymin = low, ymax = high), width = 0.15) +
geom_point(size = 3, colour = "#4c78a8") +
labs(title = "Average outcome by group with uncertainty", x = "Group", y = "Mean outcome") +
theme_minimal()Needs improvement the axis starts after the earlier decline.
library(ggplot2)
index <- data.frame(year = 2017:2026, value = c(120, 118, 114, 110, 112, 116, 119, 123, 126, 128))
ggplot(subset(index, year >= 2021), aes(year, value)) +
geom_line() +
geom_point() +
labs(title = "Index is rising", x = "Year", y = "Index") +
theme_minimal()Better chart the broader range shows the full pattern.
library(ggplot2)
index <- data.frame(year = 2017:2026, value = c(120, 118, 114, 110, 112, 116, 119, 123, 126, 128))
ggplot(index, aes(year, value)) +
geom_line() +
geom_point() +
scale_x_continuous(breaks = 2017:2026) +
labs(title = "Index recovered after an earlier decline", x = "Year", y = "Index") +
theme_minimal()Needs improvement discrete survey waves are connected as if the path between them is known.
library(ggplot2)
survey <- data.frame(wave = c("Wave 1", "Wave 2", "Wave 3", "Wave 4"), score = c(62, 66, 65, 70))
ggplot(survey, aes(wave, score, group = 1)) +
geom_line() +
geom_point(size = 2.5) +
labs(title = "Survey score by wave", x = "Wave", y = "Score") +
theme_minimal()Better chart points show separate observations without implying continuous movement.
library(ggplot2)
survey <- data.frame(wave = c("Wave 1", "Wave 2", "Wave 3", "Wave 4"), score = c(62, 66, 65, 70))
ggplot(survey, aes(wave, score)) +
geom_point(size = 3, colour = "#4c78a8") +
labs(title = "Survey score by wave", x = "Survey wave", y = "Score") +
theme_minimal()Needs improvement loaded language tells the reader what to feel.
library(ggplot2)
costs <- data.frame(year = 2021:2025, cost = c(40, 42, 45, 48, 51))
ggplot(costs, aes(year, cost)) +
geom_line() +
geom_point() +
labs(title = "Costs are exploding", x = "Year", y = "Cost") +
theme_minimal()Better chart neutral wording describes the observed change.
library(ggplot2)
costs <- data.frame(year = 2021:2025, cost = c(40, 42, 45, 48, 51))
ggplot(costs, aes(year, cost)) +
geom_line() +
geom_point() +
scale_x_continuous(breaks = costs$year) +
labs(title = "Costs increased from 2021 to 2025", x = "Year", y = "Cost") +
theme_minimal()