<p><div><div><div><div data-show-faces="false" data-height="21" data-width="100" data-layout="button_count" data-send="true" data-href="https://www.r-bloggers.com/exercise-dashboard/"></div></div><div></div></div><div></div></div>
<div>
(This article was first published on <strong><a href="http://www.nathanchaney.com/2018/03/04/exercise-dashboard/"> R – Nathan Chaney</a></strong>, and kindly contributed to <a rel="nofollow" href="https://www.r-bloggers.com/">R-bloggers)</a> </div>
<p>I posted a while back about using joy plots for heart rate data. Over the past couple of months, I grew tired of opening <a rel="nofollow" href="https://www.rstudio.com/" target="_blank">RStudio</a> every time I wanted to look through my fitness tracker data. I decided to create a <a rel="nofollow" href="https://shiny.rstudio.com/" target="_blank">shiny</a> dashboard that I can load via web browser. This involved setting up a <a rel="nofollow" href="https://www.rstudio.com/products/shiny/shiny-server/" target="_blank">shiny server</a> in a <a rel="nofollow" href="https://www.docker.com/" target="_blank">dockerized</a> container that runs an Rmd file. I’ve had issues getting plots to size themselves appropriately using base shiny, so I used the <a rel="nofollow" href="https://rmarkdown.rstudio.com/flexdashboard/" target="_blank">flexdashboard</a> package to created a dashboard that will automatically resize.</p>
<p><img sizes="(max-width: 300px) 100vw, 300px" src="https://i2.wp.com/www.nathanchaney.com/wp-content/uploads/2018/03/IMG_4C4BDB765E56-1-300x169.jpeg?resize=300%2C169" alt="" width="300" class="wp-image-1032" data-recalc-dims="1" height="169"></p>
<p>I wanted to be able to look at individual workouts as well as weekly statistics. To remain consistent with the heart rate app I use, <a rel="nofollow" href="http://motifitapp.com/" target="_blank">MotiFIT</a>, I copied the charting style for individual workouts. The screenshot at right shows how the app displays heart rate. I find this view useful during weightlifting sessions because I can tell when I’ve rested sufficiently to start another set.</p>
<p><img sizes="(max-width: 1024px) 100vw, 1024px" src="https://i2.wp.com/www.nathanchaney.com/wp-content/uploads/2018/03/Screen-Shot-2018-03-04-at-15.05.09-1024x467.png?w=450" alt="" class="wp-image-1030" data-recalc-dims="1"></p>
<p>The dashboard has a date selector so you can use small multiples to compare several workouts at once. The last workout in the multiples corresponds to the screenshot from the app, and the similarity is apparent.</p>
<p>The weekly view shows the aggregate amount of time spent in each heart rate zone. You can see a big surge in the number of hours of exercise in September 2017, which is when I started playing in a tennis league. There are noticeable gaps in December and January, when I got sick and then suffered a couple of injuries. I’ve slowly added more hours back as I’ve rehabbed the injuries.</p>
<p><img sizes="(max-width: 1024px) 100vw, 1024px" src="https://i0.wp.com/www.nathanchaney.com/wp-content/uploads/2018/03/newplot-1024x427.png?w=450" alt="" class="wp-image-1031" data-recalc-dims="1"></p>
<p>There are a couple of other views, but I’m still trying to decide how to display some of the concepts effectively. The code for the flexdashboard is below.</p>
<pre>
---
title: &quot;Exercise Dashboard&quot;
runtime: shiny
output:
flexdashboard::flex_dashboard:
orientation: rows
vertical_layout: fill
---

```{r setup, include=FALSE}

library(lubridate)
library(readr)
library(tidyverse)
library(plotly)
library(flexdashboard)
library(ggridges)
library(data.table)
library(scales)

ifelse(
dir.exists(&quot;/shiny-server/data/dir&quot;),
datadir &lt;- &quot;shiny-server/data/dir&quot;,
datadir &lt;- &quot;/local/data/dir&quot;
)
```

