Killing Two Birds with One Stone: How I learned to use Shiny to Make Working Out at Home Suck Less

Michael Hoang
13 min readJun 27, 2021

Along this whole journey in learning data science, I would say I’ve gotten an OK handle of things in terms of understanding the R language + Data Science. Sure, I’ve mainly used these newfound skills for entirely selfish and petty reasons like trying to prove my Boomer dad wrong or demonstrating how to capitalize on the lucrative world of sports betting with machine learning. Nonetheless, I still consider it a successful venture during this global downtime. So, in coming up with a new idea to go after, I realize that I’m missing something that would really stand out. Then it hit me like a ton of bricks. I needed to add a Shiny project.

I always knew that you could build some interesting interactive stuff with the R language like you can find over here ever since my time in grad school. Sure, it’s cool and all, but I never could really find the time, incentive, and/or adequate direction to apply it in my own research project, so I abandoned the whole idea. Fast forward to now with almost nothing better to do (I’ve already watched everything on Netflix), I guess I’m going to bite the bullet and figure this thing out. FML.

My Plan on Learning Shiny

To prefix this, I’m not the most conventional learner. Whenever I’m trying to pick something up and get good at it, my default process is to come up with some kind of relatively doable idea and try to make that happen from nothing by piecing things together whilst failing miserably throughout the entire process. Not the most intelligent learning strategy, but then again, I’m not a very intelligent person to start.

With that said, I’ll attempt to figure it out with a project build and go from there. The only thing is figuring out what sort of project should I attempt. So, after a 10-hour gaming session and sustaining myself on nothing but Monster Energy drinks and poutine, the thought came up was that I should probably make a home workout app.

Canada’s gift to the world of snacking

So, I’m sure like all of you, the healthy lifestyle may have taken a solid hit this past year. With our usual modes of being physically active being limited with public health order and the feeling of being confined to a small space with working from home for 12 or so months, it’s pretty understandable to see the usual healthy lifestyle habits get kicked to the wayside. Then, at some point during your 5th straight day of ordering another extra-large pizza for yourself, you’ll probably get the epiphany that “Hey, I should probably do something, right?” and your start to be “healthier” begins. However, about 1–7 days in, you realize that either (1) you have no idea on how to get started at all and continually put it off or (2) started something, but find it boring and then abandon the whole thing altogether.

Well, what’s a better time than now to solve that problem. Let’s dust off that Kinesiology degree I’m currently using as a coffee coaster and do something that sort of useful whilst I learn to use Shiny. So, let’s follow along with my process.

Where to Start

If you’re reading this, the most obvious spot would probably be checking out the Shiny package session in the Data Analyst path in R on Data Quest, which you can find here to get a basic understanding to start. Unfortunately, I totally forgot about it and ended up using Google and the University of YouTube instead. Still worked out, just took longer than necessary.

The Process

PART 1: Starting from scratch

To start off, I really needed to get the most basic skeleton in place possible to just see this app. Essentially, this involves having two elements: the UI (aka. User interface) and the server. While it’s probably best to get an explanation of these two elements here, you can see the image below to “too long didn’t read” it.

This can be achieved using the following:

ui = fluidPage()
server = function(input, output) {}
shinyApp(ui = ui, server = server)

and this is the output you will get:

It’s really plain, right?

So, one of my questions in figuring Shiny is understanding what fluidPage() does since I always see it in every tutorial setup. For those that are probably asking the same question, it really has to do with how the app interacts with the browser window which can change in size. You would use a fluid page to allow for efficient use of space on any given browser window so that whatever is being viewed remains the same. This is the preferred option for the majority of cases. You can read more about it here.

PART 2: The basic layout

The next logical step to making a layout is to understand what sort of layout to use in this application. There are two ways you can play it: (a) navigation bar on the top or (b) have a menu on the side.

(a) Navigation bar on top via navbarPage()

(b) Navigation menu panel via navlistPanel()

Since there are multiple components (specifically three functions), and I want to make it clear about transitioning from one feature to the next, let’s go with the navbarPage() function. I’ll also need to separate it out by each specific tab for a particular aspect of this app. That’s possible using the “tabPanel” function. So, this code comes out to:

ui = fluidPage(
navbarPage(
title = “Home Workout App”,
tabPanel(title = “Section 1”),
tabPanel(title = “Section 2”),
tabPanel(title = “Section 3”)
)
)

PART 3: Figuring out what I want to show

This part is more of the brainstorming element of things. Looking at at-home workouts, it seems like there are three major considerations:

  1. The sort of stuff do you have available
  2. Some sort of differentiator for purpose of a given exercise
  3. The existing knowledge base for the user

With all of this in mind and the fact that I’m currently watching Fast and the Furious presents Hobbs and Shaw, I’ll be using the typical bodybuilding approach of splitting exercises based on each muscle group.

