-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Description
I found a problem with using ggplot2::binned_scale()
. When using 5 bins, it now requires 6 colors. This broke the stable version of ggredist on CRAN. It appears that this was not caught by revdep checks. Most importantly, this means that, if you provide a character vector of colors (instead of a vctrs vector), you will get an incorrect plot but no errors, as the bins are off. When using vctrs, the stricter behavior fails loudly.
I expected five colors to map onto five bins. I don't see this breaking change in the NEWS or in existing issues. I believe it is a subtle error in the subsetting logic and not intended.
Here is the code to reproduce the bug:
library(ggplot2)
library(ggredist)
data(oregon)
scale_fill_538 <- function(...) {
ggplot2::binned_scale(aesthetics = 'fill',
palette = function(x) ggredist$fivethirtyeight,
breaks = c(0, 0.35, 0.45, 0.55, 0.65, 1),
limits = c(.25, .75),
oob = scales::squish,
guide = 'colourbar',
...
)
}
oregon |>
ggplot(aes(fill = ndv / (ndv + nrv))) +
geom_sf() +
scale_fill_538()
#> Error in `vec_slice()`:
#> ! Can't subset elements past the end.
#> ℹ Location 6 doesn't exist.
#> ℹ There are only 5 elements.
What's going on here?
scale_fill_538()
is a binned scale, based on 538's old electoral maps. It has 5 colors stored in a vctrs
class from palette
:
structure(c("#FA5A50", "#FF998A", "#EAE3EB", "#A1A9ED", "#5768AC"
), class = c("palette", "vctrs_vctr"))
If we update the scale to simply repeat the first color, it now fills in everything as expected.
library(ggplot2)
library(ggredist)
data(oregon)
scale_fill_538 <- function(...) {
ggplot2::binned_scale(aesthetics = 'fill',
palette = function(x) c(ggredist$fivethirtyeight[1], ggredist$fivethirtyeight),
breaks = c(0, 0.35, 0.45, 0.55, 0.65, 1),
limits = c(.25, .75),
oob = scales::squish,
guide = 'colourbar',
...
)
}
oregon |>
ggplot(aes(fill = ndv / (ndv + nrv))) +
geom_sf() +
scale_fill_538()
Created on 2025-09-15 with reprex v2.1.1
Digging a little deeper: When we don't use a vctrs class for the colors, we see the following:
library(ggplot2)
library(ggredist)
data(oregon)
scale_fill_538 <- function(...) {
ggplot2::binned_scale(aesthetics = 'fill',
palette = function(x) c("#FA5A50", "#FF998A", "#EAE3EB", "#A1A9ED", "#5768AC"
),
breaks = c(0, 0.35, 0.45, 0.55, 0.65, 1),
limits = c(.25, .75),
oob = scales::squish,
guide = 'colourbar',
...
)
}
oregon |>
ggplot(aes(fill = ndv / (ndv + nrv))) +
geom_sf() +
scale_fill_538()
Created on 2025-09-15 with reprex v2.1.1
ie, it produces an incorrect plot but doesn't error. It drops the first color silently, so that the 5 bins use 4 colors + white in the legend.
And again, we can correct the chart
library(ggplot2)
library(ggredist)
data(oregon)
scale_fill_538 <- function(...) {
ggplot2::binned_scale(aesthetics = 'fill',
palette = function(x) c("#FA5A50", "#FA5A50", "#FF998A", "#EAE3EB", "#A1A9ED", "#5768AC"
),
breaks = c(0, 0.35, 0.45, 0.55, 0.65, 1),
limits = c(.25, .75),
oob = scales::squish,
guide = 'colourbar',
...
)
}
oregon |>
ggplot(aes(fill = ndv / (ndv + nrv))) +
geom_sf() +
scale_fill_538()
Created on 2025-09-15 with reprex v2.1.1
Finally, this is an important issue as it silently produces incorrect plots. Here is a manually binned version, using dplyr to do the bins and adding a manual class with the names.
library(ggplot2)
library(ggredist)
data(oregon)
oregon |>
dplyr::mutate(class = dplyr::case_when(
ndv / (ndv + nrv) < 0.35 ~ "Strong R",
ndv / (ndv + nrv) < 0.45 ~ "Lean R",
ndv / (ndv + nrv) < 0.55 ~ "Tossup",
ndv / (ndv + nrv) < 0.65 ~ "Lean D",
TRUE ~ "Strong D"
)) |>
ggplot(aes(fill = class)) +
scale_fill_manual(
values = c(
"Strong R" = "#FA5A50",
"Lean R" = "#FF998A",
"Tossup" = "#EAE3EB",
"Lean D" = "#A1A9ED",
"Strong D" = "#5768AC"
)
) +
geom_sf()
Created on 2025-09-15 with reprex v2.1.1
I'm relying on data from ggredist
because it allows me to directly link an example pkgdown generated under the old ggplot version with the expected map, here: https://alarm-redist.org/ggredist/reference/scale_538.html (Sorry, I know it's never optimal to introduce a second package into the mix while reporting issues, but I figure the reference is net helpful.)
