Customizing time and date scales in ggplot2

Lea Waniek Blog, Data Science, Statistik

In the last post of this series we dealt with axis systems. In this post we are also dealing with axes but this time we are taking a look at the position scales of dates, time and datetimes. Since we at STATWORX are often forecasting – and thus plotting – time series, this is an important issue for us. The choice of axis ticks and labels can make the message conveyed by a plot clearer. Oftentimes, some points in time are – e.g. due to their business implications – more important than others and should be easily identified. Unequivocal, yet parsimonious labeling is key to the readability of any plot. Luckily, ggplot2 enables us to do so for dates and times with almost any effort at all.

We are using ggplot`s economics data set. Our base Plot looks like this:


# base plot
base_plot <- ggplot(data = economics) +
  geom_line(aes(x = date, y = unemploy), 
            color = "#09557f",
            alpha = 0.6,
            size = 0.6) +
  labs(x = "Date", 
       y = "US Unemployed in Thousands",
       title = "Base Plot") +

Scale Types

As of now, ggplot2 supports three date and time classes: POSIXct, Date and hms.
Depending on the class at hand, axis ticks and labels can be controlled by using scale_*_date, scale_*_datetime or scale_*_time, respectively. Depending on whether one wants to modify the x or the y axis scale_x_* or scale_y_* are to be employed. For sake of simplicity, in the examples only scale_x_date is employed, but all discussed arguments work just the same for all mentioned scales.

Minor Modifications

Let’s start easy. With the argument limits the range of the displayed dates or time can be set. Two values of the correct date or time class have to be supplied.


 base_plot +
   scale_x_date(limits = as.Date(c("1980-01-01","2000-01-01"))) +
   ggtitle("limits = as.Date(c(\"1980-01-01\",\"2000-01-01\"))")

The expand argument ensures that there is some distance between the displayed data and the axes. The argument expand takes two numeric values, the first is the multiplicative expansion constant, the second the additive expansion constant. The larger one of the two distances is employed in the plot, the multiplicative constant is multiplied with the range of the displayed data, the additive is multiplied with one unit of the depicted data. The resulting empty space is added at the left and right end of the x-axis or the top and bottom of the y-axis.


 base_plot +  
 scale_x_date(expand = c(0, 5000)) +   #5000/365 = 13.69863 years
   ggtitle("expand = c(0, 5000)")

position argument defines where the labels are displayed: Either “left” or “right” from the y-axis or on the “top” or on the “bottom” of the x-axis.


 base_plot +  
   scale_x_date(position = "top") +
   ggtitle("position = \"top\"")

Axis Ticks and Grid Lines

More essential than the cosmetic modifications discussed so far are the axis ticks. There are several ways to define the axis ticks of dates and times. There are the labelled major breaks and further the minor breaks, which are not labeled but marked by grid lines. These can be customized with the arguments breaks and minor_breaks, respectively. The breaks as the well as minor_breaks can be defined by a numeric vector of exact positions or a function with the axis limits as inputs and breaks as outputs. Alternatively, the arguments can be set to NULL to display (minor) breaks at all. These options are especially handy if irregular intervals between breaks are desired.


 base_plot +  
  scale_x_date(breaks = as.Date(c("1970-01-01", "2000-01-01")),
               minor_breaks = as.Date(c("1975-01-01", "1980-01-01",
                                        "2005-01-01", "2010-01-01"))) +
   ggtitle("(minor_)breaks = fixed Dates")


 base_plot +  
   scale_x_date(breaks = function(x) seq.Date(from = min(x), 
   											  to = max(x), 
   											  by = "12 years"),
                minor_breaks = function(x) seq.Date(from = min(x), 
                									to = max(x), 
                									by = "2 years")) +
   ggtitle("(minor_)breaks = custom function")


 base_plot +  
  scale_x_date(breaks = NULL,
               minor_breaks = NULL) +
   ggtitle("(minor_)breaks = NULL")

Another and very convenient way to define regular breaks are the date_breaks and the date_minor_breaks argument. As input both arguments take a character vector combining a string specifying the time unit (either “sec", "min", "hour", "day", "week", "month" or "year") and an integer specifying number of said units specifying the break intervals.


base_plot +
  scale_x_date(date_breaks = "10 years",
               date_minor_breaks = "2 years") +
  ggtitle("date_(minor_)breaks = \"x years\"")

If both are given, date(_minor)_breaks overrules (minor_)breaks.

Axis Labels

Similar to the axis ticks, the format of the displayed labels can either be defined via the labels or the date_labels argument. The labels argument can either be set to NULL if no labels should be displayed, with the breaks as inputs and the labels as outputs. Alternatively, a character vector with labels for all the breaks can be supplied to the argument. This can be very useful, since like this virtually any character vector can be used to label the breaks. The number of labels must be the same as the number of breaks. If the breaks are defined by a function, date_breaks or by default the labels must be defined by a function as well.


base_plot +
  scale_x_date(date_breaks = "15 years",
               labels = function(x) paste((x-365), "(+365 days)")) +
  ggtitle("labels = custom function") 


base_plot +
  scale_x_date(breaks = as.Date(c("1970-01-01", "2000-01-01")),
               labels = c("~ '70", "~ '00")) +
  ggtitle("labels = character vector")   

Furthermore and very conveniently, the format of the labels can be controlled via the argument date_labels set to a string of formatting codes, defining order, format and elements to be displayed:

%Ssecond (00-59)
%Mminute (00-59)
%lhour, in 12-hour clock (1-12)
%Ihour, in 12-hour clock (01-12)
%Hhour, in 24-hour clock (01-24)
%aday of the week, abbreviated (Mon-Sun)
%Aday of the week, full (Monday-Sunday)
%eday of the month (1-31)
%dday of the month (01-31)
%mmonth, numeric (01-12)
%bmonth, abbreviated (Jan-Dec)
%Bmonth, full (January-December)
%yyear, without century (00-99)
%Yyear, with century (0000-9999)

Source: Wickham 2009 p. 99


base_plot +
  scale_x_date(date_labels = "%Y (%b)") +
  ggtitle("date_labels = \"%Y (%b)\"") 

The choice of axis ticks and labels might seem trivial. However, one should not underestimate the amount of confusion that can be caused by too many, too less or poorly positioned axis ticks and labels. Further, economical yet clear labeling of axis ticks can increase the readability and visual appeal of any time series plot immensely. Since it is so easy to tweak the date and time axes in ggplot2 there is simply no excuse not to do so.


  • Wickham, H. (2009). ggplot2: elegant graphics for data analysis. Springer.
Über den Autor
Lea Waniek

Lea Waniek

I am data scientist at STATWORX, apart from machine learning, I love to play around with RMarkdown and ggplot2, making data science beautiful inside and out.


is a consulting company for data science, statistics, machine learning and artificial intelligence located in Frankfurt, Zurich and Vienna. Sign up for our NEWSLETTER and receive reads and treats from the world of data science and AI. If you have questions or suggestions, please write us an e-mail addressed to blog(at)