inst/shiny/app.R

library(shiny)
library(shinythemes)
library(shinycssloaders)
library(rmcorr)
library(glue)
library(stringr)
library(tidyr)
library(dplyr)
library(svglite)
library(bslib) #https://rstudio.github.io/bslib/index.html
library(ggiraph)
library(rclipboard)

options(shiny.maxRequestSize=30*1024^2)

#Items to add:
#1) Do we want to do something different about how the bootstrap resamples are presented?
#2) I'm thinking maybe leave out the citation section in the About page until we can list the paper? Jon: Sounds good

#Pie in the sky items
#1) Power calculation: Could be an additional panel?
#2) Interactive Tutorial or video tutorial with narrated overview? Doesn't have to be as fancy as this one, but I like the general approach:
# http://www.higherordernetwork.com/ (NS-CTA funded work)
#3) bindCache() with plotting
#c) Going pass on this one for now. Jon to-do item: Cut off the first color in sequential by default? The first color is usually too faint to see

light <- bs_theme()
dark  <- bs_theme(bg = "black", fg = "white", primary = "lightblue")

options(shiny.sanitize.errors = T)

ui <- fluidPage(
  tags$head(includeHTML(("google-analytics.html"))),
  theme = light,
  rclipboardSetup(),
  div(
    class = "custom-control custom-switch",
    tags$input(
      id = "dark_mode", type = "checkbox", class = "custom-control-input",
      onclick = HTML("Shiny.setInputValue('dark_mode', document.getElementById('dark_mode').value);")
    ),
    tags$label("Dark mode", `for` = "dark_mode", class = "custom-control-label")),
  # CSS, fixes palette picker
  tags$style(".bootstrap-select .dropdown-menu li a span.text {width: 100%;}"),
             #downloadPlot, #downloadZip {margin-top: 25px}"),

  #Title
  titlePanel("rmcorrShiny"),

  # Sidebar
  sidebarLayout(
    sidebarPanel(
      tabsetPanel(
        # type = "pills",
        tabPanel("Input",
                 dataUploadUI("rmcorr", label = "File input")),
        tabPanel("Data Options",
                 uiOutput('DataFilterColumnsUI'),
                 hr(),
                 sliderInput("CIlevel",
                             label = h5("Confidence Intervals"),
                             min = 0.5,
                             max = 0.99,
                             value = 0.95,
                             step = 0.01),

                 checkboxInput("bootstrap", "Bootstrap CIs?", FALSE),
                 conditionalPanel(
                   condition = 'input.bootstrap == true',

                   numericInput("bootseed",
                                label = h5("Bootstrapping Seed Value"),
                                value = 33,
                                step  = 1,
                                min   = -999,
                                max   = 999),

                   numericInput("bootstrapnreps",
                                label = h5("Number of resamples"),
                                value = 100,
                                min = 10),

                   checkboxInput("bootstrapout", "Show resamples?", FALSE)
                 )
        ),
        tabPanel("Plot Options",
                 hr()
                 ,
                 tabsetPanel(
                   # type = "pills",
                   tabPanel("General Options",
                            column(12,
                                   h5("Size"),
                                   numericInput("height",
                                                label = "Plot Height:",
                                                value = 600),
                                   numericInput("width",
                                                label = "Plot Width:",
                                                value = 600),
                                   h5("Legend"),
                                   checkboxInput("plotLegend",
                                                 "Show Legend",
                                                 FALSE),
                                   conditionalPanel(
                                     condition = 'input.plotLegend == true',
                                     uiOutput('legendTitleUI')
                                     # textInput("legendTitle",
                                     #           label = "Legend Title",
                                     #           value = "Legend Title")
                                   ),
                                   h5("Annotations"),
                                   checkboxInput("addText",
                                                 "Add rmcorr output text",
                                                 FALSE),
                                   conditionalPanel(
                                     condition = 'input.addText == true',
                                     selectInput("textLocation",
                                                 label = "rmcorr text location",
                                                 choices = list(
                                                   "topleft",
                                                   "topright",
                                                   "bottomleft",
                                                   "bottomright"
                                                 ))),
                                   h5("Gridlines"),
                                   checkboxInput("plotMajorGrid",
                                                 "Plot Major Grid",
                                                 FALSE),

                                   conditionalPanel(
                                     condition = 'input.plotMajorGrid == true',
                                     checkboxInput("plotMinorGrid",
                                                   "Plot Minor Grid",
                                                   FALSE)),
                                   hr())),
                   tabPanel("Theme and colors",

                            column(12,
                                   selectInput("plotTheme",
                                               label = h5("Theme"),
                                               choices = list(
                                                 "Default (Grey)" = "theme_grey()",
                                                 "Black & White" = "theme_bw()",
                                                 "Linedraw" = "theme_linedraw()",
                                                 "Light" = "theme_light()",
                                                 "Dark" = "theme_dark()",
                                                 "Minimal" = "theme_minimal()",
                                                 "Classic" = "theme_classic()",
                                                 "Void" = "theme_void()",
                                                 "Cowplot" = "theme_cowplot()"
                                               ),
                                               selected = "theme_cowplot()")),
                            column(12,
                                   pickerInput(
                                     inputId = "plotPalette",
                                     label = h5("Palette"),
                                     choices = colors_pal,
                                     selected = "Set1",
                                     width = "100%",
                                     choicesOpt = list(
                                       content = sprintf(
                                         "<div style='width:100%%;padding:2px;border-radius:4px;background:%s;color:%s'>%s</div>",
                                         unname(background_pals),
                                         colortext_pals,
                                         names(background_pals)
                                       )
                                     )
                                   )),
                            column(12,

                                   hr())),
                   tabPanel("Titles",
                            column(12,
                                   textInput("plotTitle",
                                             label = h5("Main Plot Title"),
                                             value = "Main Plot Title"),
                                   textInput("xAxisTitle",
                                             label = h5("x Axis Title"),
                                             value = "x Axis Title"),
                                   textInput("yAxisTitle",
                                             label = h5("y Axis Title"),
                                             value = "y Axis Title"),
                                   numericInput("titleFontSize",
                                                label = h5("Main Title Font Size"),
                                                value = 20,
                                                min = 0),
                                   numericInput("axisLabelFontSize",
                                                label = h5("Axis Title Font Size"),
                                                value = 15,
                                                min = 0),
                                   hr()))
                   ,
                   tabPanel("Scale",
                            column(12,
                                   numericInput("scaleFontSize",
                                                label = h5("Scale Text Font Size"),
                                                value = 15,
                                                min = 0),
                                   numericInput("xAxisAngle",
                                                label = h5("X Axis Scale Angle"),
                                                value = 0,
                                                min = 0,
                                                max = 360),
                                   selectInput("xAxishjust",
                                               label = h5("Horizontal Scale Justification"),
                                               choices = list(
                                                 "Left" = 0,
                                                 "Middle" = 0.5,
                                                 "Right" = 1),
                                               selected = .5),

                                   selectInput("yAxisvjust",
                                               label = h5("Vertical Scale Justification"),
                                               choices = list(
                                                 "Top" = 0,
                                                 "Middle" = 0.5,
                                                 "Bottom" = 1),
                                               selected = .5),

                                   checkboxInput("autoScale",
                                                 "Automatic Scale Limits",
                                                 TRUE)),
                            conditionalPanel(
                              condition = "input.autoScale == false",
                              uiOutput("scaleLimitsUI")
                            ),
                            column(12,
                                   hr()))
                 )
        )
      )
    ),
    mainPanel(
      tabsetPanel(type = "tabs",
                  tabPanel("Results",
                           column(8,
                                  h4("Variables"),
                                  withSpinner(
                                    htmlOutput("showvariables")

                                  ),
                                  h4("Output from rmcorr"),
                                  withSpinner(
                                    htmlOutput("rmcorrResults"))
                                  ,
                                conditionalPanel(
                                    condition = 'input.bootstrap == false',
                                    h4("Reportable results")),
                                conditionalPanel(
                                    condition = 'input.bootstrap == true',
                                  h4("Reportable results (bootstrapped CI)")),
                                withSpinner(
                                  htmlOutput("rmcorrReportable")),
                                conditionalPanel(
                                    condition = 'input.bootstrapout == true',
                                    br(),
                                    h4("Bootstrapped resamples")
                                    ,
                                    withSpinner(
                                      tableOutput("bootstrapResamples"))
                                  ))),
                  tabPanel("Plot",
                           br(),
                           withSpinner(
                             girafeOutput("rmcorrPlot",
                                        height = "auto"))

                  ),
                  tabPanel("R Code",
                           h4("R Code"),
                           withSpinner(verbatimTextOutput("rmcorrCode")),
                           uiOutput("clip"),
                           br()),
                  tabPanel("Processed Data",
                           # verbatimTextOutput("rmcorrDataSummary"),
                           htmlOutput("rmcorrDataSummary"),
                           tableOutput("rmcorrData")),
                  tabPanel("Download",
                           htmlOutput("rmcorrDownload")),
                  tabPanel("About",
                           includeHTML("www/about.html"))

      )
    )
  )
)