```{r load HR data}

out.file &lt;- data.frame(timestamp = as.POSIXct(character()), bpm = integer(), stringsAsFactors = FALSE)
file.names &lt;- dir(datadir, pattern = &quot;^.*HeartRateData.*.csv&quot;) # You'll need to set datadir

library(doParallel)
registerDoParallel()

dir.traversal &lt;- system.time({
# out.file &lt;- foreach (file.name = tail(file.names, 10), .combine = &quot;rbind&quot;) %dopar% {
out.file &lt;- foreach (file.name = file.names, .combine = &quot;rbind&quot;) %dopar% {
# for (file.name in file.names) {
# exported files are in at least 2 different encodings, so we're going to guess using the guess_encoding function from the readr package
# We can't use readr's read_csv or data.table's fread because some of the encodings are UTF-16LE, which makes this process s
encoding &lt;- guess_encoding(paste(datadir, &quot;/&quot;, file.name, sep=&quot;&quot;), n_max = 1000)
file &lt;- read.csv(
paste(datadir, &quot;/&quot;, file.name, sep=&quot;&quot;),
skip = 1,
sep = &quot;,&quot;,
strip.white = TRUE,
stringsAsFactors = FALSE,
fileEncoding = toString(encoding[1,1])
)

# exported data has 2 or three columns, so if it's three, we're going to join the date and time fields
if (length(file) > 2) {
temp.date &lt;- file[,1]
temp.time &lt;- trimws(file[,2])
temp.bpm &lt;- trimws(file[,3])

# date format is Wed Feb 8 2017
temp.datetime &lt;- as.POSIXct(paste(temp.date, temp.time, sep=&quot; &quot;), format = &quot;%a %b %d %Y %H:%M&quot;)

temp.df &lt;- data.frame(temp.datetime, temp.bpm, stringsAsFactors = FALSE)
names(temp.df) &lt;- c(&quot;timestamp&quot;, &quot;bpm&quot;)
file &lt;- temp.df
rm(temp.df)
} else {
names(file) &lt;- c(&quot;timestamp&quot;, &quot;bpm&quot;)
file$timestamp &lt;- as.POSIXct(file$timestamp)
}

file$name &lt;- file.name

#out.file &lt;- rbind(out.file, file)
file
}
})

```

```{r add week number }
# Used to calculate week number below
start.date &lt;- as.Date(as.character(min(out.file$timestamp)))

week.starts &lt;- seq(from = start.date, to = as.Date(Sys.Date()), by = 7)
week.starts &lt;- data.frame(
week = 0:(NROW(week.starts) - 1),
`week of` = as.Date(week.starts)
)

HR &lt;- out.file %>%
mutate(
week = as.numeric(as.Date(timestamp, tz = Sys.timezone()) - start.date) %/% 7,
bpm = as.numeric(bpm)
) %>%
left_join(week.starts, by = c(&quot;week&quot; = &quot;week&quot;)) %>%
filter(! is.na(week)) %>%
arrange(week)

HR$week.of &lt;- factor(HR$week.of)
HR$`week.of` &lt;- factor(HR$`week.of`, ordered = T, rev(unique(HR$`week.of`)))
# HR$week &lt;- factor(HR$week, ordered = TRUE, levels = rev(unique(HR$week)))

```

```{r set HR zones}

cuts &lt;- c(-Inf, 109, 123, 138, 164, Inf)
labs &lt;- c(&quot;Warm Up&quot;, &quot;Fitness&quot;, &quot;Endurance&quot;, &quot;Hardcore&quot;, &quot;Red Line&quot;)
HR$zone &lt;- cut(HR$bpm, breaks = cuts, labels = labs, include.lowest=TRUE, ordered_result = TRUE)
fitness.rainbow &lt;- c(&quot;royalblue&quot;, &quot;royalblue&quot;, &quot;green&quot;, &quot;yellow&quot;, &quot;orange&quot;, &quot;red&quot;)
rects &lt;- data.frame(ystart = cuts[1:5], yend = cuts[2:6], zone = factor(labs, levels = rev(labs), ordered = TRUE))

bpm.min &lt;- reactive({
min(HR.filtered()$bpm, na.rm = T)
})
bpm.max &lt;- reactive({
max(HR.filtered()$bpm, na.rm = T)
})

zone.breaks &lt;- reactive({
c(
bpm.min(),
(bpm.min() + cuts[2]) / 2,
(cuts[2] + cuts[3]) / 2,
(cuts[3] + cuts[4]) / 2,
(cuts[4] + cuts[5]) / 2,
bpm.max()
)
})

```

