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.

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

Converting fields of lists into wide and tall formats in R

If you have ever downloaded survey data, or any other kind of data, that has a field that is itself comma-separated, you may have found it annoying/difficult to reshape the data into a more useful form.

When do Kids Stop Believing in Santa?

When do kids stop believing in Santa? Are there any backgrounds more at risk for not believing in Santa as long? I explore these questions using a Christmas survey in part 2 of my series of Christmas articles.