Search code examples
rshinyclipboard

How do I copy a complete data frame object to clipboard?


See solution option at the bottom!

I'm trying to create an actionButton() (ID is transCopy in below code) to copy/paste rendered tables from Shiny to Excel. In the below code, it works for an incomplete extract of the transition table (object results()), but not for the completed object per output$resultsDT which shows the transitions (like results()) PLUS the periods transiting from/to along the top rows of the table rendered in Shiny.

I tried pulling datatable(...) out of output$resultsDT and creating a new reactive object with it, feeding it into both output$resultsDT and the clipboard copy function write.table(x = ...) inside the single observeEvent() below, but got "Error in <-: object of type 'closure' is not subsettable". I tried other things but with no luck yet.

So how would I change this so the user can copy/paste a more complete version of the table to Excel? The format doesn't need to be exactly the same (though it would be nice if it were), even 2 rows along the top of the pasted table specifying "From = x" and "To = y" would be helpful so the user can later see the inputs that were used in deriving the table post-paste into Excel.

The images at the bottom better explain.

Finally, I like sticking with base R (such as write.table()) if possible because otherwise, in the fuller code this is intended for, I'm suffering from the effects of package-bloat.

Reproducible code:

library(DT)
library(shiny)
library(dplyr)
library(htmltools)
library(data.table)

data <- 
  data.frame(
    ID = c(1,1,1,2,2,2,3,3,3),
    Period = c(1, 2, 3, 1, 2, 3, 1, 2, 3),
    Values = c(5, 10, 15, 0, 2, 4, 3, 6, 9),
    State = c("X0","X1","X2","X0","X2","X0", "X2","X1","X9")
  )

numTransit <- function(x, from=1, to=3){
  setDT(x)
  unique_state <- unique(x$State)
  all_states <- setDT(expand.grid(list(from_state = unique_state, to_state = unique_state)))
  dcast(x[, .(from_state = State[from], 
              to_state = State[to]), 
          by = ID]
        [,.N, c("from_state", "to_state")]
        [all_states,on = c("from_state", "to_state")], 
        to_state ~ from_state, value.var = "N"
  )
}

ui <- fluidPage(
  tags$head(tags$style(".datatables .display {margin-left: 0;}")), 
  h4(strong("Transition table inputs:")),
  numericInput("transFrom", "From period:", 1, min = 1, max = 3),
  numericInput("transTo", "To period:", 2, min = 1, max = 3),
  actionButton(inputId = "transCopy", "Copy", width = "20%"),
  h4(strong("Output transition table:")), 
  DTOutput("resultsDT"),
)

server <- function(input, output, session) {
  results <- 
    reactive({
      results <- numTransit(data, input$transFrom, input$transTo) %>% 
        replace(is.na(.), 0) %>%
        bind_rows(summarise_all(., ~(if(is.numeric(.)) sum(.) else "Sum")))
      results <- cbind(results, Sum = rowSums(results[,-1]))
    })
 
  output$data <- renderTable(data)
  
  output$resultsDT <- renderDT(server=FALSE, {
    datatable(
      data = results(),
      rownames = FALSE,
      filter = 'none',
      container = tags$table(
        class = 'display',
        tags$thead(
          tags$tr(
            tags$th(rowspan = 2, 
                    sprintf('To state where end period = %s', input$transTo), 
                    style = "border-right: solid 1px;"),
            tags$th(colspan = 10, 
                    sprintf('From state where initial period = %s', input$transFrom)
                    )
          ),
          tags$tr(
            mapply(tags$th, colnames(results())[-1], style = sprintf("border-right: solid %spx;", rep(0, ncol(results()) - 1L)), SIMPLIFY = FALSE)
          )
        )
      ),
      options = list(scrollX = F
                     , dom = 'ft'
                     , lengthChange = T
                     , pagingType = "numbers"
                     , autoWidth = T
                     , info = FALSE
                     , searching = FALSE
      ),
      class = "display"
    ) %>%
      formatStyle(c(1), `border-right` = "solid 1px")
  })
  
  observeEvent(input$transCopy,
               write.table(x = results(),
                           file = "clipboard",
                           sep = "\t",
                           row.names = FALSE,
                           col.names = TRUE
               ))
  
}

shinyApp(ui, server)

UI when first invoking App:

enter image description here

Now paste from clipboard to Excel:

enter image description here

A simpler solution to the above OP:

Replace the observeEvent() in the OP code with the below, based on advice from r2Evans in How to add a row of text above the output table when using write.table to copy and paste a data frame?:

observeEvent(
    req(input$transFrom, input$transTo),
    writeLines(
      c(sprintf('Column headers show transition `from-state` where initial period = %s', input$transFrom),
        sprintf('Row headers show transition `to-state` where end period = %s', input$transTo),
        capture.output(
          write.table(x = results(),
                      sep = "\t",
                      row.names = FALSE,
                      col.names = TRUE)
        )
      ),
      "clipboard"
    )
  )

This alternative observeEvent() outputs two descriptive lines above the transition table, so the user sees the transition inputs when copying/pasting the table. It's so easy to format in Excel that there's no point in pasting a perfectly formatted table from R (and how complicated that would be). Below is example output when pasting into Excel:

enter image description here


Solution

  • Although I haven't tried it myself, clipr seems to do what you want.

    library(shiny)
    library(clipr)
    library(rhandsontable)
    
    ui <- fluidPage(
      actionButton(inputId = 'click',label = 'COPY'),
      p('Click COPY and paste the results below witch Ctrl+V.'),
      rHandsontableOutput('rhot')
    )
    
    server <- function(input, output, session) {
      
      
      output$rhot = renderRHandsontable({
        df = data.frame(lapply(1:10, function(x){rep('',10)}))
        colnames(df) = paste('c',1:10)
        rhandsontable(df)
      })
      
      observeEvent(input$click,{
        clipr::write_clip(mtcars)
      })
      
    }
    
    shinyApp(ui, server)