# Sample Data Matching Your Plot
df <- data.frame(
ABC = factor(rep(c("A", "B", "C"), each = 3), levels = c("C", "B", "A")), # Reversed for top-down A->C
XYZ = factor(rep(c("X", "Y", "Z"), times = 3), levels = c("X", "Y", "Z")),
SKUs = c(3704, 4014, 817, 5481, 3576, 2244, 7291, 2295, 3529),
Rev_Pct = c(38.5, 36.8, 4.6, 7.1, 5.0, 2.9, 2.7, 1.0, 1.3)
) %>%
mutate(
Label = paste0("**", ABC, XYZ, "**<br>",
comma(SKUs), " SKUs<br>",
Rev_Pct, "% Rev"),
# Dynamic text color threshold based on background darkness
Text_Color = ifelse(Rev_Pct > 15, "#0f172a", "#f8fafc")
)
# Plot
ggplot(df, aes(x = XYZ, y = ABC, fill = Rev_Pct)) +
geom_tile(color = "white", linewidth = 1.2) +
# Using ggtext for rich HTML text or standard geom_text
geom_text(aes(label = paste0(ABC, XYZ, "\n", comma(SKUs), " SKUs\n", Rev_Pct, "% revenue"),
color = Text_Color),
size = 3.8, lineheight = 0.9, fontface = "bold") +
scale_color_identity() +
# Square-root scale stretches the lower range (1-7%) so differences show clearly
scale_fill_gradientn(
colors = c("#1e293b", "#0369a1", "#0284c7", "#38bdf8", "#bae6fd"),
trans = "sqrt",
labels = percent_format(scale = 1),
name = "Revenue %"
) +
scale_y_discrete(limits = c("C", "B", "A")) + # Top-to-bottom: A, B, C
labs(
title = "ABC–XYZ Inventory Classification",
subtitle = "SKU count and revenue contribution across inventory matrix",
x = "Demand Predictability (XYZ)",
y = "Revenue Importance (ABC)"
) +
theme_minimal(base_size = 13) +
theme(
panel.grid = element_blank(),
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(color = "grey40", margin = margin(b = 15)),
axis.title = element_text(face = "bold", size = 11),
axis.text = element_text(size = 12, face = "bold"),
legend.position = "right"
)