Additionally, I figured it would also be helpful to have a unit conversion calculator as well. For those meatheads out there, whoever traveled between North America and everywhere else in the world, this throws off our workout if where ever needed to make these conversions during the workout. Plus, in this current overpriced home gym equipment market, you’ll probably end up having a mix of weights in both kilograms and pounds since it’s all you could get your hands on for your own home gym.

All that’s left is to create a data set that contains links to videos to show each of these exercises, which you can find here.

This was 4 hours of my life…

PART 4: Planning out the layout

Ultimately the UI that I want to achieve is to have a section where one side contains all of the criteria and then there is a main section that displays all of the relevant output that I want. In this case, it’s probably best to have an output that displays a video showing the given exercise that best matches the various criteria factors selected. Essentially it will look something like this:

While it’s possible to manually do this (I don’t recommend doing this), there fortunately is a layout available that creates this setup called sidebarlayout(). With a sidebar layout, it allows for a two-section split:

  1. A “sidebar” section typically reserved for input
  2. A “main” section reserved for output
ui = fluidPage(

navbarPage(
title = “Home Workout App”,
tabPanel(
title = “Section 1: Exercise Generator“,
sidebarLayout(
sidebarPanel(),
mainPanel(
textOutput(outputId = "exercise"),
uiOutput(outputId = "video")
)
)
),
tabPanel(
title = “Section 2: Unit Convertor”,
sidebarLayout(
sidebarPanel(),
mainPanel(
textOutput(outputId = "load"),
uiOutput(outputId = "gif")
)
)
)
)
)

PART 5: Adding the input widgets

Knowing what I want to include as the input criteria, the next step is to add the different widgets to be used in facilitating an output. There are various inputs that you can put in here that are available on the shiny package as shown here. However, going with the sort of inputs I will be using, the best option will probably be using selectInput() and sliderInput() for the exercise generator section. While for the unit conversion, I will use the numericInput() and radioButtons().

# For the exercise generator section

sidebarPanel(
selectInput(inputId = "equip",
label = 'What equipment are you planning to use?',
choices = c("Just my body" = "nothing",
"Dumbbells" = "db",
"Kettlebells" = "kb",
"Barbell & Plates" = "bar",
"Resistance Bands" = "rb_long")),

selectInput(inputId = "area",
label = "What area of focus are you interested in working?",
choices = c("Shoulders" = "sh",
"Chest" = "ch",
"Back" = "back",
"Biceps" = "bi",
"Triceps" = "tri",
"Forearms" = "fore",
"Quadriceps" = "quad",
"Hamstrings" = "ham",
"Glutes" = "glut",
"Abdominals" = "core")),

sliderInput(input = "skill",
label = "Complexity scale for your exercise (from low to high)",
min = 1,
max = 5,
step = 1,
value = 1)),
),

# For the unit conversion section

sidebarPanel(
numericInput(inputId = "number",
label = "Enter the number to convert",
value = 0),

radioButtons(inputId = "convert",
label = "What unit is this in?",
choices = c("Kilograms" = "kg",
"Pounds" = "lbs"),
selected = "Pounds")),

PART 6: Figuring out text outputs

One of the intended outputs that I wanted to get at with this simple app is to display the name of the given exercise in the main panel section of the display. To do so, I’ll need to use one of the various output functions, which will run through the server section. In this case, I will be using the renderText() function which will define the output labeled in the main panel section. Specifically, to make this work, I’ll use the filter() and select() functions as part of the Tidyverse package.

server = function(input, output) {

output$exercise = renderText({

as.character(
SampleExerciseList %>%
filter(Stuff == input$equip) %>%
filter(Muscles == input$area) %>%
filter(Complexity == input$skill) %>%
select(Exercise)
)
})

output$load = renderText({

paste("Your weight in",
as.character(
ifelse(input$unit == "kg", "pounds (aka. American Freedom Units) is", "kilograms (aka. what the rest of the world uses) is")
),
as.character(
ifelse(input$unit == "kg", round_any((input$number / 0.4536), 1, f = round), round_any((input$number * 0.4536), 1, f = round))
)
)

})

}
There we go! Not super impressive right now, but at least something is there.

PART 7: Creating the reactive media element

So here comes the hardest part of this whole thing, having a reactive media element. Normally, Shiny will have typical outputs like plots and tables with their own reactive function. However, things like YouTube videos or images/GIFs aren’t really in R’s typical repertoire, I’ll need to use a more generalized output function. In this case, I’ll be using the renderUI() and uiOutput() functions.

