Search code examples
shinyshinyjs

Dynamic border colors


I want to have a set of dynamic textboxes that have border colors that match the color that's described by it. If the text of the textbox is "red", I want a red border. Here is how I am getting that behavior in a reprex that uses much of my original code:

library(shinyjs) # useShinyjs
library(ggplot2)
library(RColorBrewer)
library(shiny)

ui <- fluidPage(
  titlePanel("Reprex"),
  sidebarLayout(
    sidebarPanel(
      useShinyjs(),
      fluidRow(column(9, fileInput('manifest', 'Choose File',
                                    accept = c('text/csv', 'text/comma-separated-values,text/plain', '.csv'))),
               column(3, actionButton("load_button", "Load", width = "100%"))),
      fluidRow(column(5, selectInput(inputId = "group_palette_input",
                                     label = "Palette Selector",
                                     choices = NA)),
               column(5, selectInput(inputId = "column_input",
                                     label = "Column Selector",
                                     choices = NA))),
      uiOutput("group_colors"),
      width=4),
    mainPanel(
      tabsetPanel(id = "tabs",
                  tabPanel("Plot")
      )
    )
  )
)

server <- function(input, output, session) {
  
  # Load the demo data on initialization and press the load button to update the file
  data <- reactiveValues()
  observeEvent(eventExpr = input$load_button, ignoreInit = FALSE, ignoreNULL = FALSE, {
    manifestName = ifelse(is.null(input$manifest$datapath), "file.txt", input$manifest$datapath)
    man = read.table(manifestName, sep = "\t", header = T, check.names=F, strip.white = T)
    data$manifest <- man[man$include, ]
  })
  
  # Update column selector
  observeEvent(data$manifest, { 
    freezeReactiveValue(input, "column_input")
    updateSelectInput(inputId = "column_input",
                      choices = names(data$manifest),
                      selected = "group") # All files should have a group column
  })
  
  # Update palette selector
  observeEvent(data$manifest, {
    freezeReactiveValue(input, "group_palette_input")
    updateSelectInput(inputId = "group_palette_input",
                      choices = rownames(brewer.pal.info),
                      selected = "Dark2")
  })
  
  groupIncludeManualPaletteInput <- eventReactive(input$column_input, {
    
    fullColors = brewer.pal(length(groups()), input$group_palette_input)
    lapply(1:length(groups()),
           function(groupIndex) {
             colorId = paste0(groups()[groupIndex], "_color")
             fluidRow(column(5, textInput(inputId = colorId,
                                          label = NULL,
                                          value = fullColors[groupIndex])),
                      column(1, checkboxInput(inputId = as.character(groups()[groupIndex]), # Numeric column causes issues, need to wrap with as.character
                                              label = groups()[groupIndex],
                                              value = TRUE),
                             style='padding:0px;')) # Removing padding puts the two columns closer together
           }) # End lapply
  })

  groups <- reactive(sort(unique(data$manifest[[input$column_input]])))
  
  # Update groupIncludeManualPaletteInput
  observeEvent(input$group_palette_input, {
    groupColorIds = paste0(groups(), "_color")
    fullColors = brewer.pal(length(groups()), input$group_palette_input)
    for (groupColorIndex in seq_along(groupColorIds)) {
      updateTextInput(session, groupColorIds[groupColorIndex], value = fullColors[groupColorIndex])
    }
  })
  
  # Vector of booleans to mask the included groups
  includedGroups <- reactive(unlist(map(as.character(groups()), ~input[[.x]]))) # unlist() allows includedGroups to be used as an index variable
  
  # Vector of characters of group names that are included
  currentGroups <- reactive(groups()[includedGroups()])
  
  # Vector of characters of color names that are included
  currentColors <- reactive(unlist(map(groups(), ~input[[paste0(.x, "_color")]])[includedGroups()]))
  
  output$group_colors <- renderUI(groupIncludeManualPaletteInput())
  
  # Make the borders of these textboxes match the color they describe
  # This is run twice when it works, but only once when it doesn't as a simple observe
  # As an observeEvent on groups(), only activates once and temporarily shows the border color; Adding in the req(input[[colorId]]) doesn't help
  # Same thing when the observation is on the groupIncludeManualPaletteInput()
  # Doubling the js code doesn't work
  # In the html, I can see that the border color is not being set
  # observing currentColors() is a dud too
  # Adding an if statement in the javascript doesn't change behavior.
  observe({
    lapply(seq_along(groups()),
           function(groupIndex) {
             colorId = paste0(groups()[groupIndex], "_color")
             cat("Made it into Loop,", input[[colorId]], '\n')
             cat(colorId, ': ', input[[colorId]], '\n\n')
             runjs(paste0("document.getElementById('", colorId, "').style.borderColor ='", input[[colorId]] ,"'"))
             runjs(paste0("document.getElementById('", colorId, "').style.borderWidth = 'thick'"))
           })
  })
}

shinyApp(ui, server)

Here's what file.txt can look like, tab delimited:

group   disease celltype    include
1   HC  B   TRUE
2   SLE M   TRUE
2   HC  C   TRUE

Loading up, it all looks correct:

Loading Up

And then when I change what column I'm looking at, it does what I expect it to do, each textbox has a border with color described:

New Column

But if we go back to a column already chosen:

Disaster!

Then the border color is gone!

Looking at the react_log, I think it has something to do with re-initializing input$1_color and input$2_color in the groupIncludeManualPaletteInput() eventReactive. I also notice that when succesful, the runjs() section is run twice, but when unsuccessful, it only runs once.

