Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't use labels argument in scale_*() when show.limits = TRUE #5009

Closed
brunomioto opened this issue Oct 8, 2022 · 6 comments
Closed

Can't use labels argument in scale_*() when show.limits = TRUE #5009

brunomioto opened this issue Oct 8, 2022 · 6 comments
Labels
bug an unexpected problem or unintended behavior guides 📏 scales 🐍

Comments

@brunomioto
Copy link

It always gives

Error in `check_breaks_labels()`:
! `breaks` and `labels` must have the same length

Reprex

library(tidyverse)

#works
mtcars %>%
  ggplot(aes(hp, mpg, color = mpg)) +
  geom_point() +
  scale_color_stepsn(
    colors = c("#dd3497",
               "#ae017e",
               "#7a0177",
               "#49006a"),
    breaks = c(15,20,25),
    #labels = c("<15","15 text","20 text","25 text",">25")
  )+
  guides(color = guide_colorsteps(show.limits = TRUE))

#doesn't work
mtcars %>%
  ggplot(aes(hp, mpg, color = mpg)) +
  geom_point() +
  scale_color_stepsn(
    colors = c("#dd3497",
               "#ae017e",
               "#7a0177",
               "#49006a"),
    breaks = c(15,20,25),
    labels = c("<15","15 text","20 text","25 text",">25")
  )+
  guides(color = guide_colorsteps(show.limits = TRUE))
#> Error in `check_breaks_labels()`:
#> ! `breaks` and `labels` must have the same length

Created on 2022-10-08 by the reprex package (v2.0.1)

@teunbrand
Copy link
Collaborator

I agree that this is a bit finnicky, but one way you could get the results you're after is to manually set limits, and include those limits in the breaks.

library(ggplot2)

ggplot(mtcars, aes(hp, mpg, color = mpg)) +
  geom_point() +
  scale_color_stepsn(
    colors = c("#dd3497",
               "#ae017e",
               "#7a0177",
               "#49006a"),
    limits = c(5, 35),
    breaks = c(5, 15,20,25, 35),
    labels = c("<15","15 text","20 text","25 text",">25")
  )

Created on 2022-12-15 by the reprex package (v2.0.1)

@teunbrand teunbrand added scales 🐍 guides 📏 bug an unexpected problem or unintended behavior labels Jan 7, 2023
@r2evans
Copy link
Contributor

r2evans commented Jan 12, 2024

@teunbrand, there have been several questions related to scale_*_stepsn where the addition of limits= seems to resolve it. Further, the solutions I've tested all seem to work fine with limits=range(..breaks..) + c(-1,1) (pseudocode). It would seem logical to me that this may be the intuitive behavior. (Most recent: https://stackoverflow.com/q/77807880.)

I've tried tracing into binned_scale and its ggproto and don't have the proficiency atm to determine where limits= are in-play. Are you able to explain how limits=NULL is defined internally in the context of scale_*_stepsn(.., colors=, breaks=)? If not (or if it is relatively "arbitrary"), is there the ability to adjust the default behavior so that range(..breaks..)+c(-1,1) is inferred?

@teunbrand
Copy link
Collaborator

teunbrand commented Jan 12, 2024

How the limits of a binned scale are defined is a bit awkward, as they're updated once the breaks are calculated.
The way I understand them is as follows:

If limits = NULL, a temporary 'candidate' limits equal to the data range is used for calculating the breaks.
Then, if original limits = NULL, new limits are instated based on the freshly calculated breaks. The new lower limit is equal to breaks[1] + (breaks[1] - breaks[2]) or in words, 'the first break minus the first step size' and the new upper limit is equal breaks[n] + (breaks[n] - breaks[n - 1]) or 'the last breaks plus the last step size'. That happens here (where limits is the temporary one and self$limits is user input):

ggplot2/R/scale-.R

Lines 1202 to 1230 in a4be39d