Now you might be wondering, how exactly am I going to add these functions into R then. Well, the good thing about Shiny is that it allows for cross-functionality with other languages. In this specific case, I’ll need to break out a few markup languages (HTML and CSS), which are used in almost all web pages nowadays. This is possible with the use of a function in the Shiny package called “tags”, which allows for the use of HTML tags in R. There are a lot of these tags that can be used to enhance a Shiny app, but the one I’ll need to use is the iframe tag which functions the same as the iframe HTML tag that’s used to create an inline frame to embed another HTML document (aka. pretty much most things on the web). So now that I have a means of allowing the UI section to interact with the server section via renderUI + uiOutput and have a means of embedding a video. All that’s left is to combine the two…This took me a minute to figure out.

Originally my plan on the server-side of things is to create a reactive action using that tag function that links the given YouTube URL with a specific row in the data frame that gets filtered based on the selection criteria. It essentially looks like this:

# Server sideoutput$video = renderUI({

url = as.character(
SampleExerciseList %>%
filter(Stuff == input$equip) %>%
filter(Muscles == input$area) %>%
filter(Complexity == input$skill) %>%
select(Link)
)

tags$iframe(
src = url,
height = 500,
width = 800
)

})

While the expectation was that a YouTube video would be embedded onto the main panel, it didn’t in this case. I ended up with a big giant blank square. Not exactly what I have planned.

Examining further, it turns out that the problem had to do with how I copied the links to the videos which had the incorrect path used. Looking at the data set, I’ve copied the YouTube link with the “/watch?v=” when I’ve should’ve used the “/embed/” path instead.

So, a quick and simple substitution using the gsub function as shown here:

SampleExerciseList$Link = gsub("watch\\?v=*", "embed/", SampleExerciseList$Link)

And… there we have it! Got it to work now.

I’ve modified these images to avoid copyright issues, but otherwise, it works perfectly fine.

PART 8: Let’s make that text a bit bigger

To do this, all I had to do is to use another one of HTML tags called h1 and strong which are used to designate the text as a header and make it bold respectively. I’ve just wrapped it around the textOutput listed in the main panel as shown below and you’ll see the following changes:

# h1 = major heading style font and strong = bold h1(strong(textOutput(outputId = ”exercise”)), align = “center”)
h1(strong(textOutput(outputId = ”load”)), align = “center”)

PART 9: Let’s add something here

OK, sure the unit conversion section is entirely useful, but it’s boring. Let’s add some kind of interesting visual here. Considering that I’ve recently been binging on Reddit, I’ll just use GIFs to spice this up. To use this, I’ll just be employing the same process as I did earlier with the YouTube videos. However, instead of using the iframe tags, I’ll instead need to use the img tag (used specifically for images) to embed these GIFs.

How this will work is that I need to use a link for each GIF that I intend to use to denote lbs conversion and kg conversions. The next step is to then wrap it under the particular output variable within the server section. This essentially will work similarly to a function as whatever is in this output exist within its own environment and has no impact within the global environment. The code will look like this:

output$gif = renderUI(
{
url_world = "https://media0.giphy.com/media/Imkav206U2lFe/giphy.gif?cid=ecf05e4715l254damg22jouth57rmt9yegwvkc4anbwp5all&rid=giphy.gif"
url_usa = "https://media0.giphy.com/media/8KnfG3knLExpu/giphy.gif?cid=ecf05e47ljiequiwggqz7cco0zup8tdkq817u6ecxy29c8eu&rid=giphy.gif"
determine = ifelse(input$unit == "kg", url_usa, url_world)
tags$img(src = determine, width = 800, height = 500)
}
)
Again, trying to skirt around the copyright here, but it works perfectly fine.

BOOM! I’ve got myself a somewhat functioning home workout app that you can probably use for your home workout needs.

Looking at this little project, I asked myself :

Nonetheless, I figured it would be cool to show what I’ve been working on to my boss to get his thoughts on the idea since I work at a fitness studio.

Not sure how exactly to take this…

Next Steps

Well assuming that I don’t get “best of luck on your future endeavors” for using 2 weeks of company time on this little venture while working from home, the next logical step is refining this a bit more. Obviously, the visuals on this thing need an overhaul in terms of color, fonts used, organization, etc. This will be best done by getting a better understanding of the markup languages like HTML and CSS since these are used in generating web pages and web applications.

Aside from that, it’s pretty easy to build on this as well by applying other metrics to include in the data set used as well as building other things that could be useful as well like formulating an actual plan or even create an algorithm that uses some kind of input to best select a template that you can fill in your exercises of interest.

All in all, this was a nice little attempt at exploring one of the most important features made available in R in one of the many ways to approach learning to program. Sure the learning curve in this is brutal, but hopefully, you can take some little things in this project and apply them to your own projects within the R language or check out what R has to offer if you’re coming in from Python.

If you see some typos, got some questions, want to share some ideas on future projects, or make a prediction on what happened with that call with the head honcho, let me know in the comments. If you want to reach out, hit me up on LinkedIn. Alternatively, if you would like to check out the entirety of the code, you can see it here on my GitHub along with my other projects.

Thanks for the read.

--

--