Axis labels on two lines with nested x variables (year below months)
The code below provides two potential options for adding year labels.
Option 1a: Faceting
You could use faceting to mark the years. For example:
library(ggplot2)
library(lubridate)
ggplot(df, aes(Date, value)) +
geom_line() +
scale_x_date(date_labels="%b", date_breaks="month", expand=c(0,0)) +
facet_grid(~ year(Date), space="free_x", scales="free_x", switch="x") +
theme_bw() +
theme(strip.placement = "outside",
strip.background = element_rect(fill=NA,colour="grey50"),
panel.spacing=unit(0,"cm"))
Note that with this approach, if there are missing dates at the beginning or end of a year (by "missing", I mean rows for those dates are not even present in the data) then the x-axis will start/end at the first/last date in the data for that year, rather than go from Jan-1 to Dec-31. In that case, you'd need to add in rows for the missing dates and either NA
for value
or interpolate value
. In addition, with this method there is no space or line between December 31 of one year and January 1 of the next year, so there's a discontinuity across each year.
Option 1b: Faceting + centered month labels
To address @AF7's comment. You can center the month labels by adding some spaces before each label. But you have to choose the number of spaces manually, depending on the physical size of the plot when you print it to a device. (There's probably a way to center the labels programmatically based on the internal grob measurements, but I'm not sure how to do it.) I've also removed the minor vertical gridlines and lightened the line between years.
ggplot(df, aes(Date, value)) +
geom_line() +
scale_x_date(date_labels=paste(c(rep(" ",11), "%b"), collapse=""),
date_breaks="month", expand=c(0,0)) +
facet_grid(~ year(Date), space="free_x", scales="free_x", switch="x") +
theme_bw() +
theme(strip.placement = "outside",
strip.background = element_blank(),
panel.grid.minor.x = element_blank(),
panel.border = element_rect(colour="grey70"),
panel.spacing=unit(0,"cm"))
Option 2a: Edit the x-axis label grob
Here's a more complex and finicky method (though it could likely be automated by someone who understands the structure and unit spacings of grid graphics better than I do) that avoids the pitfalls of the faceting method described above:
library(grid)
# Fake data with an extra year added for illustration
set.seed(2)
df = data.frame(Date=seq(as.Date("1718-03-01"),as.Date("1721-09-20"), by="1 day"))
df$value = cumsum(rnorm(nrow(df)))
# The plot we'll start with
p = ggplot(df, aes(Date, value)) +
geom_vline(xintercept=as.numeric(df$Date[yday(df$Date)==1]), colour="grey60") +
geom_line() +
scale_x_date(date_labels="%b", date_breaks="month", expand=c(0,0)) +
theme_bw() +
theme(panel.grid.minor.x = element_blank()) +
labs(x="")
Now we want to add the year values below and in between June and July of each year. The code below does that by modifying the x-axis label grob and is adapted from this SO answer by @SandyMuspratt.
# Get the grob
g <- ggplotGrob(p)
# Get the y axis
index <- which(g$layout$name == "axis-b") # Which grob
xaxis <- g$grobs[[index]]
# Get the ticks (labels and marks)
ticks <- xaxis$children[[2]]
# Get the labels
ticksB <- ticks$grobs[[2]]
# Edit x-axis label grob
# Find every index of Jun in the x-axis labels and add a newline and
# then a year label
junes = which(ticksB$children[[1]]$label == "Jun")
ticksB$children[[1]]$label[junes] = paste0(ticksB$children[[1]]$label[junes],
"\n ", unique(year(df$Date)))
# Put the edited labels back into the plot
ticks$grobs[[2]] <- ticksB
xaxis$children[[2]] <- ticks
g$grobs[[index]] <- xaxis
# Draw the plot
grid.newpage()
grid.draw(g)
Option 2b: Edit the x-axis label grob and center the month labels
Below is the only change that needs to be made to Option 2a to center the month labels, but, once again, the number of spaces needs to be tweaked manually.
# Make the edit
# Center the month labels between ticks
ticksB$children[[1]]$label = paste0(paste(rep(" ",7),collapse=""), ticksB$children[[1]]$label)
# Find every index of Jun in the x-axis labels and a year label
junes = grep("Jun", ticksB$children[[1]]$label)
ticksB$children[[1]]$label[junes] = paste0(ticksB$children[[1]]$label[junes], "\n ", unique(year(df$Date)))
ggplot2: Add secondary x label (year below months)
library(tidyverse)
#data:
set.seed(122)
df <- as_tibble(rlnorm(1260, meanlog = 0.06, sdlog = 0.20))
#> Warning: Calling `as_tibble()` on a vector is discouraged,
#> because the behavior is likely to change in the future.
#> Use `tibble::enframe(name = NULL)` instead.
df$month <- rep(c("Jan", "Feb", "Mär", "Apr", "Mai", "Jun",
"Jul", "Aug", "Sep", "Okt", "Nov", "Dez"), 5, each=21)
df$year <- rep(c("Year 1", "Year 2", "Year 3", "Year 4", "Year 5" ), 1, each=252)
#solution:
month_lab <- rep(unique(df$month), length(unique(df$year)))
year_lab <- unique(df$year)
df %>%
as.data.frame() %>%
rename(price = 1) %>%
mutate(rnames = rownames(.)) %>%
ggplot(aes(x = as.numeric(rnames), y = price,
group = year)) +
geom_line() +
labs(title = "Stock Price Chart", y = "Price", x = "date") +
scale_x_continuous(breaks = seq(1, 1260, by = 21),
labels = month_lab, expand = c(0,0)) +
facet_grid(~year, space="free_x", scales="free_x", switch="x") +
theme(strip.placement = "outside",
strip.background = element_rect(fill=NA,colour="grey50"),
panel.spacing=unit(0,"cm"))
Created on 2019-05-28 by the reprex package (v0.3.0)
Producing two lines of complex y axis titles using atop, \n, etc in ggplot in R
You could try using element_markdown
from the ggtext
package. This allows you to use markdown (or html) to produce the line breaks, symbols, subscripts and superscripts you need without resorting to bquote
, which struggles with multi-line inputs. You can even show the units in italics, as in this example:
library(ggplot2)
library(ggtext)
ggplot(data = iris, aes(x = Sepal.Width, y = Sepal.Length)) +
geom_point() +
ylab("Efflux<br><i>(μmol CO<sub>2</sub> m<sup>-2</sup>s<sup>-1</sup>)</i>") +
theme(axis.title.y = element_markdown())
How to add months on top of daily data on the x-axis in ggplot2?
Update
Here is one way to do it using a function as labels
argument in scale_x_continuous
. This works only if the breaks
contains all days as well as the regular breaks (100, 200 etc.). Since this messes up the break lines and axis ticks (they are not symmetrically anymore) I've hidden them with element_blank()
and inserted custom break lines with geom_vline
.
library(tidyverse)
# some toy data similar to the original data
dat <- tibble(Day = 1:366,
mean = 2+rnorm(366))
# setup breaks
month <- c(31,60,91,121,152,182,213,244,274,305,335,365)
breaks <- seq(100,300,100)
mybreaks <- sort(c(month,breaks))
precip_plots <- ggplot(data = dat,
aes_string(x = "Day",
y = "mean",
group = 1)) +
geom_line(size = 1) +
scale_x_continuous(
breaks = mybreaks,
# custom function using `dplyr::case_when`
labels = function(x) {
case_when(
x == 31 ~ "\nJ",
x == 60 ~ "\nF",
x == 91 ~ "\nM",
x == 121 ~ "\nA",
x == 152 ~ "\nM",
x == 182 ~ "\nJ",
x == 213 ~ "\nJ",
x == 244 ~ "\nA",
x == 274 ~ "\nS",
x == 305 ~ "\nO",
x == 335 ~ "\nN",
x == 365 ~ "\nD",
x %% 100 == 0 ~ as.character(x))
}) +
# creates fake break lines
geom_vline(xintercept = breaks,
color = "#d6d6d6",
size = 0.5) +
theme_bw() +
# hides original break lines and axis ticks
theme(panel.grid.major.x = element_blank(),
panel.grid.minor.x = element_blank(),
axis.ticks.x = element_blank()
) +
labs(title = "Daily precipitation",
x = "Day",
y = "Precipitation (cm)")
precip_plots
Created on 2021-08-07 by the reprex package (v0.3.0)
Multirow axis labels with nested grouping variables
You can create a custom element function for axis.text.x
.
library(ggplot2)
library(grid)
## create some data with asymmetric fill aes to generalize solution
data <- read.table(text = "Group Category Value
S1 A 73
S2 A 57
S3 A 57
S4 A 57
S1 B 7
S2 B 23
S3 B 57
S1 C 51
S2 C 57
S3 C 87", header=TRUE)
# user-level interface
axis.groups = function(groups) {
structure(
list(groups=groups),
## inheritance since it should be a element_text
class = c("element_custom","element_blank")
)
}
# returns a gTree with two children:
# the categories axis
# the groups axis
element_grob.element_custom <- function(element, x,...) {
cat <- list(...)[[1]]
groups <- element$group
ll <- by(data$Group,data$Category,I)
tt <- as.numeric(x)
grbs <- Map(function(z,t){
labs <- ll[[z]]
vp = viewport(
x = unit(t,'native'),
height=unit(2,'line'),
width=unit(diff(tt)[1],'native'),
xscale=c(0,length(labs)))
grid.rect(vp=vp)
textGrob(labs,x= unit(seq_along(labs)-0.5,
'native'),
y=unit(2,'line'),
vp=vp)
},cat,tt)
g.X <- textGrob(cat, x=x)
gTree(children=gList(do.call(gList,grbs),g.X), cl = "custom_axis")
}
## # gTrees don't know their size
grobHeight.custom_axis =
heightDetails.custom_axis = function(x, ...)
unit(3, "lines")
## the final plot call
ggplot(data=data, aes(x=Category, y=Value, fill=Group)) +
geom_bar(position = position_dodge(width=0.9),stat='identity') +
geom_text(aes(label=paste(Value, "%")),
position=position_dodge(width=0.9), vjust=-0.25)+
theme(axis.text.x = axis.groups(unique(data$Group)),
legend.position="none")
Related Topics
Why Is '[' Better Than 'Subset'
Aggregating by Unique Identifier and Concatenating Related Values into a String
Subset Rows Corresponding to Max Value by Group Using Data.Table
What Are the Differences Between "=" and "≪-" Assignment Operators
How to Debug "Contrasts Can Be Applied Only to Factors With 2 or More Levels" Error
Error in If/While (Condition) {: Missing Value Where True/False Needed
How to Select the Rows With Maximum Values in Each Group With Dplyr
What Specifically Are the Dangers of Eval(Parse(...))
Drop Data Frame Columns by Name
Using Reshape from Wide to Long in R
General Suggestions For Debugging in R
Filter Rows Which Contain a Certain String
Explicitly Calling Return in a Function or Not
Count Occurrences of Value in a Set of Variables in R (Per Row)