{"id":2634,"date":"2022-08-13T19:52:38","date_gmt":"2022-08-13T19:52:38","guid":{"rendered":"http:\/\/optimumsportsperformance.com\/blog\/?p=2634"},"modified":"2022-08-13T19:52:38","modified_gmt":"2022-08-13T19:52:38","slug":"shiny-user-defined-chart-parameters","status":"publish","type":"post","link":"https:\/\/optimumsportsperformance.com\/blog\/shiny-user-defined-chart-parameters\/","title":{"rendered":"Shiny &#8211; User Defined Chart Parameters"},"content":{"rendered":"<p>A colleague was working on a web app for his basketball team and asked me if there was a way to create a {shiny} web app that allowed the user to define which parameters they would like to see on the plot. I figured this would be something others might be interested in as well, so here we go!<\/p>\n<p><span style=\"text-decoration: underline;\"><strong>Load Packages, Helper Functions &amp; Data<\/strong><\/span><\/p>\n<p>I&#8217;ll use data from the Lahman baseball database (seasons 2017 &#8211; 2019). I&#8217;m also going to create two helper functions, one for calculating the z-scores for our stats of interest and one for calculating the t-value from the z-score. The t-value will put the z-score on a 0 to 100 scale for plotting purposes in our polar plot. Additionally, we will use these standardized scores to conditionally format colors on our {gt} table (but we will hide the standardized columns so that the user only sees the raw data and colors). Finally, I&#8217;m going to create both a wide and long format of the data as it will be easier to use one or the other, depending on the type of plot or table I am building.<\/p>\n<pre class=\"brush: r; title: ; notranslate\" title=\"\">\r\n#### Load packages ------------------------------------------------\r\nlibrary(tidyverse)\r\nlibrary(shiny)\r\nlibrary(Lahman)\r\nlibrary(gt)\r\nlibrary(plotly)\r\n\r\ntheme_set(theme_minimal() + \r\n            theme(\r\n              axis.text = element_text(face = &quot;bold&quot;, size = 12),\r\n              legend.title = element_blank(),\r\n              legend.position = &quot;none&quot;\r\n            ) )\r\n\r\n#### helper functions -------------------------------------------\r\n\r\nz_score &lt;- function(x){\r\n  z = (x - mean(x, na.rm = T)) \/ sd(x, na.rm = T)\r\n  return(z)\r\n}\r\n\r\nt_score &lt;- function(x){ t = (x * 10) + 50 t = ifelse(t &gt; 100, 100, \r\n             ifelse(t &lt; 0, 0, t))\r\n  return(t)\r\n}\r\n\r\n\r\n#### Get Data ---------------------------------------------------\r\n\r\ndat &lt;- Batting %&gt;%\r\n  filter(between(yearID, left = 2017, right = 2019),\r\n         AB &gt;= 200) %&gt;% \r\n  group_by(yearID, playerID) %&gt;%\r\n  summarize(across(.cols = G:GIDP,\r\n         ~sum(.x)),\r\n         .groups = &quot;drop&quot;) %&gt;%\r\n  mutate(ba = H \/ AB,\r\n         obp = (H + BB + HBP) \/ (AB + HBP + SF),\r\n         slg = ((H - X2B - X3B - HR) + X2B*2 + X3B*3 + HR*4) \/ AB,\r\n         ops = obp + slg,\r\n         hr_rate = H \/ AB) %&gt;%\r\n  select(playerID, yearID, AB, ba:hr_rate) %&gt;%\r\n  mutate(across(.cols = ba:hr_rate,\r\n                list(z = z_score)),\r\n         across(.cols = ba_z:hr_rate_z,\r\n                list(t = t_score))) %&gt;%\r\n  left_join(People %&gt;%\r\n              mutate(name = paste(nameLast, nameFirst, sep = &quot;, &quot;)) %&gt;%\r\n              select(playerID, name)) %&gt;%\r\n  relocate(name, .before = yearID)\r\n\r\n\r\ndat_long &lt;- Batting %&gt;%\r\n  filter(between(yearID, left = 2017, right = 2019),\r\n         AB &gt;= 200) %&gt;% \r\n  group_by(playerID) %&gt;%\r\n  summarize(across(.cols = G:GIDP,\r\n                   ~sum(.x)),\r\n            .groups = &quot;drop&quot;) %&gt;%\r\n  mutate(ba = H \/ AB,\r\n         obp = (H + BB + HBP) \/ (AB + HBP + SF),\r\n         slg = ((H - X2B - X3B - HR) + X2B*2 + X3B*3 + HR*4) \/ AB,\r\n         ops = obp + slg,\r\n         hr_rate = H \/ AB) %&gt;%\r\n  select(playerID, AB, ba:hr_rate) %&gt;%\r\n  mutate(across(.cols = ba:hr_rate,\r\n                list(z = z_score)),\r\n         across(.cols = ba_z:hr_rate_z,\r\n                list(t = t_score))) %&gt;%\r\n  left_join(People %&gt;%\r\n              mutate(name = paste(nameLast, nameFirst, sep = &quot;, &quot;)) %&gt;%\r\n              select(playerID, name)) %&gt;%\r\n  relocate(name, .before = AB) %&gt;%\r\n  select(playerID:AB, ends_with(&quot;z_t&quot;)) %&gt;%\r\n  pivot_longer(cols = -c(playerID, name, AB),\r\n               names_to = &quot;stat&quot;) %&gt;%\r\n  mutate(stat = case_when(stat == &quot;ba_z_t&quot; ~ &quot;BA&quot;,\r\n                          stat == &quot;obp_z_t&quot; ~ &quot;OBP&quot;,\r\n                          stat == &quot;slg_z_t&quot; ~ &quot;SLG&quot;,\r\n                          stat == &quot;ops_z_t&quot; ~ &quot;OPS&quot;,\r\n                          stat == &quot;hr_rate_z_t&quot; ~ &quot;HR Rate&quot;))\r\n\r\n\r\ndat %&gt;%\r\n  head()\r\n\r\ndat_long %&gt;%\r\n  head()\r\n<\/pre>\n<p>&nbsp;<\/p>\n<p><span style=\"text-decoration: underline;\"><strong>The Figures for our App<\/strong><\/span><\/p>\n<p>Before I build the {shiny} app, I wanted to first construct the three figures I will include. The code for these will be accessible in <strong><span style=\"color: #0000ff;\"><a style=\"color: #0000ff;\" href=\"https:\/\/github.com\/pw2\/R-Tips-Tricks\/blob\/master\/Shiny%20-%20User%20Defined%20Chart%20Parameters.R\">Github<\/a><\/span><\/strong>, but here is what they look like:<\/p>\n<ul>\n<li>For the polar plot, I will allow the user to define which variables they want on the chart.<\/li>\n<li>For the time series plot, I am going to create an interactive {plotly} chart that allows the user to select the stat they want to see and then hover over the player&#8217;s points and obtain information like the raw value and the number of at bats in the given season via a simple tool tip.<\/li>\n<li>The table, as discussed above, will user conditional formatting to provide the user with extra context about how that player performed relative to his peers in a given season.<\/li>\n<\/ul>\n<p><a href=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-13-at-12.37.59-PM.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-2635\" src=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-13-at-12.37.59-PM.png\" alt=\"\" width=\"993\" height=\"572\" srcset=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-13-at-12.37.59-PM.png 993w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-13-at-12.37.59-PM-300x173.png 300w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-13-at-12.37.59-PM-768x442.png 768w, https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2022\/08\/Screen-Shot-2022-08-13-at-12.37.59-PM-624x359.png 624w\" sizes=\"auto, (max-width: 993px) 100vw, 993px\" \/><\/a><\/p>\n<p>Because I don&#8217;t like to clutter up my {shiny} apps, I tend to build my plots and tables into custom functions. That way, all I need to do is set up a <strong>reactive() <\/strong>in the <strong>server<\/strong> to obtain the user selected data and then call the function on that data. Here are the functions for the three figures above.<\/p>\n<pre class=\"brush: r; title: ; notranslate\" title=\"\">\r\n## table function\r\ntbl_func &lt;- function(NAME){ dat %&gt;%\r\n  filter(name == NAME) %&gt;%\r\n  select(yearID, AB:hr_rate, ends_with(&quot;z_t&quot;)) %&gt;%\r\n  gt(rowname_col = &quot;yearID&quot;) %&gt;%\r\n  fmt_number(columns = ba:hr_rate,\r\n             decimals = 3) %&gt;%\r\n  cols_label(\r\n    AB = md(&quot;**AB**&quot;),\r\n    ba = md(&quot;**Batting Avg**&quot;),\r\n    obp = md(&quot;**OBP**&quot;),\r\n    slg = md(&quot;**SLG**&quot;),\r\n    ops = md(&quot;**OPS**&quot;),\r\n    hr_rate = md(&quot;**Home Run Rate**&quot;)\r\n  ) %&gt;%\r\n  tab_header(title = NAME) %&gt;%\r\n  opt_align_table_header(align = &quot;left&quot;) %&gt;%\r\n  tab_options(column_labels.border.top.color = &quot;transparent&quot;,\r\n              column_labels.border.top.width = px(3),\r\n              table.border.top.color = &quot;transparent&quot;,\r\n              table.border.bottom.color = &quot;transparent&quot;) %&gt;%\r\n  cols_align(align = &quot;center&quot;) %&gt;%\r\n  cols_hide(columns = ends_with(&quot;z_t&quot;)) %&gt;%\r\n    tab_style(\r\n      style = cell_fill(color = &quot;palegreen&quot;),\r\n      location = cells_body(\r\n        columns = ba,\r\n        rows = ba_z_t &gt; 60\r\n      )\r\n    )  %&gt;%\r\n    tab_style(\r\n      style = cell_fill(color = &quot;red&quot;),\r\n      location = cells_body(\r\n        columns = ba,\r\n        rows = ba_z_t &lt; 40 ) ) %&gt;%\r\n    tab_style(\r\n      style = cell_fill(color = &quot;palegreen&quot;),\r\n      location = cells_body(\r\n        columns = obp,\r\n        rows = obp_z_t &gt; 60\r\n      )\r\n    )  %&gt;%\r\n    tab_style(\r\n      style = cell_fill(color = &quot;red&quot;),\r\n      location = cells_body(\r\n        columns = obp,\r\n        rows = obp_z_t &lt; 40 ) ) %&gt;%\r\n    tab_style(\r\n      style = cell_fill(color = &quot;palegreen&quot;),\r\n      location = cells_body(\r\n        columns = slg,\r\n        rows = slg_z_t &gt; 60\r\n      )\r\n    )  %&gt;%\r\n    tab_style(\r\n      style = cell_fill(color = &quot;red&quot;),\r\n      location = cells_body(\r\n        columns = slg,\r\n        rows = slg_z_t &lt; 40 ) ) %&gt;%\r\n    tab_style(\r\n      style = cell_fill(color = &quot;palegreen&quot;),\r\n      location = cells_body(\r\n        columns = ops,\r\n        rows = ops_z_t &gt; 60\r\n      )\r\n    )  %&gt;%\r\n    tab_style(\r\n      style = cell_fill(color = &quot;red&quot;),\r\n      location = cells_body(\r\n        columns = ops,\r\n        rows = ops_z_t &lt; 40 ) ) %&gt;%\r\n    tab_style(\r\n      style = cell_fill(color = &quot;palegreen&quot;),\r\n      location = cells_body(\r\n        columns = hr_rate,\r\n        rows = hr_rate_z_t &gt; 60\r\n      )\r\n    )  %&gt;%\r\n    tab_style(\r\n      style = cell_fill(color = &quot;red&quot;),\r\n      location = cells_body(\r\n        columns = hr_rate,\r\n        rows = hr_rate_z_t &lt; 40\r\n      )\r\n    ) \r\n}\r\n\r\n\r\n## Polar plot function\r\npolar_plt &lt;- function(NAME, STATS){ dat_long %&gt;%\r\n    filter(name == NAME,\r\n           stat %in% STATS) %&gt;%\r\n    ggplot(aes(x = stat, y = value, fill = stat)) +\r\n    geom_col(color = &quot;white&quot;, width = 0.75) +\r\n    coord_polar(theta = &quot;x&quot;) +\r\n    geom_hline(yintercept = seq(50, 50, by = 1), size = 1.2) +\r\n    labs(x = &quot;&quot;, y = &quot;&quot;) +\r\n    ylim(0, 100)\r\n  \r\n} \r\n\r\n\r\n## time series plot function\r\ntime_plt &lt;- function(NAME, STAT){\r\n  \r\n  STAT &lt;- case_when(STAT == &quot;BA&quot; ~ &quot;ba&quot;,\r\n                    STAT == &quot;OBP&quot; ~ &quot;obp&quot;,\r\n                    STAT == &quot;SLG&quot; ~ &quot;slg&quot;,\r\n                    STAT == &quot;OPS&quot; ~ &quot;ops&quot;,\r\n                    STAT == &quot;HR Rate&quot; ~ &quot;hr_rate&quot;)\r\n  \r\n  stat_z &lt;- paste0(STAT, &quot;_z&quot;)\r\n  \r\n  p &lt;- dat %&gt;% \r\n    filter(name == NAME) %&gt;%\r\n    select(yearID, AB, STAT, stat_z) %&gt;%\r\n    setNames(., c(&quot;yearID&quot;, &quot;AB&quot;, &quot;STAT&quot;, &quot;stat_z&quot;)) %&gt;%\r\n    ggplot(aes(x = as.factor(yearID), \r\n               y = stat_z,\r\n               group = 1,\r\n               label = NAME,\r\n               label2 = AB,\r\n               lable3 = STAT)) +\r\n    geom_hline(yintercept = 0,\r\n               size = 1.1,\r\n               linetype = &quot;dashed&quot;) +\r\n    geom_line(size = 1.2) +\r\n    geom_point(shape = 21,\r\n               size = 6,\r\n               color = &quot;black&quot;,\r\n               fill = &quot;white&quot;) +\r\n    ylim(-4, 4) \r\n  \r\n  \r\n  ggplotly(p)\r\n  \r\n}\r\n<\/pre>\n<p>&nbsp;<\/p>\n<p><span style=\"text-decoration: underline;\"><strong>Build the {shiny} app<\/strong><\/span><\/p>\n<p>The below code will construct the {shiny} app. We allow the user to select a player, select the stats of interest for the polar plot, and select the stat they&#8217;d like to track over time.<\/p>\n<p>If you&#8217;d like to see a video of the app in use, <strong>CLICK HERE<\/strong> &lt;<span style=\"color: #0000ff;\"><strong><a style=\"color: #0000ff;\" href=\"https:\/\/optimumsportsperformance.com\/blog\/wp-content\/uploads\/2022\/08\/shiny-user-defined-chart-parameters.mov\">shiny &#8211; user defined chart parameters<\/a><\/strong><\/span>&gt;<\/p>\n<p>If you want to run this yourself or build one similar to it you can access my code on <strong><span style=\"color: #0000ff;\">GitHub.<\/span><\/strong><\/p>\n<p>&nbsp;<\/p>\n<pre class=\"brush: r; title: ; notranslate\" title=\"\">\r\n#### Shiny App ---------------------------------------------------------------\r\n\r\n## User Interface\r\nui &lt;- fluidPage(\r\n  \r\n  titlePanel(&quot;MLB Hitters Shiny App\\n2017-2019&quot;),\r\n  \r\n  \r\n  sidebarPanel(width = 3,\r\n             selectInput(&quot;name&quot;,\r\n                             label = &quot;Choose a Player:&quot;,\r\n                             choices = unique(dat$name),\r\n                             selected = NULL,\r\n                             multiple = FALSE),\r\n              \r\n              selectInput(&quot;stat&quot;,\r\n                          label = &quot;Choose stats for polar plot:&quot;,\r\n                          choices = unique(dat_long$stat),\r\n                          selected = NULL,\r\n                          multiple = TRUE),\r\n              \r\n              selectInput(&quot;time_stat&quot;,\r\n                          label = &quot;Choose stat for time series:&quot;,\r\n                          choices = unique(dat_long$stat),\r\n                          selected = NULL,\r\n                          multiple = FALSE)\r\n  ),\r\n  \r\n  \r\n  mainPanel(\r\n    \r\n    gt_output(outputId = &quot;tbl&quot;),\r\n    \r\n    fluidRow(\r\n      \r\n      column(6, plotOutput(outputId = &quot;polar&quot;)),\r\n      column(6, plotlyOutput(outputId = &quot;time&quot;))\r\n    )\r\n    \r\n  )\r\n)\r\n\r\n\r\n\r\nserver &lt;- function(input, output){\r\n  \r\n  ## get player selected for table\r\n  NAME &lt;- reactive({ dat_long %&gt;%\r\n      filter(name == input$name) %&gt;%\r\n      distinct(name, .keep_all = FALSE) %&gt;%\r\n      pull(name)\r\n    })\r\n  \r\n  ## get stats for polar plot\r\n  polar_stats &lt;- reactive({ dat_long %&gt;%\r\n      filter(stat %in% c(input$stat)) %&gt;%\r\n      pull(stat)\r\n    \r\n  })\r\n  \r\n  ## get stat for time series\r\n  ts_stat &lt;- reactive({ dat %&gt;%\r\n      select(ba:hr_rate) %&gt;%\r\n      setNames(., c(&quot;BA&quot;, &quot;OBP&quot;, &quot;SLG&quot;, &quot;OPS&quot;, &quot;HR Rate&quot;)) %&gt;%\r\n      select(input$time_stat) %&gt;% \r\n      colnames()\r\n    \r\n  })\r\n  \r\n  ## table output\r\n  output$tbl &lt;- render_gt(\r\n      tbl_func(NAME = NAME())\r\n  )\r\n  \r\n  ## polar plot output\r\n  output$polar &lt;- renderPlot(\r\n    \r\n    polar_plt(NAME = NAME(),\r\n              STAT = polar_stats())\r\n    \r\n  )\r\n  \r\n  ## time series plot output\r\n  output$time &lt;- renderPlotly(\r\n    \r\n    time_plt(NAME = NAME(),\r\n             STAT = ts_stat())\r\n    \r\n  )\r\n\r\n  \r\n}\r\n\r\n\r\n\r\n\r\n\r\nshinyApp(ui, server)\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>A colleague was working on a web app for his basketball team and asked me if there was a way to create a {shiny} web app that allowed the user to define which parameters they would like to see on the plot. I figured this would be something others might be interested in as well, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[45,43,42,27],"tags":[],"class_list":["post-2634","post","type-post","status-publish","format-standard","hentry","category-r-tips-tricks","category-sports-analytics","category-sports-science","category-strength-and-conditioning"],"_links":{"self":[{"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/posts\/2634","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/comments?post=2634"}],"version-history":[{"count":1,"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/posts\/2634\/revisions"}],"predecessor-version":[{"id":2637,"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/posts\/2634\/revisions\/2637"}],"wp:attachment":[{"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/media?parent=2634"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/categories?post=2634"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/optimumsportsperformance.com\/blog\/wp-json\/wp\/v2\/tags?post=2634"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}