server <- function(input, output, session) {

  colsupdated = reactiveVal(FALSE)


  observe({
    session$setCurrentTheme(
      if (isTRUE(input$dark_mode)) dark else light
    )
  })

  # Read the input data.
  inputData <- dataUpload(id = "rmcorr")


  # Process the data. This is a reactive depending on the inputData!
  processedData <- reactive({
    req(inputData$name())
    req(input$subColumn)
    req(colsupdated())
    # browser()
    dataManipulation(input)
  })


  dataManipulation <- function(input) {

    subColumn <- input$subColumn
    m1Column <- input$m1Column
    m2Column <- input$m2Column

    cleanedData <- select(inputData$inputData(),all_of(c(subColumn, m1Column, m2Column)))
    df <- reactive({cleanedData}) %>% bindCache(cleanedData)

    my.rmc = reactive({
      req(input$subColumn)
      set.seed(input$bootseed)

      validate(
        need(is.numeric(cleanedData[[m1Column]]), "The 'Measure 1' column must be numeric"),
        need(is.numeric(cleanedData[[m2Column]]), "The 'Measure 2' column must be numeric")
      )
        rmcorr(participant = subColumn,
               measure1 = m1Column,
               measure2 = m2Column,
               dataset = cleanedData,
               CI.level = input$CIlevel,
               CIs = ifelse(input$bootstrap, "bootstrap", "analytic"),
               nreps = input$bootstrapnreps,
               bstrap.out = input$bootstrapout)

    })

    n = reactive({
      length(unique(cleanedData[[subColumn]]))
    }) %>% bindCache(unique(cleanedData[[subColumn]]))

    ##Add code for calculating rmcorr
    code <- reactive({
      glue('## Get sample size
n <- length(unique(inputData${subColumn}))\n
## Calculate rmcorr using selected columns
my.rmc <- rmcorr(participant = {subColumn},
                 measure1 = {m1Column},
                 measure2 = {m2Column},
                 dataset = inputData,
                 CI.level = {input$CIlevel},
                 CIs = "{ifelse(input$bootstrap, "bootstrap", "analytic")}",
                 nreps = {input$bootstrapnreps},
                 bstrap.out = {input$bootstrapout})\n\n')
    })

    return(list(
      rmc = my.rmc,
      df = df,
      n = n,
      code = code
    ))
  }

  output$bootstrapResamples <- renderTable({
    # We don't render the table without inputData.
    req(inputData$name())
    processedData()$rmc()$resamples
  }, colnames = F, rownames = T) %>% bindCache(processedData()$rmc()$resamples)

  # UI - legend title
  output$legendTitleUI <- renderUI({
    req(inputData$name())
    tagList(
      textInput("legendTitle",
            label = "Legend Title",
            value = input$subColumn)
    )})
  outputOptions(output, "legendTitleUI", suspendWhenHidden = FALSE)

  # UI - Plot - default scale limits.
  output$scaleLimitsUI <- renderUI({
    tagList(
      column(12,
             numericInput("xminScale",
                          label = h6("X Minimum"),
                          value = floor(min(processedData()$df()[[input$m1Column]]))),
             numericInput("xmaxScale",
                          label = h6("X Maximum"),
                          value = ceiling(max(processedData()$df()[[input$m1Column]]))),
             numericInput("yminScale",
                            label = h6("Y Minimum"),
                            value = floor(min(processedData()$df()[[input$m2Column]]))),
             numericInput("ymaxScale",
                          label = h5("Y Maximum"),
                          value = ceiling(max(processedData()$df()[[input$m2Column]])))
      )
    )
  })

  # UI - Data - Filter the data.
  output$DataFilterColumnsUI <- renderUI({
    req(inputData$conditions()) #conditions are the column names
    tagList(
      selectInput('subColumn',
                  label = HTML("<h5>Detected columns</h5>
                             Subject column:"),
                  choices = inputData$conditions(),
                  selected = inputData$conditions()[1],
                  multiple = FALSE),
      selectInput('m1Column',
                  label = HTML("Measure 1 column"),
                  choices = inputData$conditions(),
                  selected = inputData$conditions()[2],
                  multiple = FALSE),
      selectInput('m2Column',
                  label = HTML("Measure 2 column:"),
                  choices = inputData$conditions(),
                  selected = inputData$conditions()[3],
                  multiple = FALSE)
    )
  })
  outputOptions(output, "DataFilterColumnsUI", suspendWhenHidden = FALSE, priority = 10)


  output$showvariables <- renderUI({
    req(inputData$name())
    subvar <- paste("Subject variable: <b>", input$subColumn, "</b>")
    m1var <- paste("Measure 1: <b>", input$m1Column, "</b>")
    m2var <- paste("Measure 2: <b>", input$m2Column, "</b>")

    HTML(paste(subvar, m1var, m2var, sep = '</br>'))

  })


  # update plot titles:
  observeEvent(input$m1Column, {
    updateTextInput(session, "xAxisTitle", value = input$m1Column)}
  )
  observeEvent(input$m2Column, {
    updateTextInput(session, "yAxisTitle", value = input$m2Column)}
  )

  observeEvent({
    input$subColumn
    input$m1Column
    input$m2Column
  }, {
    colsupdated(TRUE)
  })

  observeEvent(inputData$name(),{
    colsupdated(FALSE)
  }, priority = 100)


  # Generate the plot code based on input options but do not evaluate yet.
  plotCode <- reactive({createPlot(input)}) %>% bindCache({createPlot(input)})

  # Evaluate the code based on the processed data.
  plotFigure <- reactive({
    plotData <- inputData$inputData()
    my.rmc <- processedData()$rmc()
    n <- processedData()$n()
    eval(parse(text = glue(plotCode()$interactive)))
  })

  # Render the plot.
  output$rmcorrPlot <- renderGirafe({
    # We don't render the plot without inputData.
    req(inputData$name())
    req(plotFigure())
    # browser()
    girafe(ggobj = plotFigure(),
           width_svg = input$width/72,
           height_svg = input$height/72,
           options = list(
             opts_sizing(rescale = FALSE),
             opts_hover_inv(css = "opacity:0.2"),
             opts_hover(css = girafe_css(
               css = "opacity:1;r:2pt;cursor:pointer",
               line = "stroke-width:2px"
             )),
             opts_selection(type = "none"),
             opts_toolbar(saveaspng = F),
             opts_zoom(max = 5),
             opts_tooltip(offx=15)
           )
    )
  })


  # ScriptCode
  scriptCode <- reactive({
    # cat(file=stderr(), processedData()$code())
    formatCode(input, inputData$code(), processedData()$code()
               , plotCode()$static
    )
  })


  output$clip <- renderUI({
    rclipButton(
      inputId = "clipbtn",
      label = "copy code",
      clipText = scriptCode(),
      icon = icon("clipboard")
    )
  })

  output$rmcorrCode <- renderText({
    # We don't render the code without inputData.
    req(inputData$name())
    # inputData$code()
    scriptCode()

  }) %>% bindCache(scriptCode())

  # Print the data
  output$rmcorrResults <- renderUI({
    req(inputData$name())
    CIlevel <- processedData()$rmc()$CI.level * 100
    str_rrm <- paste("Repeated measures correlation: ", round(processedData()$rmc()$r, digits = 3))
    str_df  <- paste("Degrees of freedom: ", processedData()$rmc()$df)
    str_p   <- paste("p-value:", ifelse(processedData()$rmc()$p < .001,
                                        format(processedData()$rmc()$p, scientific = T, digits =3),
                                               round(processedData()$rmc()$p, digits = 3)))
    str_CI  <- paste(CIlevel,"% Confidence Interval: ",
                     paste0(round(processedData()$rmc()$CI[1], digits = 3), sep = ", ", round(processedData()$rmc()$CI[2], digits = 3)), sep = "")
    HTML(paste(str_rrm, str_df, str_p, str_CI, sep = '</br>'))

  })

  output$rmcorrReportable <- renderUI({
    req(inputData$name())
    CIlevel <- processedData()$rmc()$CI.level * 100
    HTML(glue("r<sub>rm</sub>({processedData()$rmc()$df}) =
              {format(round(processedData()$rmc()$r, digits = 2), nsmall = 2)},
              {CIlevel}% CI [{round(processedData()$rmc()$CI[1], digits = 3)},
              {round(processedData()$rmc()$CI[2], digits = 3)}],
              {ifelse(processedData()$rmc()$p < .001, 'p < 0.001', paste('p = ',round(processedData()$rmc()$p, digits = 3),sep = ''))}"))
  })

  output$rmcorrDataSummary <- renderPrint({
    # We don't render the table without inputData.
    req(inputData$name())
    # summary(processedData()$df())
    HTML(glue("<h5>Sample size:</h5>
    N = {length(unique(inputData$inputData()[[input$subColumn]]))}
              <h5>Number of repeated measures (k):</h5>
              k<sub>avg</sub> = {mean(table(inputData$inputData()[[input$subColumn]]))}<br>
              k<sub>min</sub> = {min(table(inputData$inputData()[[input$subColumn]]))}<br>
              k<sub>max</sub> = {max(table(inputData$inputData()[[input$subColumn]]))}
              <h5>Dataset:</h5>"))
  })

  output$rmcorrData <- renderTable({
    # We don't render the table without inputData.
    req(inputData$name())
    inputData$inputData()
  })

  output$rmcorrDownload <- renderUI({
    req(inputData$name())
    tagList(
      h4("Download the plot"),
      p("Select the image format or download a zip file with all the
               images, the script and data used to generate the plot."),
      selectInput("downloadFormat",
                  label = "Image format",
                  choices = list(
                    "Vectorial" = list(
                      "pdf" = "pdf",
                      "svg" = "svg",
                      "eps" = "eps"
                    ),
                    "Non-vectorial" = list(
                      "tiff" = "tiff",
                      "png" = "png")
                  ),
                  selected = "pdf"),
      downloadButton("downloadPlot",
                     label = "Download Image"),
      br(),
      br(),
      downloadButton('downloadZip',
                     label = 'Download Zip')
    )
  })
  outputOptions(output, "rmcorrDownload", suspendWhenHidden = FALSE)

  # Download button
  output$downloadPlot <- downloadHandler(
    filename = function() {
      paste(paste('rmcorrPlot-',inputData$name(), sep = ""),
            input$downloadFormat, sep = ".")
    },
    content = function(file) {
      if(input$downloadFormat == 'tiff') {
        ggsave(file,
               plot = plotFigure(),
               device = input$downloadFormat,
               # Width and height are in inches. We increase the dpi to 300, so we
               # have to divide by 72 (original default pixels per inch)
               width = input$width / 72,
               height = input$height / 72,
               compression = "lzw",
               units = "in",
               dpi = 300)
      } else {
        ggsave(file,
               plot = plotFigure(),
               device = input$downloadFormat,
               width = input$width / 72,
               height = input$height / 72,
               units = "in",
               dpi = 300)
      }
    }
  )
  outputOptions(output, "downloadPlot", suspendWhenHidden = FALSE)


  # Download zip file with script, data, and plots.
  output$downloadZip <- downloadHandler(
    filename = function() {
      paste0("rmcorrPlot-", inputData$name(), ".zip")
    },
    content = function(fname) {
      fileList <- c()
      tmpdir <- tempdir()
      # Copy inputData to tmpDir
      file.copy(from = c(inputData$datapath()),
                to = tmpdir)

      # Move to the tmpDir to work with the tmpFiles
      setwd(tmpdir)

      # Change the name of the uploaded file so that the code still works.
      tmpInputFile <- basename(inputData$datapath())
      file.rename(from = tmpInputFile,
                  to = inputData$name())

      # Code
      write(scriptCode(), "rmcorrPlot.R")

      fileList <- c(fileList, inputData$name(), "rmcorrPlot.R")

      # Create all images (except tiff that is compressed).
      for (format in c('pdf','svg','eps','png')) {
        file <- paste(paste0('rmcorrPlot-',inputData$name()),
                      format, sep = ".")
        ggsave(file,
               plot = plotFigure(),
               device = format,
               width = input$width / 72,
               height = input$height / 72,
               units = "in",
               dpi = 300)
        fileList <- c(fileList, file)
      }

      # Add compressed .tiff
      tiffFile <- paste(paste0('rmcorrPlot-',inputData$name()),
                        'tiff', sep = ".")
      ggsave(tiffFile,
             plot = plotFigure(),
             device = 'tiff',
             compression = "lzw",
             width = input$width / 72,
             height = input$height / 72,
             units = "in",
             dpi = 300)
      fileList <- c(fileList, tiffFile)

      # And create the zip
      zip(zipfile=fname, files=fileList)
    },
    contentType = "application/zip"
  )
  outputOptions(output, "downloadZip", suspendWhenHidden = FALSE)
}
shinyApp(ui = ui, server = server)
lmarusich/rmcorrShiny documentation built on Aug. 10, 2024, 4:51 a.m.