Visualizing Direction in Running Routes >

Visualizing Direction in Running Routes

By Max Candocia


May 17, 2020

Since the Lakefront Trail in Chicago was closed by the mayor, I have been running a wide variety of different routes. Oftentimes I take the same streets in different runs, but going different directions. I wanted a good way to visualize the directions of the path while I ran.

Normally, arrows or tapering lines can be used to visualize direction with paths, but those do not work very well with a lot of overlap or particularly long or complex paths, so I came up with a simple method below:

  1. Calculate the angle at each point in time by looking at the change in position 5 seconds into the future
  2. Use a cyclical color scale that is easy to read and has almost the same color at 365 degerees as 0 degrees

I used the `ggmap` package in R to download Stamen maps (which are derived from OpenStreetMap maps), along with the `cetcolor` package, which contains a cyclical color map that I used. I used a sample of 26 runs as examples, as well as an almost-circle generated sample that anyone can recreate.


Use the below button to switch between a red-only map and the multicolored map.

Generated Sample

26-run Sample


Below you can see the code I used to generate the graphs. The constants at the top can be changed according to your local directories. The cyclical color legends are created using `gridExtra` and a separate plot, as I do not know how to actually generate such a legend in R otherwise.

Note the 3 required columns for the CSV are position_lat, position_long, and eid, where eid is an integer representing event ID (so that multiple routes in the same file are not connected).

Also note that the last 5 points of each event are not plotted due to the 5-second lag calculation for the angles.


# constants
# from cetcolor::cet_pal(8, 'c2s')

COORDINATE_FILENAME = '/your/directory/here/filename.csv'


PREFIX = ifelse(

# get data, either by generating or loading
  # create an almost-circle
  # note: eid = event ID, which normally separates different sets of paths
  tval = 0:1000
  df = data.frame(
    position_lat = 41.8781 + 0.1 * cos(tval * pi/450), 
    position_long = -87.8298 + 0.13 * sin(tval * pi/500), 
} else {
  df = read_csv(

bbox = c(
  bottom = min(df$position_lat) - 0.005, 
  top = max(df$position_lat) + 0.005,
  left = min(df$position_long) - 0.005, 
  right = max(df$position_long) + 0.005

# functions
direction_labeller <- function(x){
    x %% 45 == 0, 
    c('E','NE','N','NW','W','SW','S','SE')[1+(as.integer(x/45) %% 8)], 

angle <- function(x,y){
  (atan(y/(x)) + pi*(x<0)) %% (2*pi)

# attributes calculation
# longitude is scaled back by factor of cosine latitude
# so that the scaled unit is the same distance as a unit
# of latitude
df = df %>% 
  ) %>%
    angles = angle(dlat, dlon) * 180/pi,
    vd = direction_labeller(round(angles/45)*45) %>% 
      factor(levels=direction_labeller(seq(0,315, 45)))
  ) %>% 

# create compasses
hues_df = data.frame(degree = 0:359) %>%
    label=direction_labeller((degree+90) %% 360),
    colors = colorRampPalette(cet_pal(8,'c2'))(360)

color_compass = ggplot(hues_df) + 
    aes(ymin=3,ymax=4, xmin=degree-0.5,xmax=degree+0.5,color=colors,fill=colors)
  ) + coord_polar(direction=-1, start=0) +
  scale_color_identity() + 
  scale_fill_identity() +
  guides(fill=FALSE,color=FALSE) + 
  theme_void() + 
  ylim(c(1,4.5)) + 

# load map
gmap=get_map(location=bbox,source='stamen', type='toner',force=TRUE,color='bw')

mymap = ggmap(gmap) +
  geom_path(data=df, aes(x=position_long, y=position_lat, 
   group=eid,color=vd), alpha=0.7,size=1) +
  scale_color_manual(values=cetcolor::cet_pal(8, 'c2')) + 

mymap_angle = ggmap(gmap) +
    geom_path(data=df, aes(x=position_long, y=position_lat, 
   group=eid,color=(-angles) %% 360), alpha=0.7,size=1) +
      colors=cetcolor::cet_pal(8, 'c2'),
    )  + guides(color=FALSE)

png(sprintf('%s_compass_v1.png', PREFIX), height=840,width=940)
grid.arrange(mymap, color_compass, widths=c(4,1))

png(sprintf('%s_compass_v2.png', PREFIX), height=840,width=940)
grid.arrange(mymap_angle, color_compass, widths=c(4,1))


    D. Kahle and H. Wickham. ggmap: Spatial Visualization with ggplot2. The R Journal, 5(1), 144-161. URL


Recommended Articles

"Error Bars" on Tiled Heatmaps

Heatmaps are a useful way of plotting 2-dimensional data, such as cross-tabulations. Adding "error bars" can seem non-intuitive, but expressing them in your visualization is possible with a small trick.

Analyzing and Clustering Christmas Foods, Drinks, and Desserts

When celebrating Christmas, most people think of a large feast as part of the celebration. In the fourth part of my Christmas article series, I look at what foods are common across different regions of the US. Clustering also shows interesting relationships between different foods and drinks.