Search code examples
rmatrixshinysapply

How to remove a column that has no values other than the column header?


I ran the below code and below I provide screenshots below of what happens:

  1. The first image shows the user successfully adding 2 additional interpolation scenarios; all OK.
  2. The second image shows what happens when a user deletes the Scenario 2 that was input above, by clicking the [x] in the Scenario 2 input matrix column header. See the error message that pops up for the plot.
  3. The third image shows the results of the user clicking on the [x] for the empty "Scenario 3" in the preceding image; the input matrix and plot re-render correctly. All OK again.

I'm trying to automate the elimination of the empty columns so the user doesn't need to go through the additional deletion step noted in #3 above.

The code I'm using in attempt to do this is:

# Remove any empty matrix columns
      empty_columns <- sapply(tmpMatrix, function(x) all(is.na(x) | x == ""))
      tmpMatrix[, !empty_columns]

But the problem is, it is looking for complete empty columns to remove. I need to modify it so it looks for empty columns ignoring any column header. The second image below shows this type of column I'm targeting: column header "Scenario 3", but all cells beneath it are empty.

Does anyone know how to eliminate empty columns ignoring the header?

Note that if I comment out the empty_columns <- section I highlight above, this problem goes away, but a new problem arises when deleting Scenario 2: Scenario 3 becomes the second scenario as it should, but its column header remains "Scenario 3" when it should become "Scenario 2"!

Code:

library(shiny)
library(shinyMatrix)
library(dplyr)
library(ggplot2)

interpol <- function(a, b) { # a = periods, b = matrix inputs
  c <- rep(NA, a)
  c[1] <- b[1]
  c[a] <- b[2]
  c <- approx(seq_along(c)[!is.na(c)], c[!is.na(c)], seq_along(c))$y # << interpolates
  return(c)
}

ui <- fluidPage(
  sliderInput('periods','Periods to interpolate:',min=2,max=10,value=10),
  matrixInput(
    "myMatrixInput",
    label = "Values to interpolate paired under each scenario heading:",
    value =  matrix(c(1, 5), 1, 2, dimnames = list(NULL, c("Scenario 1", "NULL"))),
    cols = list(extend = TRUE,  delta = 2, names = TRUE,  delete = TRUE,  multiheader = TRUE),
    rows = list(extend = FALSE, delta = 1, names = FALSE, delete = FALSE),
    class = "numeric"),
  plotOutput("plot")
)

server <- function(input, output, session) {
  
  sanitizedMat <- reactiveVal()
  
  observeEvent(input$myMatrixInput, {
    if(any(colnames(input$myMatrixInput) == "")){
      tmpMatrix <- input$myMatrixInput
      
      # Assign column header names
      colnames(tmpMatrix) <- paste("Scenario", trunc(1:ncol(input$myMatrixInput)/2+1))
      
      # Remove any empty matrix columns
      empty_columns <- sapply(tmpMatrix, function(x) all(is.na(x) | x == ""))
      tmpMatrix[, !empty_columns]
      
      updateMatrixInput(session, inputId = "myMatrixInput", value = tmpMatrix)
    }
    else {sanitizedMat(na.omit(input$myMatrixInput))}
  })
  
  plotData <- reactive({
    lapply(seq_len(ncol(sanitizedMat())/2),
           function(i){
             tibble(
               Scenario = colnames(sanitizedMat())[i*2-1],
               X = 1:input$periods,
               Y = interpol(input$periods, sanitizedMat()[1,(i*2-1):(i*2)])
             )
           }) %>% bind_rows()
  })
  
  output$plot <- renderPlot({
    plotData() %>% ggplot() + geom_line(aes(
      x = X,
      y = Y,
      colour = as.factor(Scenario)
    ))
  })

}

shinyApp(ui, server)

enter image description here

enter image description here

enter image description here


Solution

  • Link to this post How to automatically delete a matrix column in R if there otherwise would be subscript out of bounds error? where Jan completely solved it. Below is the code that completely solves this:

    interpol <- function(a, b) { # a = periods, b = matrix inputs
      c <- rep(NA, a)
      c[1] <- b[1]
      c[a] <- b[2]
      c <- approx(seq_along(c)[!is.na(c)], c[!is.na(c)], seq_along(c))$y # << interpolates
      return(c)
    }
    
    ui <- fluidPage(
      sliderInput('periods', 'Periods to interpolate:', min=2, max=10, value=10),
      matrixInput(
        "myMatrixInput",
        label = "Values to interpolate paired under each scenario heading:",
        value =  matrix(c(1, 5), 1, 2, dimnames = list(NULL, rep("Scenario 1", 2))),
        cols = list(extend = TRUE,  delta = 2, names = TRUE,  delete = TRUE, multiheader = TRUE),
        rows = list(extend = FALSE, delta = 1, names = FALSE, delete = FALSE),
        class = "numeric"),
      plotOutput("plot")
    )
    
    server <- function(input, output, session) {
      
      observeEvent(input$myMatrixInput, {
        tmpMatrix <- input$myMatrixInput
        
        # Remove any empty matrix columns
        empty_columns <- sapply(tmpMatrix, function(x) all(is.na(x) | x == ""))
        tmpMatrix <- tmpMatrix[, !empty_columns, drop=FALSE]
        
        # Assign column header names
        colnames(tmpMatrix) <- paste("Scenario", rep(1:ncol(tmpMatrix), each = 2, length.out = ncol(tmpMatrix)))
        
        isolate( # isolate update to prevent infinite loop
          updateMatrixInput(session, inputId = "myMatrixInput", value = tmpMatrix)
        )
      })
      
      plotData <- reactive({
        tryCatch(
          lapply(seq_len(ncol(input$myMatrixInput)/2),
                 function(i){
                   tibble(
                     Scenario = colnames(input$myMatrixInput)[i*2-1],
                     X = seq_len(input$periods),
                     Y = interpol(input$periods, input$myMatrixInput[1,(i*2-1):(i*2)])
                   )
                 }) %>% bind_rows(),
          error = function(e) NULL
        )
      })
      
      output$plot <- renderPlot({
        req(plotData())
        
        plotData() %>% ggplot() + geom_line(aes(
          x = X,
          y = Y,
          colour = as.factor(Scenario)
        ))
      })
      
    }
    
    shinyApp(ui, server)