```{r make HR reactive }

HR.filtered &lt;- reactive({
HR %>%
filter(as.Date(timestamp, tz = Sys.timezone()) >= input$date[1]) %>%
filter(as.Date(timestamp, tz = Sys.timezone()) &lt;= input$date[2])
})

```

```{r calc durations }

HR.duration &lt;- reactive({
HR.filtered() %>%
mutate(date = as.Date(format(timestamp, &quot;%Y-%m-%d&quot;))) %>%
group_by(name, date) %>% # name is filename so two files from same date don't skew calculations
summarize(start = min(timestamp, na.rm = T),
end = max(timestamp, na.rm = T),
`duration (m)` = round(as.numeric(difftime(end, start, units = &quot;mins&quot;)), 1),
`average bpm` = round(mean(as.numeric(bpm)), 0),
sd = round(sd(as.numeric(bpm)), 1),
max.bpm = max(as.numeric(bpm), na.rm = T)) %>%
ungroup() %>%
mutate(start = format(start, &quot;%H:%M:%S&quot;),
end = format(end, &quot;%H:%M:%S&quot;))
})

HR.daily.duration &lt;- reactive({
HR %>%
filter(as.Date(timestamp, tz = Sys.timezone()) >= input$dailyDate[1]) %>%
filter(as.Date(timestamp, tz = Sys.timezone()) &lt;= input$dailyDate[2]) %>%
mutate(date = as.Date(format(timestamp, &quot;%Y-%m-%d&quot;))) %>%
group_by(name, date) %>%
summarize(start = min(timestamp, na.rm = T),
end = max(timestamp, na.rm = T),
`duration (m)` = round(as.numeric(difftime(end, start, units = &quot;mins&quot;)), 1),
`average bpm` = round(mean(as.numeric(bpm)), 0),
sd = round(sd(as.numeric(bpm)), 1),
max.bpm = max(as.numeric(bpm), na.rm = T)) %>%
ungroup() %>%
mutate(start = format(start, &quot;%H:%M:%S&quot;),
end = format(end, &quot;%H:%M:%S&quot;))
})

HR.zone.duration.weekly &lt;- reactive({
HR.filtered() %>%
group_by(week, zone) %>%
summarize(
`duration (s)` = n(),
`duration (m)` = round(n() / 60, 1),
`duration (h)` = round(n() / 3600, 1)
) %>%
ungroup() %>%
left_join(week.starts, by = &quot;week&quot;) %>%
filter(! is.na(week)) %>%
arrange(week)
})

HR.weekly.duration &lt;- reactive({
HR.zone.duration.weekly() %>%
group_by(week, `week.of`) %>%
summarize(
`duration (h)` = sum(`duration (h)`),
`exercise sessions` = n()
) %>%
ungroup() %>%
arrange(week)
})

HR.zone.duration.daily &lt;- reactive({
HR %>%
mutate(date = as.Date(timestamp, tz = Sys.timezone())) %>%
filter(date >= input$dailyDate[1]) %>%
filter(date &lt;= input$dailyDate[2]) %>%
group_by(date, zone) %>%
summarize(
`duration (s)` = n(),
`duration (m)` = round(n() / 60, 1),
`duration (h)` = round(n() / 3600, 1)
) %>%
ungroup()
})

```

Weekly
=======================================================================

Inputs {.sidebar}
-----------------------------------------------------------------------

Enter a date range for the weekly charts:

