Animating time series on a map using Solar Analytic data and R’s gganimate package!
At Solar Analytics we love data! We use our data and what we can learn from it to make important business decisions and help others navigate the changing energy landscape. Among other things, we make lots of pretty visualisations. Specifically, animated time series are useful. For example, having a way to easily generate visualisations showing our sites’ solar productions over time (see the image above) where the points represent our sites and the colour of the points demonstrates the normalised energy they produced. So, when it was suggested that a function be created to automatically do this, I decided to look into it. As a Solar Analytics intern in the Algorithms team for four months now, I’ve been learning the practical applications of data science, R and Python. It’s been a steep learning curve but I do enjoy manipulating our data to get exciting results and knew that the online data community would help me work it out.
Objectives
- Find package that can easily create time series animations on a map
- Animates up to a day’s worth of 5-minute data
- Handles 1000 + sites
- Shows chosen areas on a basic map — determined by input of longitude and latitude coordinates
- Ability to control the speed of the animation
- Saves animation as a gif
I decided pretty early on that while I am more familiar with python, it did not offer me the simplest package I needed to achieve my goal. R was the place to go. After reading many stack exchange articles I stumbled across a versatile package called gganimate. There are many examples online of very cool graphs that use this package and I knew I had found what I was looking for! That was until I hit a road bump… gganimate was updated early this year. This meant that every example online was either incompatible with the updates or not applicable to my use case. I was going to have to spend some time understanding the package and not rely on copying and pasting examples.
The good news is that I succeeded in this and am now writing this article so you have access to a nice example! My example includes sample data, my function and two examples of how it can be used.
Sample Data
I created the following data frame in Python:
- site_id: our location identification column representing different sites that we monitor.
- lon, lat: GPS coordinates of this site. NOTE: I have added noise to these fields for privacy purposes.
- t_stamp: 5-minute time stamps for each site between 7am and 8pm on the 2/3/2018.
- energy _norm: normalised values of energy produced in each time stamp on a scale of 0 to 1.
- over_voltage: Whether or not the network voltage at the site was above the specification (voltage > 253 V) in that time stamp.
Required Packages
library(readr)library(dplyr)library(ggplot2)library(ggthemes)library(gganimate)
Main Aspects of Function
Arguments
- df: data frame with all required information
- location_id (column reference): indicates column to order df by, such that the animation recognises which points correspond to one another. E.g. df$site_id
- time_stamp (column reference): indicates column of time to iterate frames over
- colour_col (column reference): indicates column which point colour is based off, this is what we are trying to graph e.g. df$energy_norm or df$over_voltage
- selected_title (string): title of animated map e.g. ‘Energy Generated’ or ‘Over Voltage’
- point_colours (vector): gives colours of points on map or colours to define a colour scale e.g. c(‘#7a4eb7’, ‘#FF6100’) or c(‘#000000’, ‘#fec533’, ‘#fec533’, ‘#fec533’, ‘#f99f1e’, ‘#f99f1e’, ‘#f99f1e’, ‘#FF6100’, ‘#FF6100’, ‘#F90707’, ‘#F90707’)
- num_frames (integer): The number of frames to render (default 100), e.g. 280
- dur (integer): The length of the animation in seconds (unset by default) e.g. 50.
Note: Decreasing num_frames or duration can result in clipping off animation so that you don’t see all required frames!
- longitude_lim (vector of length = 2): governs longitude shown on map e.g. c(-150, -140)
- latitude_lim (vector of length = 2): governs longitude shown on map e.g. c(20, 40)
Note: If longitude_lim and latitude_lim are not preferenced the function will take these limits from df$lon and df$lat
Order df
It is extremely important to correctly order your data frame. I have included the argument location_id in the function so that the df input is appropriately ordered:
df = df[order(location_id),]
Without this you could get the following result:
Here I have set
location_id = df$energy_norm
Instead of what is required:
location_id = df$site_id
Rather than demonstrating a site’s transition in energy_norm we are seeing each energy_norm’s transition around the map.
Create Map
- Choose general map location
- dictate colour of map
- Choose gps coordinates to determine area covered by map,
- Add and edit titles and legend aesthetics
base_map <- ggplot() +
borders("world", region = 'Australia',
colour = "gray85", fill = "gray80") +
theme_map() +
coord_fixed(xlim = longitude_lim, ylim = latitude_lim) +
labs(title = selected_title,
subtitle = '{closest_state}') +
theme(plot.title = element_text(hjust = 0, size = 14,
lineheight = .5),
plot.subtitle = element_text(hjust = 0, size = 12,
lineheight = 0),
legend.position = c(0,.1),
legend.title = element_blank(),
legend.text = element_text(size = 12))
Determine colouring of points on map
- If colour_level is a column of characters than point_colours should contain as many distinct colours as there are distinct entries in this column so that it may map a colour to each unique entry.
- the first and last colour in vector point_colours are used to create a colour scale if colour_level is a column of integers
colour_length = length(point_colours)if (typeof(colour_col) == 'character') {
coloured <- scale_color_manual(values = point_colours)
}
else {
coloured <- scale_color_gradientn(colours = point_colours)
}
Animate Map
- combine base_map, coloured, geom_point from ggplot2 and transition_states from gganimate to create our animation
- alpha argument in geom_point specifies transparency of points
- transition_length argument in transition_states determines the relative length of the transition.
- animate from gganimate controls animation speed and length
anim <- base_map +
coloured +
geom_point(aes(x = lon, y = lat, color = colour_col),
data = df, alpha = .5) +
transition_states(state_col, transition_length = 0)anim <- animate(anim, nframes = num_fr, duration = dur
Examples
- Melbourne Over Voltage
animated_map(df = be,
order_col = be$site_id,
state_col = be$t_stamp,
colour_col = be$over_voltage,
selected_title = 'Over Voltage',
point_colours = c('#7a4eb7', '#FF6100'),
num_fr = 280,
dur = 50)
- Melbourne Solar Generation
animated_map(df = be,
order_col = be$site_id,
state_col = be$t_stamp,
colour_col = be$energy_norm,
selected_title = 'Solar Generation',
point_colours = c('#000000', '#fec533', '#fec533',
'#fec533', '#f99f1e', '#f99f1e',
'#f99f1e', '#FF6100', '#FF6100',
'#F90707', '#F90707'),
num_fr = 280,
dur = 50)
Saving Animation
Save the last animation created as a gif:
anim_save(‘/example.gif’)
Entire Function
animated_map = function(df,
order_col,
state_col,
colour_col,
selected_title,
point_colours,
num_fr,
dur,
longitude_lim,
latitude_lim) { # ensure df is ordered correctly
df = df[order(order_col),] # Check for longitude and latitude limits
# if missing automatically set them
if (missing(longitude_lim)) {
longitude_lim = sort(c(max(df$lon), min(df$lon)))
}
if (missing(latitude_lim)) {
latitude_lim = sort(c(max(df$lat), min(df$lat)))
} # create basic map, dictate colour of map, area covered by map,
# labels and label aesthetics
base_map <- ggplot() +
borders("world", region = 'Australia',
colour = "gray85", fill = "gray80") +
theme_map() +
coord_fixed(xlim = longitude_lim, ylim = latitude_lim)
+
labs(title = selected_title,
subtitle = '{closest_state}') +
theme(plot.title = element_text(hjust = 0, size = 14,
lineheight = .5),
plot.subtitle = element_text(hjust = 0,
size = 12,
lineheight = 0),
legend.position = c(0,.1),
legend.title = element_blank(),
legend.text = element_text(size = 12)) # determine colouring of points on map
colour_length = length(point_colours)
if (typeof(colour_col) == 'character') {
coloured <- scale_color_manual(values = point_colours)
}
else {
coloured <- scale_color_gradientn(colours = point_colours)
#low = point_colours[1], high = point_colours[colour_length])
} # combine previous map lists, add points to map and animate
anim <- base_map +
coloured +
geom_point(aes(x = lon, y = lat, color = colour_col),
data = df, alpha = .5) +
transition_states(state_col, transition_length = 0)
# animate map
anim <- animate(anim, nframes = num_fr, duration = dur) return(anim)
}