As additional attempts to solve this issue, I thought that maybe the data types were the issue. I tried various combinations of using groupIncludeManualPaletteInput as a reactiveValue, but none seemed to mesh with renderUI in the first place.


Solution

  • You are using the same input ID in colorId when you select the same group the second time. However, you need unique input IDs in shiny. I have added a counter to change it. Try this.

    library(shinyjs) # useShinyjs
    library(ggplot2)
    library(RColorBrewer)
    library(shiny)
    
    ui <- fluidPage(
      titlePanel("Reprex"),
      sidebarLayout(
        sidebarPanel(
          useShinyjs(),
          fluidRow(column(9, fileInput('manifest', 'Choose File',
                                       accept = c('text/csv', 'text/comma-separated-values,text/plain', '.csv'))),
                   column(3, actionButton("load_button", "Load", width = "100%"))),
          fluidRow(column(5, selectInput(inputId = "group_palette_input",
                                         label = "Palette Selector",
                                         choices = NULL)),
                   column(5, selectInput(inputId = "column_input",
                                         label = "Column Selector",
                                         choices = rownames(brewer.pal.info)))),
          uiOutput("group_colors"),
          width=4),
        mainPanel(
          tabsetPanel(id = "tabs",
                      tabPanel("Plot")
          )
        )
      )
    )
    
    server <- function(input, output, session) {
      cntr <- reactiveValues(val=0)
      # Load the demo data on initialization and press the load button to update the file
      # data <- reactiveValues()
      # observeEvent(eventExpr = input$load_button, ignoreInit = FALSE, ignoreNULL = FALSE, {
      #   manifestName = ifelse(is.null(input$manifest$datapath), "file.csv", input$manifest$datapath)
      #   man = read.table(manifestName, sep = "\t", header = T, check.names=F, strip.white = T)
      #   data$manifest <- man[man$include, ]
      # })
      
      datamanifest <- reactive({
        req(input$manifest)
        read.csv(input$manifest$datapath, header = TRUE)
      })
      
      # Update column selector
      observeEvent(datamanifest(), { 
        freezeReactiveValue(input, "column_input")
        updateSelectInput(inputId = "column_input",
                          choices = names(datamanifest()),
                          selected = "group") # All files should have a group column
      })
      
      # Update palette selector
      observeEvent(datamanifest(), {
        freezeReactiveValue(input, "group_palette_input")
        updateSelectInput(inputId = "group_palette_input",
                          choices = rownames(brewer.pal.info),
                          selected = "Dark2")
      })
      
      groupIncludeManualPaletteInput <- eventReactive(groups(), {
        req(input$group_palette_input)
        cntr$val <- cntr$val + 1
        fullColors = brewer.pal(length(groups()), input$group_palette_input)
        lapply(1:length(groups()),
               function(groupIndex) {
                 colorId = paste0(groups()[groupIndex], "_color", cntr$val)
                 fluidRow(column(5, textInput(inputId = colorId,
                                              label = NULL,
                                              value = fullColors[groupIndex])),
                          column(1, checkboxInput(inputId = as.character(groups()[groupIndex]), # Numeric column causes issues, need to wrap with as.character
                                                  label = groups()[groupIndex],
                                                  value = TRUE),
                                 style='padding:0px;')) # Removing padding puts the two columns closer together
               }) # End lapply
      })
      
      groups <- eventReactive(input$column_input, {sort(unique(datamanifest()[[input$column_input]]))})
      
      # Update groupIncludeManualPaletteInput
      observeEvent(input$group_palette_input, {
        groupColorIds = paste0(groups(), "_color",cntr$val)
        fullColors = brewer.pal(length(groups()), input$group_palette_input)
        for (groupColorIndex in seq_along(groupColorIds)) {
          updateTextInput(session, groupColorIds[groupColorIndex], value = fullColors[groupColorIndex])
        }
      })
      
      # Vector of booleans to mask the included groups
      includedGroups <- eventReactive(groups(), {unlist(map(as.character(groups()), ~input[[.x]]))}) # unlist() allows includedGroups to be used as an index variable
      
      # Vector of characters of group names that are included
      currentGroups <- eventReactive(includedGroups(), {groups()[includedGroups()]})
      
      # Vector of characters of color names that are included
      currentColors <- reactive(unlist(map(groups(), ~input[[paste0(.x, "_color")]])[includedGroups()]))
      
      output$group_colors <- renderUI(groupIncludeManualPaletteInput())
      
      # Make the borders of these textboxes match the color they describe
      # This is run twice when it works, but only once when it doesn't as a simple observe
      # As an observeEvent on groups(), only activates once and temporarily shows the border color; Adding in the req(input[[colorId]]) doesn't help
      # Same thing when the observation is on the groupIncludeManualPaletteInput()
      # Doubling the js code doesn't work
      # In the html, I can see that the border color is not being set
      # observing currentColors() is a dud too
      # Adding an if statement in the javascript doesn't change behavior.
      observe({
        lapply(seq_along(groups()),
               function(groupIndex) {
                 colorId = paste0(groups()[groupIndex], "_color",cntr$val)
                 cat("Made it into Loop,", input[[colorId]], '\n')
                 cat(colorId, ': ', input[[colorId]], '\n\n')
                 runjs(paste0("document.getElementById('", colorId, "').style.borderColor ='", input[[colorId]] ,"'"))
                 runjs(paste0("document.getElementById('", colorId, "').style.borderWidth = 'thick'"))
               })
      })
    }
    
    shinyApp(ui, server)