```{r input date range for weekly chart }

dateRangeInput(&quot;date&quot;, &quot;Date Range&quot;, start = as.Date(min(HR$timestamp, na.rm = T), tz = Sys.timezone()))

```

Column
-----------------------------------------------------------------------

### Weekly Exercise

```{r weekly barplot}

renderPlotly({
ggplot(data = HR.zone.duration.weekly(), aes(x = `week.of`, y = `duration (h)`, fill = zone)) +
geom_bar(
stat = &quot;identity&quot;,
position = &quot;stack&quot;
) +
scale_fill_manual(values = fitness.rainbow[2:6]) +
scale_y_continuous(breaks = seq(from = 0, to = 8, by = 2), minor_breaks = seq(from = 0, to = 8, by = 1))
})

```

Column
-----------------------------------------------------------------------

### Heartrate Zones

```{r ridgeline plot}

renderPlot({
ggplot(
HR.filtered(),
aes(x = bpm, y = `week.of`, fill = ..x..)
) +
scale_fill_gradientn(
colors = fitness.rainbow,
breaks = zone.breaks()
) +
scale_x_continuous(breaks = function(x) {seq(from = 0, to = max(x), by = 10)}) +
geom_density_ridges_gradient(na.rm = TRUE, col = &quot;grey70&quot;, scale = 4) +
theme_ridges(font_size = 7) +
theme(
legend.position = &quot;none&quot;
)
})

```

### Weekly Durations

```{r total duration bar plot}

renderPlotly({
ggplot(data = HR.weekly.duration(), aes(x = `week.of`, y = `duration (h)`)) +
geom_bar(
aes(
text = paste(
&quot;week: &quot;, week, &quot;<br>&quot;,
&quot;# of sessions: &quot;, `exercise sessions`,
sep = &quot;&quot;
)
),
stat = &quot;identity&quot;
) +
geom_smooth(span = 0.35)
})

# renderTable({
# head(HR.weekly.duration())
# })

```

Daily {data-orientation=rows}
=======================================================================

Inputs {.sidebar}
-----------------------------------------------------------------------

Enter a date range for the daily charts:

```{r input date range for daily calcs }

dateRangeInput(&quot;dailyDate&quot;, &quot;Date Range&quot;, start = as.Date(Sys.Date() %m+% days(-15)))

```

Column
-----------------------------------------------------------------------

### Daily Heartrate Zones

```{r individual exercise zones}

renderPlotly({
ggplot(data = HR.zone.duration.daily(), aes(x = date, y = `duration (m)`, fill = zone)) +
geom_bar(
stat = &quot;identity&quot;,
position = &quot;dodge&quot;
) +
scale_fill_manual(values = fitness.rainbow[2:6]) +
scale_y_continuous(breaks = seq(from = 0, to = max(HR.zone.duration.daily()$`duration (m)`, na.rm = TRUE), by = 15)) +
theme(
legend.position = &quot;none&quot;
)
})

```

### Daily Workouts, BPM vs. Duration

```{r bubble plot}

renderPlotly({
ggplot(HR.daily.duration(), aes(x = `duration (m)`, y = `average bpm`)) +
geom_point(aes(color = `average bpm` + sd, size = -sd), alpha = 0.5) +
scale_color_gradientn(
colors = fitness.rainbow[2:6], # &quot;royalblue&quot; &quot;green&quot; &quot;yellow&quot; &quot;orange&quot; &quot;red&quot;
values = rescale(zone.breaks(), to = c(0, 1)), # 54 81.5 116 130.5 151 190
na.value = &quot;black&quot;,
breaks = cuts, # -Inf 109 123 138 164 Inf
limits = c(min(zone.breaks()), max(zone.breaks()))
) +
scale_x_continuous(
limits = c(0, as.numeric(max(HR.daily.duration()$`duration (m)`))),
breaks = seq(from = 0, to = as.numeric(max(HR.daily.duration()$`duration (m)`)), by = 15)
) +
scale_y_continuous(
limits = c(min(zone.breaks()), max(zone.breaks())),
breaks = cuts[2:5],
minor_breaks = NULL
) +
#geom_smooth(span = 0.35) +
theme(
legend.position = &quot;none&quot;
)
})

# renderTable({
# head(HR.daily.duration())
# })

# renderText({
# rescale(
# HR.daily.duration()$max.bpm,
# from = c(min(zone.breaks()), max(zone.breaks())),
# to = c(0, 1)
# )
# })

```

