If you want to keep the gradient scale and have two additional discrete values for off limits above and below, I think the easiest way would be to have separate fill scales for "in-limit" and "off-limit" values. This can be done with separate calls to geom_tile on subsets of your data and with packages such as {ggnewscale}.
I think it then would make sense to place the discrete "off-limits" at the respective extremes of your gradient color bar. You need then three geom_tile
calls and three scale_fill
calls, and you will need to specify the guide order within each scale_fill
call. You will then need to play around with the legend margins, but it's not a big problem to make it look OK.
library(tidyverse)
library(RColorBrewer)
tile_data <- data.frame(
category = letters[1:5],
region = paste0("region_", 1:5),
sales = c(1, 2, 5, 0.1, 300)
)
ggplot(tile_data, aes(
x = category,
y = region,
fill = sales
)) +
geom_tile(data = filter(tile_data, sales <= 11 & sales >=1)) +
scale_fill_gradientn(NULL,
limits = c(1, 11),
colors = brewer.pal(11, "Spectral"),
guide = guide_colorbar(order = 2)
) +
ggnewscale::new_scale_fill() +
geom_tile(data = filter(tile_data, sales > 11), mapping = aes(fill = sales > 11)) +
scale_fill_manual("Sales", values = "black", labels = "> 11", guide = guide_legend(order = 1)) +
ggnewscale::new_scale_fill() +
geom_tile(data = filter(tile_data, sales < 1), mapping = aes(fill = sales < 1)) +
scale_fill_manual(NULL, values = "darkblue", labels = "< 1", guide = guide_legend(order = 3)) +
theme_minimal() +
theme(legend.spacing.y = unit(-6, "pt"),
legend.title = element_text(margin = margin(b = 10)))

Created on 2021-11-22 by the reprex package (v2.0.1)