if (is.null(self$limits)) {
# Remove calculated breaks if they coincide with limits
breaks <- breaks[!breaks %in% limits]
nbreaks <- length(breaks)
if (nbreaks >= 2) {
new_limits <- c(
breaks[1] + (breaks[1] - breaks[2]),
breaks[nbreaks] + (breaks[nbreaks] - breaks[nbreaks - 1])
)
if (breaks[nbreaks] > limits[2]) {
new_limits[2] <- breaks[nbreaks]
breaks <- breaks[-nbreaks]
}
if (breaks[1] < limits[1]) {
new_limits[1] <- breaks[1]
breaks <- breaks[-1]
}
} else {
bin_size <- max(breaks[1] - limits[1], limits[2] - breaks[1])
new_limits <- c(breaks[1] - bin_size, breaks[1] + bin_size)
}
new_limits_trans <- suppressWarnings(transformation$transform(new_limits))
limits[is.finite(new_limits_trans)] <- new_limits[is.finite(new_limits_trans)]
if (is_rev) {
self$limits <- rev(transformation$transform(limits))
} else {
self$limits <- transformation$transform(limits)
}
}

So let's suppose we have breaks = c(10, 20, 30). The lower limit will be 10 + (10 - 20) == 0. The upper limit will be 30 + (30 - 20) == 40. This will create a scale that has 4 bins. The effective values assigned to those bins come from the midpoints, e.g. first bin will get value 5, second 15, third 25 and last 35. Because these don't include 0 and 40, scale_colour_stepsn() will never get you the 'pure' start and end colours. Using range(..breaks..) + c(-1, 1) works well when the range is large but not when the range is small. If you want pure-to-pure scale_colour_stepsn(), you should use something like range(..breaks..) + c(-1, 1) * sqrt(.Machine$double.eps) that is below the hexadecimal precision of colour encoding.

@r2evans
Copy link
Contributor

r2evans commented Jan 12, 2024

Thanks! That's helpful, though I think something else is also being done. Using code (slightly-modified here) from the recently-linked question, I cannot reproduce the default plot using new_limits.

Without limits=,

breaks <- c(10, 100)
nbreaks <- length(breaks)

gg <- data.frame(c = c("a", "b", "c"), v = c(1,50,500)) %>%
  ggplot(aes(y = c, x = "1", fill = v)) +
  geom_tile()
gg + scale_fill_stepsn(colors = c("black", "red", "white"), breaks = breaks)

image

If we use those apparent definitions for breaks into limits=,

new_limits <- c( 
  breaks[1] + (breaks[1] - breaks[2]), 
  breaks[nbreaks] + (breaks[nbreaks] - breaks[nbreaks - 1]) 
)
new_limits
# [1] -80 190
gg + scale_fill_stepsn(colors = c("black", "red", "white"), breaks = breaks, limits = new_limits)

image

So far I've been unable to debug ScaleBinned$get_breaks, so I'm clearly missing one of the transformation steps.

@teunbrand
Copy link
Collaborator

The second example is because your first bin gets the midpoint of (-80 + 10) / 2 == -35 and the last bin gets the midpoint (190 + 100) / 2 == 145. These will get the colour value of scales::rescale(c(-35, 145), from = c(-80, 190)) ~= 16% and 83% along the full gradient.

Relevant for the first example, and what I forgot to mention, is that the limit updating thing only kicks in if there were no manually provided breaks. You can debug ggproto methods like so:

debugonce(environment(ScaleBinned$get_breaks)$f)

@teunbrand
Copy link
Collaborator

I'm going to go ahead and say that including the limits in the break is the solution to the issue.
It is already the suggested solution in the warning message when you have atomic labels combined with show.limits = TRUE.

library(ggplot2)
ggplot(mtcars, aes(hp, mpg, color = mpg)) +
  geom_point() +
  scale_color_stepsn(
    colors = c("#dd3497",
               "#ae017e",
               "#7a0177",
               "#49006a"),
    breaks = c(15,20,25),
    labels = c("15 text","20 text","25 text")
  )+
  guides(color = guide_colorsteps(show.limits = TRUE))
#> Warning: `show.limits` is ignored when `labels` are given as a character vector.
#> ℹ Either add the limits to `breaks` or provide a function for `labels`.

Created on 2025-01-10 with reprex v2.1.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug an unexpected problem or unintended behavior guides 📏 scales 🐍
Projects
None yet
Development

No branches or pull requests

3 participants