Column
-----------------------------------------------------------------------

### Heartrate Curves

```{r individual exercise plots}

renderPlot({

temp &lt;- HR.filtered() %>%
# temp &lt;- HR %>%
mutate(date = as.Date(timestamp, tz = Sys.timezone())) %>%
filter(as.Date(timestamp, tz = Sys.timezone()) >= input$dailyDate[1]) %>%
filter(as.Date(timestamp, tz = Sys.timezone()) &lt;= input$dailyDate[2])

ggplot() +
geom_rect(data = rects, aes(ymin = ystart, ymax = yend, fill = zone), xmin=-Inf, xmax=Inf, inherit.aes = FALSE) +
geom_ribbon(data = temp, aes(x = timestamp, ymax = bpm, ymin = min(temp$bpm)), color = &quot;white&quot;, fill=&quot;grey90&quot;, alpha = .5) +
scale_fill_manual(values = rev(fitness.rainbow[2:6])) +
theme_minimal() +
facet_wrap(~ name, scales = &quot;free_x&quot;) +
scale_x_datetime(expand = c(0, 0)) +
scale_y_continuous(expand = c(0, 0), breaks = cuts[2:5]) +
theme(panel.grid = element_blank(), panel.border = element_blank())
})

```

</pre>
	<p>
<div>
<div>To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="http://www.nathanchaney.com/2018/03/04/exercise-dashboard/"> R – Nathan Chaney</a></strong>.</div>
<hr>
<a rel="nofollow" href="https://www.r-bloggers.com/">R-bloggers.com</a> offers <strong><a rel="nofollow" href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers">daily e-mail updates</a></strong> about <a rel="nofollow" href="https://www.r-project.org/" title="The R Project for Statistical Computing">R</a> news and <a rel="nofollow" href="https://www.r-bloggers.com/search/tutorial" title="R tutorials">tutorials</a> on topics such as: <a rel="nofollow" href="https://www.r-bloggers.com/search/data%20science" title="Data science">Data science</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/Big%20Data" title="Big Data">Big Data, <a rel="nofollow" href="https://www.r-users.com/" title="R jobs">R jobs</a>, visualization (<a rel="nofollow" href="https://www.r-bloggers.com/search/ggplot2" title="ggplot and ggplot2 tutorials">ggplot2</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/boxplot" title="Boxplots using lattice and ggplot2 tutorials">Boxplots</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/map" title="Maps and gis">maps</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/animation" title="Animation in R">animation</a>), programming (<a rel="nofollow" href="https://www.r-bloggers.com/search/RStudio" title="RStudio IDE for R">RStudio</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/sweave" title="Sweave and literate programming">Sweave</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/LaTeX" title="LaTeX in R">LaTeX</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/SQL" title="SQL and databases">SQL</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/eclipse" title="Eclipse IDE for R">Eclipse</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/git" title="git and github, Version Control System">git</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/hadoop" title="Large data in R using Hadoop">hadoop</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/Web+Scraping" title="Web Scraping of google, facebook, yahoo, twitter and more using R">Web Scraping</a>) statistics (<a rel="nofollow" href="https://www.r-bloggers.com/search/regression" title="Regressions and ANOVA analysis tutorials">regression</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/PCA" title="principal component analysis tutorial">PCA</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/time+series" title="Time series">time series</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/trading" title="finance trading">trading</a>) and more...
<img width="1" alt="" src="http://feeds.feedburner.com/~r/RBloggers/~4/bpGmVNEMJj4" height="1"></a></div>