Goal
Use ggplot2
(latest version) to produce a graph that duplicates the x- or y-axis on both sides of the plot, where the scale is not continuous.
Minimal Reprex
# Example data
dat1 <- tibble::tibble(x = c(rep("a", 50), rep("b", 50)),
y = runif(100))
# Standard scatterplot
p1 <- ggplot2::ggplot(dat1) +
ggplot2::geom_boxplot(ggplot2::aes(x = x, y = y))
When the scale is continuous, this is easy to do with an identity transformation (clearly one-to-one).
# This works
p1 + ggplot2::scale_y_continuous(sec.axis = ggplot2::sec_axis(~ .))
However, when the scale is not continuous, this doesn't work, as other scale_*
functions don't have a sec.axis
argument (which makes sense).
# This doesn't work
p1 + ggplot2::scale_x_discrete(sec.axis = ggplot2::sec_axis(~ .))
Error in discrete_scale(c("x", "xmin", "xmax", "xend"), "position_d", :
unused argument (sec.axis = <environment>)
I also tried using the position
argument in the scale_*
functions, but this doesn't work either.
# This doesn't work either
p1 + ggplot2::scale_x_discrete(position = c("top", "bottom"))
Error in match.arg(position, c("left", "right", "top", "bottom")) :
'arg' must be of length 1
Edit
For clarity, I was hoping to duplicate the x- or y-axis where the scale is anything, not just discrete (a factor variable). I just used a discrete variable in the minimal reprex for simplicity.
For example, this issue arises in a context where the non-continuous scale is datetime
or time
format.
Duplicating (and modifying) discrete axis in ggplot2
You can adapt this answer by just putting the same labels on both sides. As far as "you can convert anything non-continuous to a factor, but that's even more inelegant!" from your comment above, that's what a non-continuous axis is, so I'm not sure why that would be a problem for you.
TL:DR Use
as.numeric(...)
for your categorical aesthetic and manually supply the labels from the original data, usingscale_*_continuous(..., sec_axis(~., ...))
.Edited to update:
I happened to look back through this thread and see that it was asked for dates and times. This makes the question worded incorrectly: dates and times are continuous not discrete. Discrete scales are factors. Dates and times are ordered continuous scales. Under the hood, they're just either the days or the seconds since "1970-01-01".
scale_x_date
will indeed throw an error if you try to pass asec.axis
argument, even if it'sdup_axis
. To work around this, you convert your dates/times to a number, and then fool your scales using labels. While this requires a bit of fiddling, it's not too complicated.I just made a simple vector of 11 days (0 to 10) added to "2017-08-01". If you run
as.numeric
on that, you get the number of days since the beginning of the Unix epoch. (see?lubridate::as_date
).When you plot
tm_num
againsty
, it's treated just like normal numbers, and you can usescale_x_continuous(sec.axis = dup_axis(), ...)
. Then you have to figure out how many breaks you want and how to label them.The
breaks =
is a function that takes the limits of the data, and calculates nice looking breaks. First you round the limits, to make sure you get integers (dates don't work well with non-integers). Then you generate a sequence of your desired width (thedays(2)
). You could useweeks(1)
ormonths(3)
or whatever, check out?lubridate::days
. Under the hood,days(x)
generates a number of seconds (86400 per day, 604800 per week, etc.),as_date
converts that into a number of days since the Unix epoch, andas.numeric
converts it back to an integer.The
labels =
is a function takes the sequence of integers we just generated and converts those back to displayable dates.This also works with times instead of dates. While dates are integer days, times are integer seconds (either since the Unix epoch, for datetimes, or since midnight, for times).
Let's say you had some observations that were on the scale of minutes, not days.
The code would be similar, with a few tweaks:
Here I updated the dummy data to span 11 minutes from just before midnight to just after midnight. In
breaks =
I modified it to make sure I got an integer number of minutes to create breaks on, changedas_date
toas_datetime
, and usedminutes(2)
to make a break every two minutes. Inlabels =
I added a functionalstamp(...)(...)
, which creates a nice format to display.Finally just times.
Here we've got an observation every millisecond for 11 hours starting at t = 20min34.567sec. So in
breaks =
we dispense with any rounding, since we don't want integers now. Then we use breaks everymilliseconds(2)
. Thenlabels =
needs to be formatted to accept decimal seconds, the "%OS3" means 3 digits of decimals for the seconds place (can accept up to 6, see?strptime
).Is all of this worth it? Probably not, unless you really really want a duplicated time axis. I'll probably post this as an issue on the
ggplot2
GitHub, becausedup_axis
should "just work" with datetimes.Option 1: This is not very elegant but it works using the
cowplot::align_plots
function:Option 2:
Note: an answer to similar problem was posted [here] using the cowplot package (Duplicating Discrete Axis in ggplot2), but it didn't work for me. The
cowplot::switch_axis_position()
function has been deprecated.