Examples

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:

Completeness

Rule 1: Clear title

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()

Rule 2: Axis labels

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()

Rule 3: Missing values and gaps

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()

Rule 4: Legend clarity

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()

Rule 5: Source and context

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()

Rule 6: Annotation for key message

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()

Readability

Rule 7: Readable text

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)

Rule 8: Accessible color choices

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")

Rule 9: Avoid clutter

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()

Rule 10: Grid and guide balance

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()
  )

Rule 11: Consistent formatting

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()

Rule 12: Appropriate chart type

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()

Rule 13: Readable scale labels

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()

Rule 14: Overplotting management

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()

Rule 15: Direct labeling when useful

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")

Rule 16: Visual hierarchy

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()

Integrity

Rule 17: Truthful scale

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()

Rule 18: No distorted baseline

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()

Rule 19: Avoid misleading encodings

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()

Rule 20: Handle outliers honestly

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()

Rule 21: Proportional areas

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()

Rule 22: Uncertainty when needed

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()

Rule 23: Avoid cherry picking

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()

Rule 24: Respect data granularity

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()

Rule 25: Neutral framing

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()