If you have a few minutes to spare, consider filling in my “Vote for Candy” survey to rank your favorite candies as part of my latest project, especially if you are a resident of a smaller US state. Click/tap here to go to the survey. This tab will stay open.

Gift cards will be given to random participants.

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 http://journal.r-project.org/archive/2013-1/kahle-wickham.pdf


Recommended Articles

Hashing Data to Memorable Phrases

Do you have trouble memorizing long strings, but want to keep things easy to remember? Look no further than the new keyToEnglish package in R, now available on CRAN.

Scraping Data from Reddit

A new tool, Tree Grab for Reddit, can be used to store user, thread, and comment data in a PostgreSQL database, with a variety of command-line options to customize and specify what kind of data is selected.