>


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.

Examples

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

Generated Sample

26-run Sample

Code

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.

library(tidyverse)
library(ggthemes)
library(cetcolor)
library(ggmap)
library(gridExtra)

# constants
# from cetcolor::cet_pal(8, 'c2s')
colors=c("#2E22EA","#9E3DFB","#F86BE2","#FCCE7B",
  "#C4E416","#4BBA0F","#447D87","#2C24E9"
)

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

USE_GENERATED_SAMPLE=FALSE

PREFIX = ifelse(
  USE_GENERATED_SAMPLE, 
  'sample',
  'data'
)

# get data, either by generating or loading
if (USE_GENERATED_SAMPLE){
  # 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), 
    eid=1
  )
} else {
  df = read_csv(
    COORDINATE_FILENAME
  ) 
}

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){
  ifelse(
    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 %>% 
  group_by(
    eid
  ) %>%
  mutate(
    dlon=(lead(position_long,5)-position_long)*cos(median(position_lat)*pi/180),
    dlat=lead(position_lat,5)-position_lat,
    angles = angle(dlat, dlon) * 180/pi,
    vd = direction_labeller(round(angles/45)*45) %>% 
      factor(levels=direction_labeller(seq(0,315, 45)))
  ) %>% 
  ungroup()

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

color_compass = ggplot(hues_df) + 
  geom_rect(
    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)) + 
  geom_text(
    aes(x=degree,y=4.5,label=label) 
  )


# 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')) + 
  guides(color=FALSE)


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) +
    scale_color_gradientn(
      colors=cetcolor::cet_pal(8, 'c2'),
      breaks=seq(0,315,45),
      limits=c(0,359)
    )  + guides(color=FALSE)


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

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

Citations

    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


Tags: 

Recommended Articles

Modeling Heart Rate Recovery with Nonlinear Regression

Nonlinear regression models can succeed where linear models fail and highly complex models cannot be interpreted. Using heart rate data I collected from my runs, I demonstrate how my heart rate recovers after stopping as a function of temperature and rest time.

Outliers in a Triathlon

How do you identify an "outlier" in a triathlon?