I'm running two expandable rhandsontables who should always have the same number of columns and the same column headers, though the rows differ. One of the tables (myDF1 rendered in "hottable1") is the master where the user adds/deletes columns from the tabPanel() housing that table and the second table (myDF2 rendered in "hottable2") parrots the first table in terms of number of columns and column headers but is placed in a separate tabPanel() reacting to the action buttons in the first tabPanel(). The strange thing is, this linked column addition/deletion works fine when the two tables are rendered in Shiny's fluidPage() or when using Shiny's pageWithSidebar() the two tables are housed in the same tabPanel(). However, when the two tables are in separate tabPanels() (as shown in the code below), column addition works fine but the second table in tab "Slave" crashes when deleting columns from tab "Master".
I must be missing something very basic about tabPanels(). What am I doing wrong?
I've always assumed reactivity cuts across tabPanels().
Code:
library(dplyr)
library(rhandsontable)
library(shiny)
myDF1 <- data.frame('Series 1' = c(1,24,0), check.names = FALSE)
rownames(myDF1) <- c('Term A','Term B','Term C')
myDF2 <- data.frame('Series 1' = c(20,15), check.names = FALSE)
rownames(myDF2) <- c('Boy','Girl')
ui <- pageWithSidebar(
headerPanel(""),sidebarPanel(""),
mainPanel(
tabsetPanel(
tabPanel("Master table", hr(),
rHandsontableOutput('hottable1'),br(),
actionButton("addSeries", "Add", width = 80),
fluidRow(
column(2,actionButton("delSeries","Delete", width = 80)),
column(3,uiOutput("delSeries2"))
),
),
tabPanel("Slave table", hr(),rHandsontableOutput('hottable2'))
)
)
)
server <- function(input, output) {
emptyTbl1 <- reactiveVal(myDF1)
emptyTbl2 <- reactiveVal(myDF2)
observeEvent(input$hottable1, {emptyTbl1(hot_to_r(input$hottable1))})
observeEvent(input$hottable2, {emptyTbl2(hot_to_r(input$hottable2))})
output$hottable1 <- renderRHandsontable({
rhandsontable(emptyTbl1(),rowHeaderWidth = 100, useTypes = TRUE)%>%
hot_context_menu(allowRowEdit = FALSE, allowColEdit = FALSE)
})
output$hottable2 <- renderRHandsontable({
rhandsontable(emptyTbl2(),rowHeaderWidth = 100, useTypes = TRUE)%>%
hot_context_menu(allowRowEdit = FALSE, allowColEdit = FALSE)
})
observeEvent(input$addSeries, {
newCol1 <- data.frame(c(1,24,0))
newCol2 <- data.frame(c(20,15))
names(newCol1) <- paste("Series", ncol(hot_to_r(input$hottable1)) + 1)
names(newCol2) <- paste("Series", ncol(hot_to_r(input$hottable2)) + 1)
emptyTbl1(cbind(emptyTbl1(), newCol1))
emptyTbl2(cbind(emptyTbl2(), newCol2))
})
observeEvent(input$delSeries3, {
tmp1 <- emptyTbl1()
tmp2 <- emptyTbl2()
if(ncol(tmp1) > 1){
delCol <- input$delSeries3
tmp1 <- tmp1[ , !(names(tmp1) %in% delCol), drop = FALSE]
tmp2 <- tmp2[ , !(names(tmp2) %in% delCol), drop = FALSE]
newNames <- sprintf("Series %d",seq(1:ncol(tmp1)))
names(tmp1) <- newNames
names(tmp2) <- newNames
emptyTbl1(tmp1)
emptyTbl2(tmp2)
}
})
output$delSeries2 <-
renderUI(
selectInput("delSeries3",
label = NULL,
choices = colnames(hot_to_r(input$hottable1)),
selected = "", width = '100px',
multiple = TRUE)
)
}
shinyApp(ui,server)
The below "resolved code" resolves the issue. The few changes from OP code are commented below and are summarized as follows:
Insert outputOptions(output, 'hottable2', suspendWhenHidden = FALSE) in the server() section in order to update the 2nd table located in a separate tab panel from the action buttons driving that table from another tab panel; allows reactivity to instantly cross tab panels that aren't being viewed
Even with the above fix, the "hottable2" table had to be clicked on in order to completely render it. R whiz Stéphane Laurent pointed out that there's a known bug in Shiny when re-rendering this way, his html solution is accordingly included and commented in the revised code below for the rhandsontable() function used for "hottable2" in the server() section
Resolved code:
library(dplyr)
library(rhandsontable)
library(shiny)
myDF1 <- data.frame('Series 1' = c(1,24,0), check.names = FALSE)
rownames(myDF1) <- c('Term A','Term B','Term C')
myDF2 <- data.frame('Series 1' = c(20,15), check.names = FALSE)
rownames(myDF2) <- c('Boy','Girl')
ui <- pageWithSidebar(
headerPanel(""),sidebarPanel(""),
mainPanel(
tabsetPanel(
tabPanel("Master table", hr(),
rHandsontableOutput('hottable1'),br(),
actionButton("addSeries", "Add", width = 80),
fluidRow(
column(2,actionButton("delSeries","Delete", width = 80)),
column(3,uiOutput("delSeries2"))
),
),
tabPanel("Slave table", hr(),rHandsontableOutput('hottable2'))
)
)
)
server <- function(input, output) {
emptyTbl1 <- reactiveVal(myDF1)
emptyTbl2 <- reactiveVal(myDF2)
observeEvent(input$hottable1, {emptyTbl1(hot_to_r(input$hottable1))})
observeEvent(input$hottable2, {emptyTbl2(hot_to_r(input$hottable2))})
output$hottable1 <- renderRHandsontable({
rhandsontable(emptyTbl1(),rowHeaderWidth = 100, useTypes = TRUE)%>%
hot_context_menu(allowRowEdit = FALSE, allowColEdit = FALSE) %>%
hot_cols(colWidths = 80)
})
output$hottable2 <- renderRHandsontable({
rhandsontable(emptyTbl2(),rowHeaderWidth = 100, width = 800, height = 450,useTypes = TRUE)%>%
hot_context_menu(allowRowEdit = FALSE, allowColEdit = FALSE) %>%
hot_cols(colWidths = 80) %>%
# next section of html addresses issue of correcltly rendering the slave table:
htmlwidgets::onRender(
"function(el, x){
var hot = this.hot;
$('a[data-value=\"Slave table\"').on('click', function(){
setTimeout(function(){ hot.render(); }, 200);
});
}"
)
})
observeEvent(input$addSeries, {
newCol1 <- data.frame(c(1,24,0))
newCol2 <- data.frame(c(20,15))
names(newCol1) <- paste("Series", ncol(hot_to_r(input$hottable1)) + 1)
names(newCol2) <- paste("Series", ncol(hot_to_r(input$hottable2)) + 1)
emptyTbl1(cbind(emptyTbl1(), newCol1))
emptyTbl2(cbind(emptyTbl2(), newCol2))
})
observeEvent(input$delSeries3, {
tmp1 <- emptyTbl1()
tmp2 <- emptyTbl2()
if(ncol(tmp1) > 1){
delCol <- input$delSeries3
tmp1 <- tmp1[ , !(names(tmp1) %in% delCol), drop = FALSE]
tmp2 <- tmp2[ , !(names(tmp2) %in% delCol), drop = FALSE]
newNames <- sprintf("Series %d",seq(1:ncol(tmp1)))
names(tmp1) <- newNames
names(tmp2) <- newNames
emptyTbl1(tmp1)
emptyTbl2(tmp2)
}
})
output$delSeries2 <-
renderUI(
selectInput("delSeries3",
label = NULL,
choices = colnames(hot_to_r(input$hottable1)),
selected = "", width = '100px',
multiple = TRUE)
)
outputOptions(output, 'hottable2', suspendWhenHidden = FALSE) # this updates slave panel even when hidden
}
shinyApp(ui,server)
Related
In the below code, I took one of the examples from https://community.rstudio.com/t/shiny-app-with-dynamic-number-of-datatables/2405/4 for dynamically adding tables. The example used tables rendered with DT and I made minor modifications to use it for rhandsontable.
However, I'm having trouble making the tables independent of one another. When adding a new table, it should be "seeded" with the default values per dataframe data1 and its related rowNames1, but thereafter they should be independent as illustrated below. I'm fairly sure the solution has something to do with creating a dynamic equivalent of the reactive uiTable1 used in the code for holding table values, but I don't know how to do this.
Any ideas for accomplishing this?
Inputs into the base (master) and added tables will be used elsewhere in the full code this is intended for.
Code:
library(rhandsontable)
library(shiny)
rowNames1 <- c('A','B','C','Sum')
data1 <- data.frame(row.names = rowNames1, 'Col 1' = c(1,1,0,2), check.names = FALSE)
ui <- fluidPage(
rHandsontableOutput('hottable1'), # undeletable base table
actionButton("addTbl", "Add table"), # adds new table
tags$div(id = "placeholder")
)
server <- function(input, output) {
uiTable1 <- reactiveVal(data1) # undeletable base table
rv <- reactiveValues() # used for dynamic table add/removal
# records changes to base table and will need same for added tables:
observeEvent(input$hottable1,{uiTable1(hot_to_r(input$hottable1))})
output$hottable1 <- renderRHandsontable({
rhandsontable(uiTable1(),rowHeaderWidth = 100, useTypes = TRUE)
})
# adds column summation to last row of table, will need for all added tables too:
observe({
req(input$hottable1)
DF <- hot_to_r(input$hottable1)
DF[setdiff(rowNames1, "Sum"),]
DF["Sum",] <- colSums(DF[setdiff(rowNames1, "Sum"),, drop = FALSE], na.rm = TRUE)
uiTable1(DF)
})
# dynamically add/remove tables:
observeEvent(input$addTbl, {
divID <- gsub("\\.", "", format(Sys.time(), "%H%M%OS3"))
dtID <- paste0(divID, "DT")
btnID <- paste0(divID, "rmv")
insertUI(
selector = "#placeholder",
ui = tags$div(id = divID,
actionButton(btnID, "Remove table", class = "pull-left btn btn-danger"),
rHandsontableOutput(dtID),
hr()
)
)
output[[dtID]] <- renderRHandsontable({
rhandsontable(uiTable1(),rowHeaderWidth = 100, useTypes = TRUE)
})
# remove table from the app when remove button clicked
observeEvent(input[[btnID]], {
removeUI(selector = paste0("#", divID))
rv[[divID]] <- NULL
}, ignoreInit = TRUE, once = TRUE)
})
}
shinyApp(ui,server)
We can use reactiveValues to store the information of each new table. These tables will start with the values of the base table available at that time. Afterwards they will stop reacting to changes inside the main table.
First we create the base table uiTable1 <- reactiveValues(table_base = data1) # undeletable base table
And finally all the subsequent tables will be created as uiTable1[[glue("{divID}table")]] <- uiTable$table_base
library(rhandsontable)
library(shiny)
library(glue)
rowNames1 <- c("A", "B", "C", "Sum")
data1 <- data.frame(row.names = rowNames1, "Col 1" = c(1, 1, 0, 2), check.names = FALSE)
ui <- fluidPage(
rHandsontableOutput("hottable1"), # undeletable base table
actionButton("addTbl", "Add table"), # adds new table
tags$div(id = "placeholder")
)
server <- function(input, output) {
uiTable1 <- reactiveValues(table_base = data1) # undeletable base table
rv <- reactiveValues() # used for dynamic table add/removal
# records changes to base table and will need same for added tables:
observeEvent(input$hottable1, {
uiTable1$table_base <- hot_to_r(input$hottable1)
})
output$hottable1 <- renderRHandsontable({
rhandsontable(uiTable1$table_base, rowHeaderWidth = 100, useTypes = TRUE)
})
# adds column summation to last row of table, will need for all added tables too:
observe({
req(input$hottable1)
DF <- hot_to_r(input$hottable1)
DF[setdiff(rowNames1, "Sum"), ]
DF["Sum", ] <- colSums(DF[setdiff(rowNames1, "Sum"), , drop = FALSE], na.rm = TRUE)
uiTable1$table_base <- DF
})
# dynamically add/remove tables:
observeEvent(input$addTbl, {
divID <- gsub("\\.", "", format(Sys.time(), "%H%M%OS3"))
dtID <- paste0(divID, "DT")
btnID <- paste0(divID, "rmv")
# capture the current state of the main table
uiTable1[[glue("{divID}table")]] <- uiTable1$table_base
insertUI(
selector = "#placeholder",
ui = tags$div(
id = divID,
actionButton(btnID, "Remove table", class = "pull-left btn btn-danger"),
rHandsontableOutput(dtID),
hr()
)
)
output[[dtID]] <- renderRHandsontable({
req(uiTable1[[glue("{divID}table")]])
rhandsontable(uiTable1[[glue("{divID}table")]], rowHeaderWidth = 100, useTypes = TRUE)
})
# adds column summation to last row of table, will need for all added tables too:
observeEvent(input[[dtID]], {
DF <- hot_to_r(input[[dtID]])
DF[setdiff(rowNames1, "Sum"), ]
DF["Sum", ] <- colSums(DF[setdiff(rowNames1, "Sum"), , drop = FALSE], na.rm = TRUE)
uiTable1[[glue("{divID}table")]] <- DF # update the table with the sum
})
# remove table from the app when remove button clicked
observeEvent(input[[btnID]],
{
removeUI(selector = paste0("#", divID))
rv[[divID]] <- NULL
uiTable1[[glue("{divID}table")]] <- NULL
},
ignoreInit = TRUE,
once = TRUE
)
})
}
shinyApp(ui, server)
I have a reactive dataframe (called selected_df()) in which I am trying to consolidate information from input$tableID_cells_selected and another dataframe.
The second reactive dataframe, called storage_df(), is 1 row and 2 columns. It collects the background color and text labels from action buttons that are pressed, and then that data stays static in storage_df() until a different button is pressed.
selected_df() then collects the info about whatever button was last pressed from storage_df() when a cell in the table is selected (or whenever input$plate_cells_selected is updated), and shows these data in the same row.
The problem is that the selected_df() must reference storage_df() inside of the reactive environment, so it updates all of the values in the cond_selected and color_selected columns from selected_df(). I don't want to have those old values from storage_df() updated and replaced with whatever new values exist in storage_df(). I want those old rows of selected_df() to keep those old values, and for the new rows of selected_df() to have the new values of storage_df(). So basically, storage)df() is updated upon button click, as it is reactive, but the references to storage_df() made by selected_df() would not be reactive.
I have a gif here that will hopefully explain what I am trying to do, in case this is confusing. This is a previous attempt but it is the closest that I have gotten to success. In the gif, the color and cond columns of selected_df() are set such as follows for example: cond_selected = isolate(paste0(rep(storage_df()[[1,2]]))), so that value changes when a new button is pressed. The first three rows of column cond_selected within selected_df() should ideally stay gse1, while the latter three rows should be cox8a. As you can see, this is not what happens.
In other words, this is what I have in these columns at the end of this gif:
And this is what I want in these columns:
With my latest attempt (included in my MRE). I thought if I were to only try to update the values of the last added row, as I think the selected_df() might be appended whenever a new cell is selected, then that might work. However, the app crashes and only gives this warning, which it usually gave because selected_df() had no rows before a cell was selected:
Warning: Error in [: subscript out of bounds
Also, here is my MRE and latest attempt, and what I currently need help trying to figure out:
library(shiny)
library(colourpicker)
library(dplyr)
library(DT)
library(glue)
library(shinyWidgets)
####Create the matrix and organization for the 96 well plate####
plate96 <- function(id) {
div(
style = "position: relative; height: 500px",
tags$style(HTML('
.wells {
transform: translateX(50%);
}
.wells table.dataTable tr:nth-child(9) td { /*for the row 9, need to make it not look like a row*/
border-bottom: unset;
}
.wells tbody tr td:not(:first-of-type) {
border: 1px solid black;
height: 15px;
width: 15px;
padding: 15px;
font-size: 0;
}
')),
div(
style = "position: absolute; left: 50%; transform: translateX(-100%);",
div(
class = "wells",
DTOutput(id, width = "90%", height= "100%")
)
)
)
}
####Create the matrix and organization for the 96 well plate####
renderPlate96 = function(id, colors = rep("white", 108)) {
plate <- matrix(1:108,
nrow = 9,
ncol = 12,
byrow = TRUE,
dimnames = list(LETTERS[1:9], 1:12))
colnames (plate) = stringr::str_pad(colnames(plate), 2, "left", "0")
return(plate_return1 <-
datatable(
plate,
options = list(dom = 't', ordering = F),
selection = list(mode = 'multiple',
target = "cell"),
class = 'cell-border compact'
) %>%
formatStyle(
1:12,
cursor = 'pointer',
backgroundColor = styleEqual(1:108, colors, default = NULL)
)
)
}
storage_df <- (data.frame(
matrix(ncol = 2, nrow = 1),
color_selected = NA,
cond_selected = NA
))
# app code
ui <- fluidPage(
plate96("plate"),
tags$b("Wells Selected:"),
verbatimTextOutput("plateWells_selected"),
br(),
helpText("Step 1: Add in a couple of buttons"),
numericInput("num_conds",
label = h3("Enter the number of treatments/ conditions"),
min = 1,
max = 20,
value = 1),
htmlOutput("cond_buttons", align = 'center'),
helpText("Step 2: Type in any name for a condition for the buttons"),
uiOutput("boxes_conds"),
helpText("Step 3: Choose any color for the buttons"),
uiOutput("cond_colors"),
helpText("Step 4: Select cells from the table above"),
DT::dataTableOutput("selected_table"),
DT::dataTableOutput("storage_table"),
)
server <- function(input, output, session){
### ★★ ↓↓↓↓ PROBLEM AREA ↓↓↓↓ ★★ ###
####Storage data.frame for when the buttons are clicked####
observeEvent(input$num_conds, {
lapply(1:input$num_conds, function(x){
observeEvent(input[[paste0("cond_buttons",x)]], {
newdf <- tibble(
color_selected = input[[paste0("colors",x)]],
cond_selected = input[[paste0("condID",x)]]
)
storage_df(newdf)
})
})
})
storage_df <- reactiveVal(tibble::tribble(
~color_selected, ~cond_selected
))
output$storage_table <- renderDataTable(
req(storage_df()),
options = list(paging = FALSE,
ordering = FALSE,
scrollx = FALSE,
searching = FALSE,
stringsAsFactors = FALSE
)
)
####Create a DT that stores the values of the cells selected in the plate####
selected_df <- reactive(data.frame(rows = req(input$plate_cells_selected[,1]),
columns = req(input$plate_cells_selected[,2]),
color_selected = 0,
cond_selected = 0,
stringsAsFactors = FALSE),
)
####Take out this portion of the code if trying to reproduce my GIF###
observeEvent(input$plate_cells_selected, {
selected_df() %>% mutate(selected_df(), color_selected = replace(color_selected, color_selected== '0', isolate(paste0(rep(storage_df()[[1,1]]))))
)})
####Take out this portion of the code if trying to reproduce my GIF###
output$selected_table <- renderDataTable(
req(selected_df()),
options = list(paging = FALSE,
ordering = FALSE,
scrollx = FALSE,
searching = FALSE,
lengthChange = FALSE
)
)
### ★★ ↑↑↑↑ PROBLEM AREA ↑↑↑↑ ★★ ###
#....#
#Past here isn't as important to the question...#
####Input for user browse and data upload####
output$contents <- renderTable({ req(input$data) })
#####Slider for frames per second####
output$value <- renderPrint({ input$Frames })
#####Check boxes for no-movement cell exclusion####
output$value <- renderPrint({ input$emptyWell_checkbox })
#####Number output for number of conditions#####
output$value <- renderPrint({ input$num_conds })
#### Condition boxes for UI text input####
output$boxes_conds <- renderUI({
num_conds = as.integer(input$num_conds)
lapply(1:num_conds, function(i) {
cond_names <- textInput(paste0("condID", i),
label = paste0("Treatment/ Conditions: ", i),
placeholder = "Enter condition..."
)
})
})
#### Color selection for UI input####
output$cond_colors <- renderUI({
num_conds = as.integer(input$num_conds)
lapply(1:num_conds, function(i) {
colourInput(paste0("colors", i),
label = (paste0("Select a color for condition ", i)),
show = c("both"),
value = "black",
palette = c("limited"),
)
})
})
#### Create action buttons for conditions to be selected####
output$cond_buttons <- renderUI({
num_conds = as.integer(input$num_conds)
lapply(1:num_conds, function(i) {
bg = input[[paste0("colors", i)]]
style = paste0(
collapse = " ",
glue("background-color:{bg};
color:#ffffff;
border-color:#000000")
)
label = input[[paste0("condID", i)]]
actionButton(paste0("cond_buttons", i),
label = label,
style = style,
)
})
})
####Create the 96 well plate image####
output$plate <- renderDT({
renderPlate96()
})
output$plateWells_selected <- renderPrint({
input$plate_cells_selected
})
}
shinyApp(ui = ui, server = server)
UPDATE UPON REQUEST
Here is a portion of the code needed to reproduce what you see in the GIF. This is not my latest attempt and is not what I need help with troubleshooting, this is merely to give what I used to make the GIF that explains what I want. Just replace the similar version of the code with this in my MRE and take out the mutate function. The mutate function to remove has been marked by comments in my MRE.
####Create a DT that stores the values of the cells selected in the plate####
selected_df <- reactive(data.frame(rows = req(input$plate_cells_selected[,1]),
columns = req(input$plate_cells_selected[,2]),
color_selected = isolate(paste0(rep(storage_df()[[1,1]]))),
cond_selected = isolate(paste0(rep(storage_df()[[1,2]]))),
stringsAsFactors = FALSE)
)
UPDATE, REPONSE TO YBS
Try this
# app code
ui <- fluidPage(
plate96("plate"),
tags$b("Wells Selected:"),
verbatimTextOutput("plateWells_selected"),
br(),
helpText("Step 1: Add in a couple of buttons"),
numericInput("num_conds",
label = h3("Enter the number of treatments/ conditions"),
min = 1,
max = 20,
value = 1),
htmlOutput("cond_buttons", align = 'center'),
helpText("Step 2: Type in any name for a condition for the buttons"),
uiOutput("boxes_conds"),
helpText("Step 3: Choose any color for the buttons"),
uiOutput("cond_colors"),
helpText("Step 4: Select cells from the table above"),
DTOutput("selected_table"),
DTOutput("storage_table"),
)
server <- function(input, output, session){
### ★★ ↓↓↓↓ PROBLEM AREA ↓↓↓↓ ★★ ###
# storage_df <- reactiveVal(tibble::tribble(
# ~color_selected, ~cond_selected
# ))
storage_df <- reactiveVal(storage)
####Storage data.frame for when the buttons are clicked####
observeEvent(input$num_conds, {
lapply(1:input$num_conds, function(x){
observeEvent(input[[paste0("cond_buttons",x)]], {
newdf <- data.frame(
color_selected = input[[paste0("colors",x)]],
cond_selected = input[[paste0("condID",x)]]
)
storage_df(newdf)
}, ignoreInit = TRUE)
})
})
output$storage_table <- renderDataTable(
req(storage_df()),
options = list(paging = FALSE,
ordering = FALSE,
scrollx = FALSE,
searching = FALSE,
stringsAsFactors = FALSE
)
)
selected <- reactiveValues(df=NULL,cum=NULL)
df1 <- data.frame()
observeEvent(input$plate_cells_selected, {
n = dim(req(input$plate_cells_selected))[1]
df1 <<- data.frame(rows = req(input$plate_cells_selected[,1]), columns = req(input$plate_cells_selected[,2]))
###Create a DT that stores the values of the cells selected in the plate####
selected$cum <- rbind(selected$df,data.frame(rows = input$plate_cells_selected[n,1],
columns = input$plate_cells_selected[n,2],
color_selected = storage_df()[1,1], cond_selected = storage_df()[1,2]))
}, ignoreNULL=FALSE)
observeEvent(selected$cum, {
n1 = dim(df1)[1]
n2 = dim(selected$cum)[1]
if (n1 > n2) { ## add a row
df <- selected$cum
}else df <- left_join(df1, selected$cum, by=c("rows","columns"))
selected$df <- df[!duplicated(df[,1:2]),]
#print(selected$df)
})
####Create a DT that stores the values of the cells selected in the plate####
# selected_df <- reactive(data.frame(rows = req(input$plate_cells_selected[,1]),
# columns = req(input$plate_cells_selected[,2]),
# stringsAsFactors = FALSE) %>% mutate(color_selected = c(0), cond_selected = c(0))
# )
output$selected_table <- renderDT(
#selected_df(),
selected$df,
options = list(paging = FALSE,
ordering = FALSE,
scrollx = FALSE,
searching = FALSE,
lengthChange = FALSE
)
)
### ★★ ↑↑↑↑ PROBLEM AREA ↑↑↑↑ ★★ ###
#....#
#Past here isn't as important to the question...#
####Input for user browse and data upload####
output$contents <- renderTable({ req(input$data) })
#####Slider for frames per second####
output$value <- renderPrint({ input$Frames })
#####Check boxes for no-movement cell exclusion####
output$value <- renderPrint({ input$emptyWell_checkbox })
#####Number output for number of conditions#####
output$value <- renderPrint({ input$num_conds })
#### Condition boxes for UI text input####
output$boxes_conds <- renderUI({
num_conds = as.integer(input$num_conds)
lapply(1:num_conds, function(i) {
cond_names <- textInput(paste0("condID", i),
label = paste0("Treatment/ Conditions: ", i),
placeholder = "Enter condition..."
)
})
})
#### Color selection for UI input####
output$cond_colors <- renderUI({
num_conds = as.integer(input$num_conds)
lapply(1:num_conds, function(i) {
colourInput(paste0("colors", i),
label = (paste0("Select a color for condition ", i)),
show = c("both"),
value = "black",
palette = c("limited"),
)
})
})
#### Create action buttons for conditions to be selected####
output$cond_buttons <- renderUI({
num_conds = as.integer(input$num_conds)
lapply(1:num_conds, function(i) {
bg = input[[paste0("colors", i)]]
style = paste0(
collapse = " ",
glue("background-color:{bg};
color:#ffffff;
border-color:#000000")
)
label = input[[paste0("condID", i)]]
actionButton(paste0("cond_buttons", i),
label = label,
style = style,
)
})
})
####Create the 96 well plate image####
output$plate <- renderDT({
renderPlate96()
})
output$plateWells_selected <- renderPrint({
input$plate_cells_selected
})
}
shinyApp(ui = ui, server = server)
I am creating a dynamic shiny app that works like a look up table -- it allows users to select input values and in return gives two corresponding output values (one numeric and one character) which exist in the same table.
My code needs to be dynamic, so that when the data frame changes, the user interface changes accordingly. For example, if the data table contains 3 input variables instead of 2, there needs to be one more selectInput box in the sidebar. If one variable ends up having 3 possible values instead of 2, there needs to be another option.
Thus, my code needs to:
check the updated table,
see how many variables there are and update input options in the sidebar accordingly
update range of values each of these variables has
Update the output accordingly.
Below is a simplified code:
{
library(shiny)
library(shinydashboard)
library(shinyjs)
}
Test <- data.frame(
stringsAsFactors = FALSE,
input1 = c("precarious", "precarious", "good"),
input2 = c("precarious", "moderate", "precarious"),
NumericOutput = c(3.737670877,6.688008306,8.565495761),
CharacterOutput = c("precarious", "moderate", "good")
)
ui <- dashboardPage(
dashboardHeader(),
dashboardSidebar(
useShinyjs(),
selectInput("a", label = colnames(Test[1]),
choices = unique(Test[[1]])),
selectInput("b", colnames(Test[2]),
choices = unique(Test[[2]]))
),
dashboardBody(fluidRow(valueBoxOutput("info_box1", width = 6)),
fluidRow(valueBoxOutput("info_box2", width = 6)))
)
server <- function(input, output) {
output$info_box1 <- renderValueBox({
valueBox(
value = paste0("Score in %: ",
Test$NumericOutput[Test$input1 == input$a & Test$input2 == input$b],
collapse = ", "),
subtitle = NULL)
})
output$info_box2 <- renderValueBox({
valueBox(value = paste0(
"Assessment: ",
Test$CharacterOutput[Test$input1 == input$a & Test$input2 == input$b],
collapse = ", "),
subtitle = NULL)
})
}
shinyApp(ui, server)
Here is the outline of code. I've adopted logic you provided - input cols are the ones on which filtering is done, ouput cols are the ones on which some aggregation is done. You requested only dynamic filtering and not the output. data is reactive because from your text it's obvious you want to change datasets. Code inside its reactivity is something you need to come up with because you didn't provide any information beside Test data.frame.
library(shiny)
library(shinydashboard)
library(shinyjs)
Test <- data.frame(
stringsAsFactors = FALSE,
input1 = c("precarious", "precarious", "good"),
input2 = c("precarious", "moderate", "precarious"),
NumericOutput = c(3.737670877,6.688008306,8.565495761),
CharacterOutput = c("precarious", "moderate", "good")
)
ui <- dashboardPage(
dashboardHeader(),
dashboardSidebar(
uiOutput("dynamicSidebar")
),
dashboardBody(fluidRow(valueBoxOutput("info_box1", width = 6)),
fluidRow(valueBoxOutput("info_box2", width = 6)))
)
server <- function(input, output){
rv <- reactiveValues()
data <- reactive({Test})
output$dynamicSidebar <- renderUI({
req(data())
rv$input_cols <- names(data()) %>% str_subset("^input")
input_values <- data() %>%
select(rv$input_cols) %>%
map(unique)
rv$input_cols %>%
map2(input_values, ~selectInput(.x, .x, choices = .y))
})
observe({
cond <- reactiveValuesToList(input) %>%
.[rv$input_cols] %>%
imap(~str_c(.y, "=='", .x, "'")) %>%
str_c(collapse = "&")
rv$filtered_data <- data() %>%
filter(eval(parse(text = cond)))
})
output$info_box1 <- renderValueBox({
req(rv$filtered_data)
my_value <- if(nrow(rv$filtered_data) > 0){
str_c(rv$filtered_data[["NumericOutput"]],collapse = ", ")
} else {
"empty data"
}
valueBox(
subtitle = "Score in %: ",
value = my_value
)
})
output$info_box2 <- renderValueBox({
req(rv$filtered_data)
my_value <- if(nrow(rv$filtered_data) > 0){
str_c(rv$filtered_data[["CharacterOutput"]], collapse = ", ")
} else {
"empy data"
}
valueBox(
subtitle = "Assessment:",
value = my_value
)
})
}
shinyApp(ui, server)
I'm stumped on a three part process:
I'm trying to filter what is displayed to a dataTable via Shiny inputs (in the real app there would be dozens of these).
Then, I'd like to edit cell values in the DT.
Finally, I'd like to be able to change the filters and keep the edited cell values.
The example app below does 1 and 2, but not 3. After I make an edit AND click the only_johns checkbox, the dataTable displays the original data.
Any ideas would be appreciated!
library(shiny)
library(shinydashboard)
library(tidyverse)
library(DT)
header <- dashboardHeader(title = "demo")
sidebar <- dashboardSidebar(
sidebarMenu(
downloadButton("downloadResults","Download Results"),
checkboxInput("only_johns", "only_johns")
)
)
body <- dashboardBody(
tabItems(
tabItem(
tabName = 'admin', class = 'active',
fluidRow(
box(
dataTableOutput('userTable'), width = 6
)
)
)
)
)
ui <- dashboardPage(title = 'admin function test', header, sidebar, body)
server <- function(input, output, session){
#1
start.df <- reactiveValues(data=NA)
start.df$data <- data.frame(userName = rep(c("John","Ida","Mike"),20),
id = 1:60, stringsAsFactors = FALSE)
#2 temp display filters df
display.df <- reactiveValues(data=start.df)
observe({
temp <- isolate(start.df$data)
if (input$only_johns) {
display.df$data <- temp[temp$userName == "John",]
} else {
display.df$data <- temp
}
})
# Display editable datatable
output$userTable <- renderDataTable({
req(display.df$data)
DT::datatable(isolate(display.df$data),
editable = TRUE,
rownames = FALSE)
})
###Tracking Changes###
proxy = dataTableProxy('userTable')
observe({
DT::replaceData(proxy, display.df$data, rownames = FALSE, resetPaging = FALSE)
})
observeEvent(input$userTable_cell_edit, {
display.df$data <<- editData(display.df$data, input$userTable_cell_edit, rownames = FALSE)
})
output$downloadResults <- downloadHandler(
filename = function(){paste("userTest.csv", sep = "")},
content = function(file){write.csv(start.df$data, file, row.names = FALSE)}
)
}
shinyApp(ui = ui, server = server)
So far you only update the diplay.df$data, but you need to update the original start.df$data. I've included this in my solution, to find the correct row irrespective of the current filtering, I've introduced the column row_id that is hidden in the DT. Also, I've simplified your code a bit.
library(shiny)
library(shinydashboard)
library(tidyverse)
library(DT)
header <- dashboardHeader(title = "demo")
sidebar <- dashboardSidebar(
sidebarMenu(
downloadButton("downloadResults","Download Results"),
checkboxInput("only_johns", "only_johns")
)
)
body <- dashboardBody(
tabItems(
tabItem(
tabName = 'admin', class = 'active',
fluidRow(
box(
dataTableOutput('userTable'), width = 6
)
)
)
)
)
ui <- dashboardPage(title = 'admin function test', header, sidebar, body)
server <- function(input, output, session){
#1
start.df <- reactiveValues(data=NA)
start.df$data <- data.frame(userName = rep(c("John","Ida","Mike"),20),
id = 1:60,
row_id = 1:60,
stringsAsFactors = FALSE)
#2 temp display filters df
display.df <- reactiveValues(data=start.df)
observeEvent(input$only_johns, {
temp <- isolate(start.df$data)
if (input$only_johns) {
display.df$data <- temp[temp$userName == "John",]
} else {
display.df$data <- temp
}
})
# Display editable datatable
output$userTable <- renderDataTable({
req(display.df$data)
DT::datatable(isolate(display.df$data),
editable = TRUE,
rownames = FALSE,
options = list(
columnDefs = list(
list(
visible = FALSE,
targets = 2
)
)
))
})
###Tracking Changes###
proxy = dataTableProxy('userTable')
observeEvent(input$userTable_cell_edit, {
display.df$data <- editData(display.df$data, input$userTable_cell_edit, rownames = FALSE)
DT::replaceData(proxy, display.df$data, rownames = FALSE, resetPaging = FALSE)
# update the data in the original df
# get the correct row_id
curr_row_id <- display.df$data[input$userTable_cell_edit[["row"]], "row_id", drop = TRUE]
# get the correct column position
column_pos <- input$userTable_cell_edit[["col"]] + 1 # DT starts indexing at 0
# update the data
temp <- start.df$data
temp[temp$row_id == curr_row_id, column_pos] <- input$userTable_cell_edit[["value"]]
start.df$data <- temp
})
output$downloadResults <- downloadHandler(
filename = function(){paste("userTest.csv", sep = "")},
content = function(file){write.csv(start.df$data, file, row.names = FALSE)}
)
}
shinyApp(ui, server)
Edit
Here is a version where the page gets not reset. The problem was that with the edited data, display.df$data was changed, which triggered the rerendering of output$userTable and this resetted the page. To circumvent this, I've added another reactive value that contains the edited data and don't change display.df anymore, it is only changed by changing the input filtering.
library(shiny)
library(shinydashboard)
library(tidyverse)
library(DT)
header <- dashboardHeader(title = "demo")
sidebar <- dashboardSidebar(
sidebarMenu(
downloadButton("downloadResults","Download Results"),
checkboxInput("only_johns", "only_johns")
)
)
body <- dashboardBody(
tabItems(
tabItem(
tabName = 'admin', class = 'active',
fluidRow(
box(
dataTableOutput('userTable'), width = 6
)
)
)
)
)
ui <- dashboardPage(title = 'admin function test', header, sidebar, body)
server <- function(input, output, session){
#1
start.df <- reactiveValues(data=NA)
start.df$data <- data.frame(userName = rep(c("John","Ida","Mike"),20),
id = 1:60,
row_id = 1:60,
stringsAsFactors = FALSE)
#2 temp display filters df
display.df <- reactiveValues(data=isolate(start.df))
edit.df <- reactiveValues(data = isolate(start.df))
observeEvent(input$only_johns, {
temp <- isolate(start.df$data)
if (input$only_johns) {
display.df$data <- temp[temp$userName == "John",]
edit.df$data <- temp[temp$userName == "John",]
} else {
display.df$data <- temp
edit.df$data <- temp
}
})
# Display editable datatable
output$userTable <- renderDataTable({
req(display.df$data)
DT::datatable(display.df$data,
editable = TRUE,
rownames = FALSE,
options = list(
columnDefs = list(
list(
visible = FALSE,
targets = 2
)
)
))
})
###Tracking Changes###
proxy = dataTableProxy('userTable')
observeEvent(input$userTable_cell_edit, {
edit.df$data <- editData(edit.df$data, input$userTable_cell_edit, rownames = FALSE)
DT::replaceData(proxy, edit.df$data, rownames = FALSE, resetPaging = FALSE)
# update the data in the original df
# get the correct row_id
curr_row_id <- edit.df$data[input$userTable_cell_edit[["row"]], "row_id", drop = TRUE]
# get the correct column position
column_pos <- input$userTable_cell_edit[["col"]] + 1 # DT starts indexing at 0
# update the data
temp <- start.df$data
temp[temp$row_id == curr_row_id, column_pos] <- input$userTable_cell_edit[["value"]]
start.df$data <- temp
})
output$downloadResults <- downloadHandler(
filename = function(){paste("userTest.csv", sep = "")},
content = function(file){write.csv(start.df$data, file, row.names = FALSE)}
)
}
shinyApp(ui, server)
__
Hello!
This post is very interesting.
I have used the same code above, but when I edit a cell, an error message occurs for users : "Warning : JSON invalid response" at each edition !
Everything seems correct. How can I delete this error message ?
I try this but it does not work :
tags$script(HTML("$.fn.dataTable.ext.errMode = 'throw';")),
Many thanks for your collaboration,
Kind regards,
I want to display a table showing duplicate count along with the user defined columns. I have selectinput option in the shiny app by which the user can select multiple columns to check duplicate combinations.
But when the user selects first column, incorrect column name is displayed. when two columns are selected, the column names are correct.
Please help me to find a solution for this issue. When user selects first column, correct column should be displayed.
code,
library(shiny)
library(shinydashboard)
ui <- dashboardPage(skin = "black",
dashboardHeader(title = "test"),
dashboardSidebar(
sidebarMenu(
menuItem("Complete", tabName = "comp"))),
dashboardBody(useShinyjs(),
tabItems(
tabItem(tabName = "comp",
fluidRow(
box(selectInput("dup_var", "Variable", multiple = TRUE, c("1"="1","2"="2")),
width = 3, status = "primary")),
fluidRow(
box(title = "Duplicate Records", width = 12, solidHeader = TRUE, status = "primary",
collapsible = TRUE, DT::dataTableOutput("dup_data")))))))
server <- function(input, output, session) {
observe({
cname <- c("Select All", names(mtcars))
col_options <- list()
col_options[ cname] <- cname
updateSelectInput(session, "dup_var",
label = "",
choices = c("Choose Attributes"="",col_options))
})
output$dup_data <- DT::renderDT({
if (input$dup_var == "Select All"){
col_names = colnames(mtcars)
df = count(mtcars, col_names)
df = df[df$freq > 1,]
Dup <- df$freq
df1 <- cbind.data.frame(Dup, df[,!names(df) %in% "freq"])
df1 <- df1[order(-df1$Dup),]
names(df1)[names(df1) == 'Dup'] <- 'Duplicate Count'
dp <- DT::datatable(df1, rownames = FALSE)
return(dp)
} else {
col_names = colnames(mtcars[,c(input$dup_var)])
df = count(mtcars[,c(input$dup_var)], col_names)
df = df[df$freq > 1,]
Dup <- df$freq
df1 <- cbind.data.frame(Dup, df[,!names(df) %in% "freq"])
df1 <- df1[order(-df1$Dup),]
names(df1)[names(df1) == 'Dup'] <- 'Duplicate Count'
dp <- DT::datatable(df1, rownames = FALSE)
return(dp)
}
})
}
shinyApp(ui, server)
Thanks in Advance.
It looks like you miss a few drop = FALSE. Adding this, you can handle the special case of one column the same way as the cases with multiple columns:
else {
col_names = colnames(mtcars[, c(input$dup_var), drop = FALSE])
df = count(mtcars[, c(input$dup_var), drop = FALSE], col_names)
df = df[df$freq > 1, ]
Dup <- df$freq
df1 <- cbind.data.frame(Dup, df[, !(names(df) %in% "freq"), drop = FALSE])
df1 <- df1[order(-df1$Dup), ]
names(df1)[names(df1) == 'Dup'] <- 'Duplicate Count'
Note that I am not sure about your function count, but the above seems plausible to me.
You do not need to put an if-else statement inside the output because subsetting the data frame by column will give you the values you need here. I could not completely reproduce your code, and maybe this give you an idea.
library(shiny)
choices <- c("Select All", names(mtcars))
ui <- fluidPage(
selectInput("dup_var", "Variable", choices, multiple = TRUE),
DT::dataTableOutput("dup_data")
)
server <- function(input, output, session) {
observe({
if ("Select All" %in% input$dup_var) {
allchoices <- setdiff(choices, "Select All")
updateSelectInput(session, "dup_var", selected = allchoices)
}
})
output$dup_data <- DT::renderDataTable({
data <- mtcars[input$dup_var]
do.call(rbind, lapply(names(data), function(name) {
x <- data[, name, drop = TRUE]
aggregate(list(count = x), by = list(name = x), length)
})) -> df
df <- df[df$count > 1, ]
data.frame(duplicate_count = df$count, x = df[,!names(df) %in% "count"],
stringsAsFactors = FALSE)
}, rownames = FALSE)
}
shinyApp(ui, server)