Tracking my new year's resolutions
I can’t count the number of times I’ve set a goal to run 1,000 miles in a year. As an avid runner since high school, it felt achievable - less than 3 miles a day - and I usually lasted through January or mid-February, not running every day but consistently enough to remain roughly on track. I used Running Ahead during my most serious training phases, which gave me a daily average to hit, simply dividing the number of miles to go by the days left in the year.
Without fail, I’d hit a wall at some point, miss a week or two of running, get some aches that would require some rest, and fall behind enough to forget or give up.
As 2020 approached, I wanted to try gain, but also give myself shorter-term, achievable goals to keep motivated. I came up with some other goals, too - to ride my bike 2,000 miles, to read 5,000 pages, and to keep the time looking at my phone below 2 hours per day.
Having built a few dashboards for Open Justice Oklahoma, I decided to build myself something to track these goals in a way that would help me maintain my motivation. The result is this dashboard built with the incredibly handy flexdashboard package.
Getting the data
tl;dr I set up my dashboard to pull in running and cycling data from Strava using the rStrava package and reading and phone time entries from a Google Form. Getting the authentication to have the Shiny server talk to Google was the most challenging/frustrating part. If you’re hoping to do something similar, I would recommend being very patient and persistent in getting one of the methods described by Mark Edmondson to work for you.
The data was wrangled into tidy format to look like this:
d
## # A tibble: 185 x 5
## week type value goal pct_goal
## <date> <fct> <dbl> <dbl> <dbl>
## 1 2019-12-29 Ride 58.8 40 1
## 2 2019-12-29 Run 16.1 20 0.805
## 3 2020-01-05 Run 14.4 20 0.72
## 4 2020-01-12 Run 26.3 20 1
## 5 2020-01-19 Ride 44 40 1
## 6 2020-01-19 Run 20.3 20 1
## 7 2020-01-26 Ride 42.8 40 1
## 8 2020-01-26 Run 16 20 0.8
## 9 2020-02-02 Ride 58.2 40 1
## 10 2020-02-02 Run 20.5 20 1
## # … with 175 more rows
Weekly goals with geom_tile()
To reach my annual goals, I set weekly goals to run 20 miles each week, cycle 40 miles, and read 100 pages worth of book. Hitting all of these every single week would put me well over my annual goals, but I knew that wasn’t going to happen so it gave me a bit of wiggle room.
I needed a way to visualize my four goals each week and see the results in a not-overwhelmingly-busy way. I settled on using ggplot’s geom_tile()
to represent each week over the course of the year, so four tiles for each week. The opacity of each tile is determined by the progress of that week - a fully opaque tile if I met my goal, fully transparent if I didn’t make any progress at all.
The end result looks like this:
ggplotly( # Enables hover details with the plot_ly package
# The plot
ggplot(d, aes(week, type,
fill = type,
color = type,
alpha = pct_goal, # Determines the opacity of the tile
text = value)) +
geom_tile() +
# The cosmetic stuff
scale_x_date(breaks = c(ymd("2020-01-01"),
ymd("2020-04-01"),
ymd("2020-07-01"),
ymd("2020-10-01")),
date_labels = "%B") +
labs(title = str_to_upper("Weekly Results")) +
scale_fill_manual(values = trace_colors) +
scale_color_manual(values = trace_colors) +
theme_np() +
theme(plot.title = element_text(size = 16)),
tooltip = c("value", "pct_goal")
) %>%
# Plotly commands to get rid of the toolbar and disallow changing the axes
config(displayModeBar = F) %>%
layout(xaxis=list(fixedrange=TRUE)) %>%
layout(yaxis=list(fixedrange=TRUE))
For some reason, having rarely used geom_tile()
before, it was the first idea that came to mind, and I’m very pleased with the story it tells. You can see the points at which I was consistent with my running (spring through mid-summer, late fall/winter), the periods where I was just hanging on (the chokingly humid Oklahoma late summer), and the one week I was on a road trip and didn’t run at all. You can also see how I hit my cycling goal quite consistently, then abruptly stopped in September (I took a spill on my bike and sprained my wrist, and it took a long time to heal fully).
Yearly and weekly progress with geom_bar()
In addition to the week-by-week goals, I set up simple bar charts to track how many pages I’d read and miles I’d run and cycled.
# This is how I got the current week. Not sure why I used Sys.time() instead of Sys.Date() here!
# current_week <- floor_date(Sys.time(), "week") %>%
# str_sub(1, 10) %>%
# ymd
current_week <- floor_date(ymd("2020-07-10"), "week")
thisweek <- d %>%
filter(type %in% c("Run", "Ride", "Read"),
week == current_week) %>%
bind_rows(tibble(week = current_week,
type = c("Run", "Read", "Ride"),
value = 0)) %>%
group_by(week, type) %>%
summarize(value = sum(value, na.rm = TRUE)) %>%
mutate(goal = case_when(type == "Run" ~ 20,
type == "Read" ~ 100,
type == "Ride" ~ 40),
goal_unit = case_when(type == "Run" ~ "miles",
type == "Read" ~ "pages",
type == "Ride" ~ "miles"),
pct_done = ifelse(value/goal < 1,
value/goal,
1)) %>%
ungroup %>%
mutate(type = type %>%
fct_relevel(c("Read", "Ride", "Run")))
ggplotly(
ggplot(thisweek, aes(type, pct_done, fill = type)) +
geom_bar(stat = "identity") +
geom_text(aes(y = pct_done + .13,
label = ifelse(pct_done == 1,
paste("Done.\n", value, goal_unit),
paste(goal - round(value, 1), goal_unit, "\nto go"))),
hjust = -1,
size = 3,
color = plot_title_color) +
ylim(0, 1.2) +
geom_hline(aes(yintercept = 1), linetype = "dotted", color = plot_title_color) +
coord_flip() +
theme_np() +
theme(axis.text.x = element_blank()) +
labs(title = "THIS WEEK'S PROGRESS") +
scale_fill_manual(values = trace_colors[2:5])
) %>%
config(displayModeBar = F) %>%
layout(xaxis=list(fixedrange=TRUE)) %>%
layout(yaxis=list(fixedrange=TRUE))
Gauging if I’m on-pace with geom_step()
To bridge the gap between weekly and annual goals, Page 2 of my dashboard uses a step plot to show how the weeks are totaling up to keep me on pace for my annual goals. The dotted horizontal line shows where I need to be in the current week to hit my annual goal; the solid line shows the annual goal.
I turned the whole plot into a function to take the activity and annual goals as arguments to avoid repeating all of this three times.
progress <- d %>%
filter(week < ymd("2020-09-01")) %>% # Adding this to show progress on
# reading as of September
group_by(type) %>%
mutate(ytd = cumsum(value))
progress_plot <- function(activity, annual_goal) {
pace <- n_distinct(progress$week)/52*annual_goal
pace_diff <- round(max(progress[progress$type == activity, "ytd"]) - pace, digits = 0)
pace_word <- ifelse(pace_diff > 0, "ahead of", "behind")
subtitle_text <- paste(abs(pace_diff),
ifelse(activity == "Read", "pages", "miles"),
pace_word,
"pace")
ggplotly(
ggplot(progress %>%
filter(type == activity), aes(week, ytd)) +
geom_step(color = trace_colors[[1]], size = 1.5) +
geom_hline(aes(yintercept = annual_goal), color = plot_title_color) +
geom_hline(aes(yintercept = pace),
color = trace_colors[[1]],
linetype = "dotted") +
scale_x_date(breaks = c(ymd("2020-01-01"),
ymd("2020-04-01"),
ymd("2020-07-01"),
ymd("2020-10-01")),
date_labels = "%B",
limits = c(ymd("2019-12-29"), ymd("2020-12-31"))) +
ylim(0, annual_goal) +
theme_np() +
labs(title = paste(str_to_upper(activity), "<br>",
subtitle_text)) +
NULL
) %>%
config(displayModeBar = F) %>%
layout(xaxis=list(fixedrange=TRUE)) %>%
layout(yaxis=list(fixedrange=TRUE))
}
progress_plot("Read", 5000)
It worked like gangbusters
The combination of weekly and annual goals worked perfectly. At the beginning of each week, I’d be motivated to fill in each tile, but if I had a rough week I just started over the next. I ended up juuuuust hitting my annual running goal in the last week of the year with exactly 1,000 miles (first time hitting four digits!), far exceeded my reading goal, and came in averaging 1 hour 55 minutes looking at my phone each day. Cycling goal hit a hard wall, but can’t win ’em all.
It worked so well, I replicated it for this year with some modifications. More on that in future posts! Probably!