Taras Kaduk

7 minute read

(One year after building it and forgetting it)

Cross-posted: Medium

 

Is it just me, or is your hard drive also full of abandoned projects, ideas et cetera? I know I’m not alone.

Cleaning up my R folder the other day, I stumbled upon a file I hardly remember creating. It is was a Shiny app built to help one calculate the hourly rate of services, given a desired income and an amount of time working (billable hours in a day, days in a week, weeks in a year). I think I built it watching the RStudio intro webinars into Shiny: part 1, part 2 and part 3.

The idea is that the user inputs all the values, and then the app suggests how to change any of the values to bring the system to the equilibrium.

Now that I’ve found the file, I’m publishing it as is at https://taraskaduk.shinyapps.io/rate/. Below is the app, the explanation and the code-through.

The app

Here is the app itself, hosted on https://www.shinyapps.io/. (LMK if you don’t see the embeded app here: something must have gone wrong) 1

How does it work

I know, I know, good UI needs no explanation. Please pardon my poor design. Finessing the details is something I may work on later. Today, I’m living by “ship daily”, and so I ship my minimum viable product

So, it works like this. First, you enter your desired figures into the input box. Let’s say, you are guessing a $75/hr rate, and you’re hoping to have 6 billable hours a day, working 5 days a week, 48 weeks a year. You’re hoping to make $100,000 a year in gross income

Now looking at the output pane, the result is not too far from our initial estimate, but we’re slightly off, making only $108,000 instead of $120,000:

How can we get to $120K? Well, the app suggests a few options:

  • we can bump up our rate to $83/hr
  • we can shoot for more than 6 hours billable hours a day (it still says 6, but that’s obviously rounding)
  • we could keep the billable hours and the rate the same, but work more days a week
  • we can leave all the parameters as they are, but increase the amount of weeks working from 48 to… 53. Bummer, a year only has 52 weeks, therefore this won’t work.
  • we could settle for less and keep the $108K
  • finally, we could find a compromise by changing every parameter slightly.

Moving the needle on any of these parameters will “stabilize” the system. Watch:

Changing the rate from $75 to $84/hour Changing the rate to $80/hour and working 50 weeks a year

That is pretty much it, in a nutshell. I was interested in making this kind dynamic calculation possible. It took me a while, but it works as expected.

The code

Here is a reminder for me: always annotate your code! When I dug it out last week, I could barely understand anything. I still don’t get a solid chunk of it.

I’m going to annotate it as much as I can, with the time I have. Hopefully you can comb through it.

Any improvements? Suggestions? It’s all on Github — you know what to do!

https://github.com/taraskaduk/shiny_app_rate

library(ggplot2)
library(dplyr)
library(tibble)
library(dplyr)
library(shiny)
library(rsconnect)
library(scales)


# UI ----------------------------------------------------------------------

ui <- fluidPage(
  headerPanel('Hourly rate calculator by Taras Kaduk'),
  sidebarPanel(

    # I should have probably parametirized the mins and maxs.
    sliderInput('iRate', 'Rate', 70,
                min = 50, max = 200),
    sliderInput('iHours', 'Hours', 8,
                min = 5, max = 12),
    sliderInput('iDays', 'Days', 5,
                min = 4 , max = 7),
    sliderInput('iWeeks', 'Weeks', 48,
                min = 42, max = 52),
    sliderInput('iIncome', 'Income', 145000,
                min = 55000, max = 200000, step = 1000)
  ),
  mainPanel(
    plotOutput('plot1', width = 600, height = 450),
    plotOutput('plot2', width = 600, height = 100)
  )
)


# Server ------------------------------------------------------------------

