Time series plots are required often, so if you do a lot of data analytics, you will find yourself making them often. I wanted to provide you two use-cases of time series plots I have made using the famous R visualization package ggplot2. I wanted to illustrate two concepts.
- If you format your dataframe properly, it is very easy to get the basic plot to run. That’s both a blessing and a challenge with ggplot2 code, but after you make a few plots, you can get used to it.
- Once you get your basic plot, you will likely find yourself staying up all night customizing your plot. It’s actually totally awesome that ggplot2 allows for such customization, but it does mean you will dump a lot of time into formatting your time series plots just right.
When you prepare to stay up all night customizing your plot, you will probably have fun when it comes to some of the modifications – choosing colors, and fussing with certain labels. But sometimes you run into challenges – usually with formatting axes or legends – that will ruin your night. I wanted to give you these two examples, because maybe my experience will help you troubleshoot whatever issue you are encountering in your ggplot2 time series plots.
Time Series Plots: Mentions in the Literature over Time
Our first plot showcased today is from an article by my colleague Dr. Audrey Pereira and myself, titled “Deeper Learning Methods and Modalities in Higher Education: A 20-year Review”. This was actually a project Dr. Pereira did, where she collected all the data herself, and she came to me for advice on visualizations. She was studying deeper learning (abbreviated DL on the plots), which is an approach to teaching in higher education.
Dr. Pereira and I both teach mostly online, but we used to teach face-to-face. Therefore, we were both intrigued by the relative lack of research into deeper learning methods for online modalities. We have been working on education studies together since about 2015, and we had noticed anecdotally that there was little in the literature about evidence-based ways to teach online.
After working with the data Dr. Pereira had collected, I proposed the following visualization:
This chart was helpful for us to see if our anecdotal observation had any evidence behind it. Dr. Pereira’s data, when analyzed this way, seemed to validate our anecdotal observation. Even in recent years, there are more articles about face-to-face and hybrid than online deeper learning modalities.
The chart is clean, but includes many lines of code. It starts with a dataset I constructed just for plotting, which I put here for you. I always have trouble visualizing how to set up the plot dataframe in ggplot2, because this is something you usually don’t have to do in SAS, and that’s the first statistics program I learned. The dataframe really needs to be in an entity-attribute-value (EAV) format that ggplot2 uses when plotting.
To that end, our dataframe has three columns: Year, Frequency, and Condition_Modality.
- Year actually is a Date/Time field set to the last day of the year I wanted to graph. You will see I had to fight with this field to get it to show up as a year on the x-axis.
- Frequency is the value I wanted to graph on the y-axis.
- Condition_Modality is a categorical variable that could take on one of three values: Face-to-face, Hybrid, or Online. The idea was to graph this variable, and produce three lines over time to compare the frequency of articles published about face-to-face, hybrid, and online deeper learning studies in higher education.
Now let’s start going through the code. I already have posted it here on Github – it is among other code, so just look at the code for the time series plot without error bars. You will see it starts with this:
timeseries_df <- readRDS("timeseries_df.rds") library(ggplot2) library(scales) library(dplyr)
The name of the dataframe as I showed above is timeseries_df, and not only do we need the ggplot2 package, but we also need the scales package to deal with getting the year to display properly on the x-axis. We also need the dplyr package, and I’m not remembering why right now – I think the scales package uses it….?
Next, we have to be sure to set the colors for the lines in a vector, as I recommend when plotting with ggplot2.
line_colors <- c("slateblue4", "springgreen4", "steelblue4")
As you could see by our dataframe printout, we have three levels of Condition_Modality, so we will have three lines. Therefore, I needed to specify three colors. For guidance in selecting these colors, I looked at a printout of mapped R colors that I found on the web.
You will find that those who did the mapping used numbers to indicate where in a value gradation the hue was – as you can see above, I used four colors from the “4” level of gradation of slateblue, springgreen, and steelblue. I’ve found that sticking with the same number of level of gradation for each color is a quick way to select a group of compatible colors for a plot from an R mapping like this.
Finally, we have the ggplot code:
ggplot(timeseries_df, aes(x = Year, y = Frequency)) + geom_line(aes(color = Condition_Modality), size = 1) + scale_color_manual(values = line_colors) + labs(color="DL Method Modality") + ylab("Frequency of DL Method Studied") + xlab("Year Article Published") + scale_x_datetime(date_breaks = "1 year", labels = date_format("%Y")) + theme(axis.text.x = element_text(angle=90))
I will curate here some lines of code that I feel warrant further discussion:
- Notice that for the time series chart, the object declared in ggplot2 is geom_line (as opposed to geom_boxplot as I demonstrated in my box plot post), and Condition_Modality is the argument, because that determines the color of the line.
- How I got ggplot2 to use my line_colors vector for the colors was by using scale_color_manual and declaring the name of the vector as the argument. This is where things go wrong if you don’t have the same number of entries in your color vector as you have levels of your categorical variable.
- The labs argument puts a title on the legend (obviously, LOL!)
- ylab and xlab place axis labels
- scale_x_datetime is from the scales package. You actually need to use this hack if you want to use regular time increments (years, days, hours) on your x-axis. Otherwise, it normally does NOT show up right. When I say you stay up all night on ggplot2 – well, I spent all day trying to figure this scales thing out when we were writing the original article.
- Lastly, I apply a theme to turn the axis titles 90 degrees. You will see if you run the plot code without the last line, the year labels are horizontal and overlap each other.
Time Series Plots: Size of Inhibition Zones Over Time
Our second and final time series plot today is one I did for a laboratory study. In the study, the researcher used agar dishes and swabbed them with a solution containing an infective organism. Then, she put two different experimental antibiotic solutions in the middle of the plates: MS, and MSN. The idea is that over time, the bacteria would not grow in an area around where the solutions were placed, and those would create “inhibition zones”. The bigger the inhibition zone, the better the antibiotic was at killing the bacteria.
Measurements were taken at three (3) time increments: Time 1, Time 2, and Time 3. The researcher did not know if MS or MSN was better from just looking at the values in the data. There were very few agar plates in each condition, so I told the researcher that we would need to use median inhibition zone rather than means (and the interquartile range [IQR] for the lower and upper limits). If you are having a flashback to basic statistics, watch my videos below.
We used the visualization I proposed, which is here.
As you can see, it looks like MS was much better at killing bacteria compared to MSN only at Time 2. At Time 1 and Time 3, they performed pretty much the same.
So, to get this plot, you will see I put some code and a dataframe on Github for you. The dataframe is called timeseries_error_colors.rds, and here is what it looks like.
As you can see, the plot dataset has five columns. The time column refers to the time the agar dish was measured – Times 1, 2, or 3 – and because this variable was an integer, I did not have to use the scale_x_datetime command as I did in the last plot. As I said before, we used the median as the measure of central tendency since there were so few plates per condition, so then I calculated the LL and UL variables (for lower and upper limits, respectively) as the 25th and 75th percentiles, respectively. The group variable indicates whether the row refers to measurements of inhibition zones for the MS group or the MSN group. We are making one line per group for a total of two lines.
Now, let’s look at the code.
timeseries_errorbars_df <- readRDS("timeseries_errorbars_df.rds") library(ggplot2) timeseries_error_colors <- c("red4", "navyblue")
These first few lines of code read in the plot dataframe, call up the ggplot2 library, and set a color vector called timeseries_error_colors to the colors mapped to red4 and navyblue. I am in the United States, and I was feeling patriotic.
Now, let’s look at the plot code.
ggplot(timeseries_errorbars_df, aes(x = Time, y = Median)) + geom_line(aes(color = Group), size = 1) + geom_text(data=timeseries_errorbars_df, show.legend = FALSE, aes(x= Time - .1, y = Median + .5, label = round(Median, 0), color = Group)) + scale_color_manual(values = timeseries_error_colors) + geom_errorbar(data=timeseries_errorbars_df, mapping=aes(x = Time, ymin=LL, ymax=UL), width=0.1, size=.5, color="black") + labs(color="Group") + ylab("Inhibition Zone (mm)") + xlab("Time Measured") + ylim(0, 20) + scale_x_continuous(breaks = c(1,2,3)) + theme(axis.text.x = element_text(angle=90)) + theme_classic()
Let’s walk through some nuances in this code:
- As you probably guessed, and as it shows in the aes argument, we are graphing the Time variable along the x-axis, and the Median variable along the y-axis.
- Again, we use the shape geom_line, and because we want one line for each group, MS and MSN, we set color equal to Group
- The next line, geom_text, tells R to put the data labels in. Basically, it says to place the value of the Median rounded to 0 decimal places at the coordinates designated by x= Time – .1, y = Median + .5. This approach is typically how data values are placed on a ggplot2 plot. The value is taken from a variable, and then the x and y coordinates of where to place the label are based on x and y coordinates being graphed plus some padding. This is so the labels are not overwriting each other or the line.
- Again, we use scale_color_manual to apply our manual color vector timeseries_error_colors.
- Next, we place the shape geom_errorbar to get our error bars. I use the mapping option and set the arguments for the aes, which are x, ymin, and ymax (to the Median, LL, and UL, respectively), which makes sense as the minimal definition of an error bar. I also add some formatting.
- Again, labs is “obviously” the command for naming the legend, and I place axis labels using ylab and xlab. Also, I set the limits of the y-axis using the ylim option. If you want to fuss with the y limits because you don’t know exactly what you want, you can always create a numeric vector with the values, and set the arguments to the name of the numeric vector, as I demonstrate in this blog post.
- You’d think an x-axis on a time series plot would be the least of your problems, but it’s a headache in ggplot2. Again, I was having trouble getting ggplot2 to make exactly three time points on the graph – 1, 2, and 3 – to match the study design. After fighting with it, I realized I had to use scale_x_continuous and set the breaks argument to 1, 2, and 3 to get it to display the way I wanted. Again, you can do this with a numeric vector instead of hardcoding it like I did.
- Finally, I needed the theme option to turn the x-axis text 90 degrees so it was horizontal, and I applied theme_classic() to give the chart a clean look.
Updated April 11, 2022. Added banners March 6, 2023. Revised banners June 26, 2023.
Time series plots in R are totally customizable using the ggplot2 package, and can come out with a look that is clean and sharp. However, you usually end up fighting with formatting the x-axis and other options, and I explain in my blog post.