Skip to main content

Section 6.5 Spatio-temporal data visualisation

For the next set of exercises, we are going to look at temporal variations on burglary across Greater Manchester. We are going to focus on wards as the unit of analysis. Accordingly, we will need to import another data set, this time a geojson which contains the wards (administrative boundaries) for Manchester, and the monthly burglary count for each ward in 2018. To load the ward geometries for Manchester into a sf object, we use code we had already used in previous chapters.
mcr_burglary <- st_read("data/mcr_burglary.geojson")
With this data in our environment, we can plot a map using tmap.
library(tmap)

tm_shape(mcr_burglary) +
  tm_fill("burglaries") +
  tm_borders()
The wards of Manchester separated by a thin black border, shaded in shades of blue. A legend to the bottom right, labeled ’burglaries’ matches boxes of six shades of blue to ranges of values, twenty at a time. The map only uses the first three shades of blue, with only two areas near the middle using the third one.
Figure 6.5.1. Burglaries in Manchester
So this is something we’re already familiar with. But how can we map the temporal information? We will now cover some approaches.

Subsection 6.5.1 Small multiples to show temporal variation

As noted by [Pebesma (2012)], spatio-temporal data often come in the form of single tables expressed in one of the three following formats:
  • time-wide where different columns reflect different moments in time,
  • space-wide where different columns reflect different measurement locations or areas, or
  • long formats where each record reflects a single time and space combination.
What we have in the mcr_burglary object if we view the data frame is a table expressed in the long format, where each row represents a single month and ward combination. We can see as well that this is not simply a data table but a sf object which embeds the geographical information that allow us to place it in a map. We can now try to produce the small multiples with the tm_facets() function. (Note: if you wanted to use ggplot2 instead of tmap, you can look up the function facet_wrap() to achieve the same results).
tm_shape(mcr_burglary) +
  tm_fill("burglaries") +
  tm_borders() +
  tm_facets("month", free.coords=FALSE)
Twelve copies of the previous map, each with the label 2018 dash one through twelve, are tiled in a six by two grid. The maps have different shadings. The same legend from the previous figure has moved to the right of these maps. Each map is shaded slightly differently, although central Manchester is consistently the darkest. It appears the darkest in the fifth map, corresponding to May.
Figure 6.5.2. Small multiples of burglary across Manchester by month
So this is one way to visualise temporal variation. What are some more?

Subsection 6.5.2 Spaghetti plots

In longitudinal studies and in studies looking at areas over time, sometimes researchers use spaghetti plots. On their own they are not great, but they can be used when one wants to put a trajectory within a broader context or when comparing different trajectories. You can read more about how to not use them in [Nussbaumer (2013)].
While we will include the ward name as a spatial component in a way, this isn’t technically spatio-temporal data visualisation, as we are stripping away any sort of spatial element, and keeping ward name only as a nominal variable. It is not great, but let’s show you why you might use it. The basic concept is just the same line chart we saw earlier, but with the trajectories grouped within the wards. So instead of one line, we will get 32: one for each ward in our data.
ggplot(mcr_burglary, aes(x = month, y = burglaries, group = ward_name)) +
  geom_line() + theme_minimal()
Many line charts in one plot, with vertical axis labeled ’burglaries’ ranging from zero to one hundred, and horizontal axis labeled ’month’, ranging through the months of 2018. Most of the lines form a mess between values of five and thirty, with a few lines rising above this, and one notable line staying above fifty nearly each month.
Figure 6.5.3. Spaghetti plot of burglaries by ward
This is quite the mess. So in what situation may this be useful? Well, maybe you want to compare the variation here with some central tendency. For this we can "grey out" the lines (by changing the colour) and add some summary such as the median, let’s say, by using the stat_summary() function.
ggplot(mcr_burglary, aes(x = month,  y = burglaries, group = ward_name)) +
  geom_line(color="darkgrey") +
  stat_summary(aes(group = 1), # we want to summarise all the data together
               geom = "line",  # geometry to display the summary
               fun.y = median, # function to apply
              lwd = 1.5) +   # specifying thick line for attention
  theme_minimal()
The previous plot of line chars, with all but one of the lines turned light grey. The one line which has stayed black, is slightly thicker, and hovers around twenty the whole year.
Figure 6.5.4. Spaghetti plot of burglaries by ward showing variation around central tendency measure
This way we can show something like the median, trajectory compared with the individual observations’ trajectories. We could also use colour to highlight a specific area compared to the rest of the wards. For example, if we are interested in Fallowfield ward, we might highlight that as an additional layer in our spaghetti pile:
ggplot(mcr_burglary, aes(x = month, y = burglaries, group = ward_name)) +
  geom_line(color="darkgrey") +
  geom_line(data = mcr_burglary %>%    # pass new data in this layer
              filter(ward_name == "Fallowfield"),  #filter only Fallowfield
            aes(x = month,
           y = burglaries),
           colour = "blue",  # change colour for emphasis
           lwd = 1.5) + # change line width for emphasis
  theme_minimal()
The same plot, but this time another line has been chosen. It is in a thicker blue colour, while all other lines are light grey. It fluctuates between around ten and thirty.
Figure 6.5.5. Spaghetti plot to highlight burglaries in Fallowfield ward
We can now maybe draw some conclusions about Fallowfield’s burglary trajectory compared with the other wards in our data set. But let’s move on now to the final approach, where we again make use of our spatial component.

Subsection 6.5.3 Animations

The final way we will present here to visualise time and place together is through the use of animations. This feature is brought to ggplot2 thanks to the gganimate extension. The idea behind this display is really similar to that behind the small multiples, introduced earlier. A separate map is created for each month, and these are displayed near one another in order to show the change over time. However, instead of side-by-side, these maps are sequential — they appear in the same place but one after another, in order to present change. So first thing we do is to load the gganimate package:
library(gganimate)
Also, to apply gganimate to sf objects, you need to have a package called transformr installed. You don’t need to load this, but make sure it is installed! If not, install with install.packages(transformr). Then, we need to make sure that our temporal variable is a date object. We can use the ymd() function, from the fantastic lubridate package (really I cannot praise this package enough, it makes handling dates so easy!) to make sure that our month variable is a date object. One thing you might notice looking at this data is that there is no date associated with each observation, only month and year. How can we use ymd() which clearly requires year, month and day! Well, one approach is to make this up, and just say that everything in our data happened on the 1st of the month. We can use the paste0() function to do this:
mcr_burglary$date_month <- ymd(paste0(mcr_burglary$month, "-01"))
Now, we can create a simple static plot, the way we already know how. Let’s plot the number of burglary incidents per ward, and save this in an object called anim:
anim <- ggplot() +
  geom_sf(data = mcr_burglary, aes(fill = burglaries)) +
  theme_void()
Now, finally, we can animate this graph. Take the object of the static graph (anim) and add a form of transition, which will be used to animate the graph. In this case, we can use transition_states(). This transition splits your data into multiple states based on the levels in a given column, much like how faceting splits up the data in multiple panels. It then shifts between the defined states and pauses at each state. Layers with data without the specified column will be kept constant during the animation (again, mimicking facet_wrap). States are the unquoted name of the column holding the state levels in the data. You can then use closest_state to dynamically label the graph:
anim +
  transition_states(date_month,  # column holding the state levels in the data
                    transition_length = 1, # relative length of the transition
                    state_length = 2) + # elative length of the pause
  labs(title = "Month: {closest_state}")
In the print book, you will not see this result, so it’s not very exciting, but hopefully you are following along, and in your own R environment you are now are looking at a very smooth animation of how burglary changed over the months of 2018 in Manchester.