server <- function(input, output) {
  
  #These are initial inputs
  iRate <- reactive({input$iRate})
  iHours <- 10
  iDays <- 7
  iWeeks <- 48
  iIncome <- 145000
  
  # Colors
  col_need <- 'red4'
  col_value <- 'grey30'
  
  # Calculating annual income based on inputs.
  # BTW, what is all this reactive({ }) stuff? Shiny syntax baby!
  calcIncome <- reactive({input$iRate * input$iHours  * input$iDays * input$iWeeks})
  
  # Creating a vector with input variables.
  # Why vector? I don't remember.
  # Tried switching to a data frame and failed.
  vInput <- reactive({
    c("Rate" = input$iRate, 
      "Hours" = input$iHours, 
      "Days" = input$iDays, 
      "Weeks" = input$iWeeks, 
      "Income" = calcIncome())
  })
  
  # Another 2 vectors, this time for min & max.
  # Still wondering why these are not in a dataframe...
  vMin <- as.integer(c(50,6,4,40,55000))
  vMax <- as.integer(c(200,12,7,52,200000))
  
  # OK, now compiling it all into a dataframe. 
  # I swear I'm not sure why I haven't started with a data frame...
  df <- reactive({data_frame(Param = names(vInput()), 
                             Value = vInput(), 
                             Min = vMin, 
                             Max = vMax, 
                             
                             ## this line below calculates how much you NEED of the current param,
                             ## ...all other things being equal
                             Need = as.integer(input$iIncome/(calcIncome()/vInput())),
                             
                             ## next 4 lines take the real value and scale them to fit into 1 graph
                             ## you don't want days and hours to be displayed to scale with weeks and...
                             ## ...hundreds of thousands of dollars
                             normNeed = 0,
                             normMin = (vMin - input$iIncome/(calcIncome()/vInput()))/(vMax - vMin),
                             normMax = (vMax - input$iIncome/(calcIncome()/vInput()))/(vMax - vMin),
                             normValue = (vMin - input$iIncome/(calcIncome()/vInput()))/
                                         (vMax - vMin) + 
                                         ((vInput()-vMin))/(vMax - vMin)
  )})
  
  # OK, I added this piece of code recently.
  # I wanted to break out the initial data frame.
  # Otherwise, the total income looked weird on the graph. 
  # It was, like, reversed to the other params
  df2 <- reactive({df() %>% filter(Param != 'Income')})
  df3 <- reactive({df() %>% filter(Param == 'Income')})
  
  
  # Building the plot. Plot1 - plot of all params BUT the annual income
  output$plot1 <- renderPlot({
    ggplot(df2(), aes(x = factor(Param, levels = c("Weeks", "Days", "Hours", "Rate")))) +
      
      # Segment between min and max
      # Basically, the grey lines with 2 points at the ends of each line
      geom_segment(aes(xend = Param, y = normMin, yend = normMax), size = 1, col = "grey")+ 
      geom_point(aes(y = normMax), col = "dark grey") +
      geom_point(aes(y = normMin), col = "dark grey") +
      geom_text(aes(y = normMin, label = Min), position = position_nudge(x=-0.1, y=0), size = 4, col = "dark grey")+
      geom_text(aes(y = normMax, label = Max), position = position_nudge(x=-0.1, y=0), size = 4, col = "dark grey")+
      
      #The "need". AKA how much it is supposed to be
      geom_text(aes(y = 0, label = Need), position = position_nudge(x=0.3, y=0.1), size = 6, col = col_need)+
      geom_point(aes(y = normNeed), size = 4, col = col_need) +      
      
      #Your current value
      geom_point(aes(y = normValue), size = 4, col = col_value) +
      geom_text(aes(y = normValue, label = Value), position = position_nudge(x=-0.3), size = 6, col = col_value)+
      
      xlab("")+
      ylab("")+
      labs(
        title = 'Entered and calculated parameters based on other entered parameters'
      ) +
      theme(axis.text.y=element_text(size=14,colour="#535353",face="bold"),
            axis.text.x = element_blank(),
            axis.ticks = element_blank()) +
      coord_flip()
  })
  
  #Plot2 - just the annual income.
  #Same stuff, only dots are reversed.
  #Also, $ formatting applied.
  output$plot2 <- renderPlot({
    ggplot(df3(), aes(x = Param)) +
      
      geom_segment(aes(xend = Param, y = normMin, yend = normMax), size = 1, col = "grey")+ 
      geom_point(aes(y = normMax), col = "dark grey") +
      geom_point(aes(y = normMin), col = "dark grey") +
      geom_text(aes(y = normMin, label = paste0('$',comma(Min))), position = position_nudge(x=-0.1, y=0), size = 4, col = "dark grey")+
      geom_text(aes(y = normMax, label = paste0('$',comma(Max))), position = position_nudge(x=-0.1, y=0), size = 4, col = "dark grey")+      
         
      geom_text(aes(y = 0, label = paste0('$',comma(Need))), position = position_nudge(x=0.3, y=0.1), size = 6, col = col_value)+
      geom_point(aes(y = normNeed), size = 4, col = col_value) +      
      
      
      geom_point(aes(y = normValue), size = 7, col = 'red') +
      geom_text(aes(y = normValue, label = paste0('$',comma(Value))), position = position_nudge(x=-0.3), size = 7, col = 'red')+
      
      xlab("")+
      ylab("")+
      labs(
        title = 'Entered and calculated income based on other entered parameters'
      ) +
      theme(axis.text.y=element_text(size=14,colour="#535353",face="bold"),
            axis.text.x = element_blank(),
            axis.ticks = element_blank()) +
      coord_flip()
  })  
  
}

# Et voila!
shinyApp(ui = ui, server = server)

  1. I must say, it is quite nice to be able to embed my Shiny app here on my site which runs on blogdown: my original Medium post wasn’t able to do that

comments powered by Disqus