Quick summary
Summarize this blog with AI
Introduction
ggplot2 uses discrete-scale order, usually derived from factor levels or sorted character values; it does not infer a business ranking. Choose whether the order represents meaning, a summarized metric or observed frequency. This recipe supplies each case and checks the resulting levels before drawing.
Tested with R 4.5.1, ggplot2 4.0.0 and forcats 1.0.1. Run the code blocks in order. Install the named packages before starting; inputs are supplied below.
Use explicit levels for a semantic order
A tier sequence should remain Starter, Team, Scale even when a different tier has the largest count. factor(levels = ...) records that order. Validate labels before conversion so an unexpected value does not quietly become NA. Row sorting by itself does not redefine factor levels.
library(ggplot2)
library(forcats)
tiers <- c("Starter", "Team", "Scale")
subscriptions <- data.frame(tier = c("Team", "Starter", "Scale"), accounts = c(80, 30, 50))
stopifnot(all(subscriptions$tier %in% tiers))
subscriptions$tier <- factor(subscriptions$tier, levels = tiers)
semantic <- ggplot(subscriptions, aes(tier, accounts)) +
geom_col(fill = "#447f98") +
labs(title = "Account counts in tier order", x = "Tier", y = "Accounts") +
theme_minimal(base_size = 12)
print(semantic)
stopifnot(identical(levels(subscriptions$tier), tiers))
The bars appear Starter, Team, Scale with heights 30,80,50. That order encodes the tier progression rather than ranking the counts.
Rank by a specified metric and tie rule
fct_reorder() orders levels using a summary of another variable; its default summary is the median, so name the intended summary. Here each tier already has one row. For horizontal bars, ascending levels put the largest value at the top after coord_flip().
ranked <- subscriptions
ranked$tier <- fct_reorder(ranked$tier, ranked$accounts, .fun = sum, .desc = FALSE)
ranking <- ggplot(ranked, aes(tier, accounts)) +
geom_col(fill = "#447f98") + coord_flip() +
labs(title = "Accounts ranked by count", x = "Tier", y = "Accounts") +
theme_minimal(base_size = 12)
print(ranking)
stopifnot(identical(levels(ranked$tier), c("Starter", "Scale", "Team")))
tied <- data.frame(label = c("B", "A", "C"), value = c(10, 10, 20))
tie_order <- tied$label[order(tied$value, tied$label, method = "radix")]
tied$label <- factor(tied$label, levels = tie_order)
stopifnot(identical(levels(tied$label), c("A", "B", "C")))
Team is visually at the top, then Scale, then Starter. The separate tied example orders equal values alphabetically using an explicit secondary key. For raw transactions, aggregate first or specify the desired summary; ranking by the median transaction is different from ranking by total revenue.
Order raw categories by frequency when frequency is the question
fct_infreq() orders levels by decreasing frequency. Unlike geom_col(), geom_bar() counts input rows, so use raw observations here. A declared initial level order provides a reproducible tie convention.
raw <- data.frame(tier = factor(c("Starter", "Team", "Team", "Scale", "Team", "Scale"),
levels = tiers))
raw$tier <- fct_infreq(raw$tier)
frequency <- ggplot(raw, aes(tier)) + geom_bar(fill = "#447f98") +
labs(title = "Observed frequency order", x = "Tier", y = "Records") +
theme_minimal(base_size = 12)
print(frequency)
stopifnot(identical(levels(raw$tier), c("Team", "Scale", "Starter")),
identical(ggplot_build(frequency)$data[[1]]$count, c(3, 2, 1)))
The bar order is Team, Scale, Starter with counts 3,2,1. Frequency ordering can change after filtering, so retain semantic levels when consistency across reports matters more than the current ranking.
Treat axes, legends and facets as related but separate scales
A factor mapped to fill supplies a legend order; a factor used in facet_wrap() supplies panel order. Discrete scale limits or breaks and guide settings can override defaults. Set drop = FALSE on the relevant scale or facet when unused levels should remain visible; an unused level’s appearance does not establish that an unmeasured value is zero.
subset_tiers <- subscriptions[subscriptions$tier != "Team", ]
retained <- ggplot(subset_tiers, aes(tier, accounts)) + geom_col() +
scale_x_discrete(limits = tiers, drop = FALSE)
stopifnot(identical(as.character(ggplot_build(retained)$layout$panel_params[[1]]$x$breaks), tiers))
empty <- subscriptions[FALSE, ]
stopifnot(identical(levels(empty$tier), tiers))
Check missing and unexpected labels explicitly
Do not overwrite the source column merely to format one report. Keep a separate factor where different charts need different orders. Missing category labels need an explicit display or exclusion policy, and unknown labels must be resolved before conversion. Factors also control model reference coding; changing chart order and model reference level are separate decisions.
as_tier <- function(x) {
if (!is.character(x) || anyNA(x) || any(!x %in% tiers))
stop("Expected nonmissing Starter, Team or Scale labels")
factor(x, levels = tiers)
}
fails <- function(expr) inherits(tryCatch(expr, error = identity), "error")
stopifnot(fails(as_tier("Enterprise")), fails(as_tier(NA_character_)),
identical(levels(as_tier(character())), tiers),
identical(as.character(subscriptions$tier), c("Team", "Starter", "Scale")))