The below code works as intended, bug-free. Matrix 1, including any user input into matrix 1, is linked to ("downstreams to") matrix 2 as "Scenario 1", and the user can add scenarios via matrix 2. Results are plotted. Additional scenarios input into matrix 2 are shown as additional lines in the plot. For sake of illustration, all matrix inputs are simply summed and plotted over 10 periods via the sumMat() function.
Any ideas for how to move matrix 2 into modal dialog? The user would optionally input into matrix 2 after clicking the single actionButton() in the code. Matrix 1 remains in the sidebarPanel(). While maintaining the exact same functionality that the code currently has, whereby the plot reactively updates as the user inputs into either matrix 1 or matrix 2 and where both matrices remain linked?
library(dplyr)
library(ggplot2)
library(shiny)
library(shinyMatrix)
sumMat <- function(x){return(rep(sum(x,na.rm = TRUE), 10))}
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
matrixInput("matrix1",
label ="Matrix 1 (scenario 1):",
value = matrix(c(60,5), nrow = 1, ncol = 2, dimnames = list(NULL,c("X","Y"))),
rows = list(extend = TRUE, delete = TRUE),
class = "numeric"),
actionButton(inputId = "showMat2", "Add scenarios"),br(),br(),
matrixInput("matrix2",
label = "Matrix 2:",
value = matrix(c(60,5), ncol = 2, dimnames = list(NULL, rep("Scenario 1", 2))),
rows = list(extend = TRUE, delete = TRUE),
cols = list(extend = TRUE, delta = 2, delete = TRUE, multiheader = TRUE),
class = "numeric")
),
mainPanel(plotOutput("plot"))
)
)
server <- function(input, output, session){
observeEvent(input$matrix1, {
a <- apply(input$matrix2,2,'length<-',max(nrow(input$matrix2),nrow(input$matrix1)))
b <- apply(input$matrix1,2,'length<-',max(nrow(input$matrix2),nrow(input$matrix1)))
c <- if(length(a) == 2){c(b)} else {c(b,a[,-1:-2])}
d <- ncol(input$matrix2)
tmpMat2 <- matrix(c(c), ncol = d)
colnames(tmpMat2) <- paste("Scenario",rep(1:ncol(tmpMat2),each=2,length.out=ncol(tmpMat2)))
if(any(rownames(input$matrix1) == "")){
tmpMat1 <- input$matrix1
rownames(tmpMat1) <- paste("Row", seq_len(nrow(input$matrix1)))
updateMatrixInput(session, inputId = "matrix1", value = tmpMat1)
}
updateMatrixInput(session, inputId = "matrix2", value = tmpMat2)
})
observeEvent(input$matrix2, {
if(any(colnames(input$matrix2) == "")){
tmpMat2 <- input$matrix2
colnames(tmpMat2) <- paste("Scenario",rep(1:ncol(tmpMat2),each=2,length.out=ncol(tmpMat2)))
updateMatrixInput(session, inputId = "matrix2", value = tmpMat2)
}
if(any(rownames(input$matrix2) == "")){
tmpMat2 <- input$matrix2
rownames(tmpMat2) <- paste("Row", seq_len(nrow(input$matrix2)))
updateMatrixInput(session, inputId = "matrix2", value = tmpMat2)
}
input$matrix2
})
observeEvent(input$showMat2,{
showModal(
modalDialog(
h5("Matrix 2 needs to be shown here, with user ability to input into matrix 2"),
h5("User inputs into matrix 1 would automatically downstream to the 2 left-most columns of matrix 2 as Scenario 1"),
footer = tagList(modalButton("Close"))
))
})
plotData <- reactive({
tryCatch(
lapply(seq_len(ncol(input$matrix2)/2),
function(i){
tibble(
Scenario= colnames(input$matrix2)[i*2-1],X=seq_len(10),
Y=sumMat(input$matrix2[,(i*2-1):(i*2), drop = FALSE])
)
}) %>% bind_rows(),
error = function(e) NULL
)
})
output$plot <- renderPlot({
req(plotData())
plotData() %>% ggplot() +
geom_line(aes(x = X, y = Y, colour = as.factor(Scenario))) +
theme(legend.title=element_blank())
})
}
shinyApp(ui, server)
The below code resolves the question. Matrix 2 is successfully moved into modal dialog. The last matrix modified "controls", as the 2 matrices are linked. In a departure from original code posted where matrix 1 simply "downstreams" to matrix 2, in the below code matrix 1 downstreams to matrix 2 AND matrix 2 (scenario 1) changes "upstream" to matrix 1, with the last matrix modified controlling.
The below also has added to it a table output in the main panel showing the values of reactive matrix currentMat(), which is plotted. Watching what happens in currentMat() was important for debugging and getting this to work correctly.
To see evolution of this solution, also see post In R shiny, how to modify reactivity chain so last object modified controls other chained objects?
library(dplyr)
library(ggplot2)
library(shiny)
library(shinyMatrix)
sumMat <- function(x) {return(rep(sum(x, na.rm = TRUE), 10))}
ui <- fluidPage(sidebarLayout(
sidebarPanel(
matrixInput(
"matrix1",
label = "Matrix 1:",
value = matrix(c(60,5),ncol=2,dimnames=list(NULL,rep("Scenario 1",2))),
rows = list(extend = TRUE, delete = TRUE),
cols = list(multiheader = TRUE),
class = "numeric"
),
actionButton(inputId = "showMat2", "Add scenarios"),br(),br(),
),
mainPanel(plotOutput("plot"),
h5("currentMat() values:"),
tableOutput("table1"))
))
server <- function(input, output, session) {
currentMat <- reactiveVal(isolate(input$matrix1))
observeEvent(input$matrix1, {
tmpMat1 <- input$matrix1
if(any(rownames(input$matrix1)=="")){rownames(tmpMat1)<-paste("Row",seq_len(nrow(input$matrix1)))}
updateMatrixInput(session, inputId = "matrix1", value = tmpMat1)
tmpMat2 <- currentMat()
if(nrow(tmpMat1) > nrow(tmpMat2)){tmpMat2 <- rbind(tmpMat2, rep(NA, ncol(tmpMat2)))}
if(nrow(tmpMat2) > nrow(tmpMat1)){tmpMat1 <- rbind(tmpMat1, rep(NA, ncol(tmpMat1)))}
currentMat(cbind(tmpMat1[drop=FALSE], tmpMat2[,-1:-2,drop=FALSE]))
})
observeEvent(input$showMat2, {
showModal(modalDialog(
matrixInput(
"matrix2",
label = "Matrix 2:",
value = currentMat(),
rows = list(extend = TRUE, delete = TRUE),
cols = list(extend = TRUE,delta = 2,delete = TRUE,multiheader = TRUE),
class = "numeric"
),
footer = tagList(modalButton("Close"))
))
})
observeEvent(input$matrix2, {
tmpMat2 <- input$matrix2
rownames(tmpMat2) <- paste("Row", seq_len(nrow(input$matrix2)))
colnames(tmpMat2) <-
paste("Scenario", rep(1:ncol(tmpMat2),each = 2,length.out = ncol(tmpMat2)))
currentMat(tmpMat2)
updateMatrixInput(session, inputId = "matrix2", value = tmpMat2)
updateMatrixInput(session, inputId = "matrix1", value = tmpMat2[, 1:2, drop = FALSE])
})
output$table1 <- renderTable(currentMat())
plotData <- reactive({
tryCatch(
lapply(seq_len(ncol(currentMat()) / 2),
function(i) {
tibble(
Scenario = colnames(currentMat())[i * 2 - 1],
X = seq_len(10),
Y = sumMat(currentMat()[, (i * 2 - 1):(i * 2), drop = FALSE])
)
}) %>% bind_rows(),
error = function(e)
NULL
)
})
output$plot <- renderPlot({
plotData() %>% ggplot() +
geom_line(aes(
x = X,
y = Y,
colour = as.factor(Scenario)
)) +
theme(legend.title = element_blank())
})
}
shinyApp(ui, server)
Related
In running the below code, observeEvent(input$matrix2, {...}) is nullifying observeEvent(input$matrix1, {...}). Why is this happening and how do I fix?
Matrix 1 and matrix 2 are linked. Values from matrix 1 downstream to matrix 2 as matrix 2 "Scenario 1", and matrix 2 allows the user to input additional scenarios via horizontally-expanding matrix. Matrix 2 is rendered in modal dialog, after clicking the single action button. The App (plot) works fine when matrix 1 is input into first (plotting user inputs into both matrices 1 and 2 as it should); but when matrix 2 is viewed (with our without any user inputs into the matrix 2) before inputting into matrix 1, then matrix 1 is rendered useless. By useless I mean inputs into matrix 1 are no longer plotted.
Output for illustration purposes is simply the sum of matrix inputs, plotted over 10 periods, per sumMat(...) function.
I've played around with all variations of isolate(...), req(...), etc., with no luck so far.
The images at the bottom illustrate the issue: the first 2 images show the App working well when inputting into matrix 1 first; the 3rd images shows the failure when accessing matrix 2 before matrix 1.
library(dplyr)
library(ggplot2)
library(shiny)
library(shinyMatrix)
sumMat <- function(x){return(rep(sum(x,na.rm = TRUE), 10))}
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
matrixInput("matrix1",
value = matrix(c(60,5), ncol = 2, dimnames = list(NULL,c("X","Y"))),
rows = list(extend = TRUE, delete = TRUE), class = "numeric"),
actionButton("matrix2show","Add scenarios"),
),
mainPanel(plotOutput("plot"))
)
)
server <- function(input, output, session){
observeEvent(input$matrix1, {
tmpMat1 <- input$matrix1
if(any(rownames(input$matrix1) == "")){rownames(tmpMat1) <- paste("Row", seq_len(nrow(input$matrix1)))}
updateMatrixInput(session, inputId = "matrix1", value = tmpMat1)
})
observeEvent(input$matrix2, { ### updates matrix 2 to reflect larger of matrix 1 and matrix 2 rows
req(input$matrix1)
a <- apply(input$matrix2,2,'length<-',max(nrow(input$matrix2),nrow(input$matrix1)))
b <- apply(input$matrix1,2,'length<-',max(nrow(input$matrix2),nrow(input$matrix1)))
c <- if(length(a) == 2){c(b)} else {c(b,a[,-1:-2])}
d <- ncol(input$matrix2)
tmpMat2 <- matrix(c(c), ncol = d)
tmpMat2[1,2] <- input$matrix1[1,2]
colnames(tmpMat2) <- paste("Scenario",rep(1:ncol(tmpMat2),each=2,length.out=ncol(tmpMat2)))
rownames(tmpMat2) <- paste("Row", seq_len(nrow(input$matrix2)))
updateMatrixInput(session,inputId="matrix2",value=tmpMat2)
})
observeEvent(input$matrix2show,{
showModal(
modalDialog(
matrixInput("matrix2",
label = "Matrix 2 (Value Y applied in Period X):",
value = if(is.null(input$matrix2))
{matrix(c(input$matrix1[,1],input$matrix1[,2]),
ncol = 2, dimnames = list(NULL, rep("Scenario 1", 2)))}
else {input$matrix2},
rows = list(extend = TRUE, delete = TRUE),
cols = list(extend = TRUE, delta = 2, delete = TRUE, multiheader = TRUE),
class = "numeric"),
footer = tagList(modalButton("Exit box"))
))
})
plotData <- reactive({
tryCatch(
if(isTruthy(input$matrix2)){
lapply(seq_len(ncol(input$matrix2)/2), # column counter to set matrix index as it expands
function(i){
tibble(Scenario = colnames(input$matrix2)[i*2-1],
X = seq_len(10),Y = sumMat(input$matrix2[,(i*2-1):(i*2), drop = FALSE]))
}) %>% bind_rows()
}
else {tibble(Scenario = "Scenario 1", X = seq_len(10),Y = sumMat(input$matrix1))},
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)
This is a partial solution. Each time you click on the actionButton, you are creating the same ID for matrix2. That is a problem as Shiny requires unique ID. Once we adjust for that, it works fine. See below. You still need to work on how to display the previous columns of input$matrix2.
library(dplyr)
library(ggplot2)
library(shiny)
library(shinyMatrix)
sumMat <- function(x){return(rep(sum(x,na.rm = TRUE), 10))}
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
matrixInput("matrix1",
value = matrix(c(60,5), ncol = 2, dimnames = list(NULL,c("X","Y"))),
rows = list(extend = TRUE, delete = TRUE), class = "numeric"),
actionButton("matrix2show","Add scenarios"),
),
mainPanel(plotOutput("plot"))
)
)
server <- function(input, output, session){
rv <- reactiveValues(tmpMat=NULL)
observeEvent(input$matrix1, {
tmpMat1 <- input$matrix1
if(any(rownames(input$matrix1) == "")){rownames(tmpMat1) <- paste("Row", seq_len(nrow(input$matrix1)))}
updateMatrixInput(session, inputId = "matrix1", value = tmpMat1)
})
observeEvent(input$matrix2, { ### updates matrix 2 to reflect larger of matrix 1 and matrix 2 rows
req(input[[paste0("matrix2",input$matrix2show)]])
req(input$matrix1)
imatrix2 <- input[[paste0("matrix2",input$matrix2show)]]
a <- apply(imatrix2,2,'length<-',max(nrow(imatrix2),nrow(input$matrix1)))
b <- apply(input$matrix1,2,'length<-',max(nrow(imatrix2),nrow(input$matrix1)))
c <- if(length(a) == 2){c(b)} else {c(b,a[,-1:-2])}
d <- ncol(imatrix2)
tmpMat2 <- matrix(c(c), ncol = d)
tmpMat2[1,2] <- input$matrix1[1,2]
colnames(tmpMat2) <- paste("Scenario",rep(1:ncol(tmpMat2),each=2,length.out=ncol(tmpMat2)))
rownames(tmpMat2) <- paste("Row", seq_len(nrow(imatrix2)))
updateMatrixInput(session,inputId=paste0("matrix2",input$matrix2show),value=tmpMat2)
rv$tmpMat <- tmpMat2
})
observe({print(rv$tmpMat)})
observeEvent(input$matrix2show,{
if (input$matrix2show==1) ivalue <- matrix(c(input$matrix1[,1],input$matrix1[,2]),
ncol = 2, dimnames = list(NULL, rep("Scenario 1", 2)))
else{ if (!is.null(rv$tmpMat)) ivalue <- rv$tmpMat else ivalue <- input$matrix1}
showModal(
modalDialog(
matrixInput(paste0("matrix2",input$matrix2show),
label = "Matrix 2 (Value Y applied in Period X):",
value = ivalue,
rows = list(extend = TRUE, delete = TRUE),
cols = list(extend = TRUE, delta = 2, delete = TRUE, multiheader = TRUE),
class = "numeric"),
footer = tagList(modalButton("Exit box"))
))
})
plotData <- reactive({
req(input$matrix1)
imatrix2 <- input[[paste0("matrix2",input$matrix2show)]]
tryCatch(
if(isTruthy(imatrix2)){
lapply(seq_len(ncol(imatrix2)/2), # column counter to set matrix index as it expands
function(i){
tibble(Scenario = colnames(imatrix2)[i*2-1],
X = seq_len(10),Y = sumMat(imatrix2[,(i*2-1):(i*2), drop = FALSE]))
}) %>% bind_rows()
}
else {tibble(Scenario = "Scenario 1", X = seq_len(10),Y = sumMat(input$matrix1))},
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)
I believe I have a fundamental misunderstanding of how the isTruthy() function works.
In the below MWE, inputs into the first matrix are summed and plotted straight-line over 10 periods. However this works ONLY IF the matrix 2 creation function is commented-out like in this MWE. When matrix 2 creation is un-commented (matrix 2 does the same thing as matrix 1: inputs are summed and plotted, but plotted as a separate line), matrix 1 inputs are no longer summed and plotted.
The issue appears to lie in if(isTruthy(input$matrix2)){... under the plotData <- reactive({...} function in the server section. I thought if(isTruthy(...) in this context meant if there are manual inputs into matrix 2, then plot matrix 2, otherwise skip ahead to the else{...} and just plot matrix 1. (Note that in the larger App this code is derived from, matrix 2 is rendered in modal dialog, thus the reason why plotting has been broken up this way).
How would I change this code so that matrix 2 is only plotted if there are manual inputs into matrix 2; otherwise plot matrix 1? In order to solve a larger problem I have, I'd like matrix 1 to plot correctly even when matrix 2 is present.
library(dplyr)
library(ggplot2)
library(shiny)
library(shinyMatrix)
sumMat <- function(x){return(rep(sum(x,na.rm = TRUE), 10))}
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
matrixInput("matrix1",
value = matrix(c(60,5), ncol = 2, dimnames = list(NULL,c("X","Y"))),
rows = list(extend = TRUE, delete = TRUE), class = "numeric"),
# matrixInput("matrix2",
# label = "Matrix 2 (Value Y applied in Period X):",
# value = matrix(c(60,5),ncol=2,dimnames=list(NULL,rep("Scenario 1",2))),
# rows = list(extend = TRUE, delete = TRUE),
# cols = list(extend = TRUE, delta = 2, delete = TRUE, multiheader = TRUE),
# class = "numeric"),
),
mainPanel(plotOutput("plot"))
)
)
server <- function(input, output, session){
observeEvent(input$matrix1, {
tmpMat1 <- input$matrix1
if(any(rownames(input$matrix1) == "")){rownames(tmpMat1) <- paste("Row", seq_len(nrow(input$matrix1)))}
updateMatrixInput(session, inputId = "matrix1", value = tmpMat1)
})
plotData <- reactive({
tryCatch(
if(isTruthy(input$matrix2)){
lapply(seq_len(ncol(input$matrix2)/2),
function(i){
tibble(Scenario = colnames(input$matrix2)[i*2-1],
X = seq_len(10),Y = sumMat(input$matrix2[,(i*2-1):(i*2), drop = FALSE]))
}) %>% bind_rows()
} else
{tibble(Scenario = "Scenario 1", X = seq_len(10),Y = sumMat(input$matrix1))},
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)
See error explanation in CuriousJorge response to Limey comment (incorrect use of isTruthy(), flawed if/then/else logic in the plotData() function).
Note how matrix 1 inputs "downstream" to matrix 2 as scenario 1.
Below is the resolved code:
library(dplyr)
library(ggplot2)
library(shiny)
library(shinyMatrix)
sumMat <- function(x){return(rep(sum(x,na.rm = TRUE), 10))}
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
matrixInput("matrix1",
label ="Matrix 1 (scenario 1):",
value = matrix(c(60,5), nrow = 1, ncol = 2, dimnames = list(NULL,c("X","Y"))),
rows = list(extend = TRUE, delete = TRUE),
class = "numeric"),
matrixInput("matrix2",
label = "Matrix 2:",
value = matrix(c(60,5), ncol = 2, dimnames = list(NULL, rep("Scenario 1", 2))),
rows = list(extend = TRUE, delete = TRUE),
cols = list(extend = TRUE, delta = 2, delete = TRUE, multiheader = TRUE),
class = "numeric")
),
mainPanel(plotOutput("plot"))
)
)
server <- function(input, output, session){
observeEvent(input$matrix1, {
a <- apply(input$matrix2,2,'length<-',max(nrow(input$matrix2),nrow(input$matrix1)))
b <- apply(input$matrix1,2,'length<-',max(nrow(input$matrix2),nrow(input$matrix1)))
c <- if(length(a) == 2){c(b)} else {c(b,a[,-1:-2])}
d <- ncol(input$matrix2)
tmpMat2 <- matrix(c(c), ncol = d)
colnames(tmpMat2) <- paste("Scenario",rep(1:ncol(tmpMat2),each=2,length.out=ncol(tmpMat2)))
if(any(rownames(input$matrix1) == "")){
tmpMat1 <- input$matrix1
rownames(tmpMat1) <- paste("Row", seq_len(nrow(input$matrix1)))
updateMatrixInput(session, inputId = "matrix1", value = tmpMat1)
}
updateMatrixInput(session, inputId = "matrix2", value = tmpMat2)
})
observeEvent(input$matrix2, {
if(any(colnames(input$matrix2) == "")){
tmpMat2 <- input$matrix2
colnames(tmpMat2) <- paste("Scenario",rep(1:ncol(tmpMat2),each=2,length.out=ncol(tmpMat2)))
updateMatrixInput(session, inputId = "matrix2", value = tmpMat2)
}
if(any(rownames(input$matrix2) == "")){
tmpMat2 <- input$matrix2
rownames(tmpMat2) <- paste("Row", seq_len(nrow(input$matrix2)))
updateMatrixInput(session, inputId = "matrix2", value = tmpMat2)
}
input$matrix2
})
plotData <- reactive({
tryCatch(
lapply(seq_len(ncol(input$matrix2)/2),
function(i){
tibble(
Scenario = colnames(input$matrix2)[i*2-1],
X = seq_len(10),
Y = sumMat(input$matrix2[,(i*2-1):(i*2), drop = FALSE])
)
}) %>% bind_rows(),
error = function(e) NULL
)
})
output$plot <- renderPlot({
req(plotData())
plotData() %>% ggplot() +
geom_line(aes(x = X, y = Y, colour = as.factor(Scenario))) +
theme(legend.title=element_blank())
})
}
shinyApp(ui, server)
The following almost-MWE code links user inputs into three objects using observeEvent. The image at the bottom also shows and explains these three objects:
First object is a slider input for number of modeled periods, called variable X.
Second object is a 1x1 matrix where the user inputs a single variable Y.
Third object is a vertically expandable matrix where the user inputs a series of values X|Y.
The calculation underlying the plot is a simple sum product, used as a placeholder simplification for this MWE.
User input linkages are shown in the image at the bottom.
How would I replace the two observeEvents used in MWE below with an observe in each case? If possible. (I believe this will offer a solution to another issue I'm having, based on my research of what the observe function does). I've only used observeEvents in my limited Shiny experience.
MWE code:
library(dplyr)
library(ggplot2)
library(shiny)
library(shinyMatrix)
sumProd <- function(a, b) {
c <- rep(NA, a)
c[] <- sum(b[,1], na.rm = T) %*% sum(b[,2],na.rm = T)
return(c)
}
ui <- fluidPage(
sliderInput('periods', 'Modeled periods (X):', min=1, max=10, value=10),
matrixInput("matrix1",
value = matrix(c(5), nrow = 1, ncol = 1, dimnames = list("Base rate (Y)",NULL)),
cols = list(names = FALSE),
class = "numeric"),
matrixInput("matrix2",
value = matrix(c(10,5), nrow = 1, ncol = 2, dimnames = list(NULL,c("X","Y"))),
rows = list(extend = TRUE, delete = TRUE),
class = "numeric"),
plotOutput("plot")
)
server <- function(input, output, session){
observeEvent(input$periods,{
updateMatrixInput(
session, inputId = "matrix2",
value = matrix(c(input$periods,input$matrix2[1,2]),1,2,dimnames=list(NULL,c("X","Y")))
)
})
observeEvent(input$matrix1, {
tmpMat2 <- c(input$matrix2[,1],input$matrix2[,2])
tmpMat2[length(input$matrix2)/2+1] <- input$matrix1[,1]
updateMatrixInput(session,inputId="matrix2",value=matrix(tmpMat2,ncol=2,dimnames=list(NULL,c("X","Y")))
)
})
plotData <- reactive({
tryCatch(
lapply(seq_len(ncol(input$matrix2)/2), # column counter to set matrix index as it expands
function(i){
tibble(
Scenario = colnames(input$matrix2)[i*2-1],
X = seq_len(input$periods),
Y = sumProd(input$periods,input$matrix2[,(i*2-1):(i*2), drop = FALSE])
)
}) %>% bind_rows(),
error = function(e) NULL
)
})
output$plot <- renderPlot({
req(plotData())
plotData() %>% ggplot() +
geom_line(aes(x = X, y = Y, colour = as.factor(Scenario))) +
theme(legend.title=element_blank())
})
}
shinyApp(ui, server)
Image showing user input linkages among the 3 objects:
I reviewed the post How to listen for more than one event expression within a Shiny observeEvent, which does a nice job of illustrating the interchangeability, in certain circumstances, of observe and observeEvent. Based on that post, I came up with the following substitutions of observeEvent with observe:
sumProd <- function(a, b) {
c <- rep(NA, a)
c[] <- sum(b[,1], na.rm = T) %*% sum(b[,2],na.rm = T)
return(c)
}
ui <- fluidPage(
sliderInput('periods', 'Modeled periods (X):', min=1, max=10, value=10),
matrixInput("matrix1",
value = matrix(c(5), nrow = 1, ncol = 1, dimnames = list("Base rate (Y)",NULL)),
cols = list(names = FALSE),
class = "numeric"),
matrixInput("matrix2",
value = matrix(c(10,5), nrow = 1, ncol = 2, dimnames = list(NULL,c("X","Y"))),
rows = list(extend = TRUE, delete = TRUE),
class = "numeric"),
plotOutput("plot")
)
server <- function(input, output, session){
observe({
input$periods
if(input$periods!=0){
isolate(
updateMatrixInput(
session, inputId = "matrix2",
value = matrix(c(input$periods,input$matrix2[1,2]),1,2,dimnames=list(NULL,c("X","Y")))
)
)
}
})
observe({
input$matrix1
if(input$matrix1!=0){
tmpMat2 <- c(input$matrix2[,1],input$matrix2[,2])
tmpMat2[length(input$matrix2)/2+1] <- input$matrix1[,1]
isolate(
updateMatrixInput(
session,inputId="matrix2",
value=matrix(tmpMat2,ncol=2,dimnames=list(NULL,c("X","Y")))
)
)
}
})
plotData <- reactive({
tryCatch(
lapply(seq_len(ncol(input$matrix2)/2), # column counter to set matrix index as it expands
function(i){
tibble(
Scenario = colnames(input$matrix2)[i*2-1],
X = seq_len(input$periods),
Y = sumProd(input$periods,input$matrix2[,(i*2-1):(i*2), drop = FALSE])
)
}) %>% bind_rows(),
error = function(e) NULL
)
})
output$plot <- renderPlot({
req(plotData())
plotData() %>% ggplot() +
geom_line(aes(x = X, y = Y, colour = as.factor(Scenario))) +
theme(legend.title=element_blank())
})
}
shinyApp(ui, server)
The following "Resolved code" resolves my original question (original question shown below this "Resolved code"). Basically I parse the upstream matrix values inside the observeEvents and then downstream them to the applicable matrix in the same observeEvent. I ask however: is this non-renderUI method better than the renderUI method shown in "https://stackoverflow.com/questions/69718072/in-r-shiny-how-to-establish-a-reactivity-chain-for-a-series-of-linked-matrix-in"?
Resolved code:
ui <- fluidPage(
sliderInput('periods', 'Modeled periods (X):', min=1, max=10, value=10),
h5(strong("Matrix 1 is omitted for MWE")),
h5(strong("Matrix 2:")),
matrixInput("matrix2",
value = matrix(c(10, 5), 1, 2, dimnames = list(NULL,c("X","Y"))),
rows = list(extend = TRUE, names = TRUE, delete = TRUE),
class = "numeric"),
h5(strong("Matrix 3:")),
matrixInput("matrix3",
value = matrix(c(10,5), ncol = 2, dimnames = list(NULL, rep("Scenario 1", 2))),
rows = list(extend = TRUE, delta = 1, names = TRUE, delete = TRUE),
cols = list(extend = TRUE, delta = 2, names = TRUE, delete = TRUE, multiheader = TRUE),
class = "numeric"),
plotOutput("plot")
)
server <- function(input, output, session){
observeEvent(input$periods, {
updateMatrixInput(session, inputId = "matrix2",
value = matrix(c(input$periods, 5), 1, 2, dimnames = list(NULL,c("X","Y"))))
})
observeEvent(input$matrix2, {
if(any(rownames(input$matrix2) == "")){
tmpMatrix <- input$matrix2
rownames(tmpMatrix) <- paste("Row", seq_len(nrow(input$matrix2)))
isolate(updateMatrixInput(session, inputId = "matrix2", value = tmpMatrix))
isolate(updateMatrixInput(session, inputId = "matrix3",
value = tmpMatrix))
}
input$matrix2
isolate(
updateMatrixInput(
session,
inputId = "matrix3",
value = matrix(
c(input$matrix2[,1],input$matrix2[,2]),
ncol = 2,
dimnames = list(NULL, rep("Scenario 1", 2)))
)
)
})
observeEvent(input$matrix3, {
if(any(colnames(input$matrix3) == "")){
tmpMatrix <- input$matrix3
colnames(tmpMatrix) <- paste("Scenario",rep(1:ncol(tmpMatrix),each=2,length.out=ncol(tmpMatrix)))
isolate(updateMatrixInput(session, inputId = "matrix3", value = tmpMatrix))
}
input$matrix3
})
plotData <- reactive({
req(input$periods)
tryCatch(
lapply(seq_len(ncol(input$matrix3)/2), # column counter to set matrix index as it expands
function(i){
tibble(
Scenario = colnames(input$matrix3)[i*2-1],
X = seq_len(input$periods),
Y = interpol(input$periods,input$matrix3[,(i*2-1):(i*2), drop = FALSE])
)
}) %>% bind_rows(),
error = function(e) NULL
)
})
output$plot <- renderPlot({
req(plotData())
plotData() %>% ggplot() +
geom_line(aes(x = X, y = Y, colour = as.factor(Scenario))) +
theme(legend.title=element_blank())
})
}
shinyApp(ui, server)
This is a follow-on to my earlier post today "https://stackoverflow.com/questions/69718072/in-r-shiny-how-to-establish-a-reactivity-chain-for-a-series-of-linked-matrix-in". In the code in that post, I used renderUI for the matrices and their links and it works well (as corrected by ismirsehregal). I'm trying to move away from renderUI (for sake of simplicity) and use updateMatrixInput inside observeEvent in order to maintain a reactivity chain without renderUI.
The below without-renderUI code ALMOST works like the renderUI version posted earlier, except I can't get the slider input input$periods to reactively downstream to the matrices, as shown in the image below. Also, I lost the automatic generation of sequential column headers for Matrix 3 in the below code, as shown in the image.
Basically, I don't know how to parse or subset inputs into updateMatrixInput; as the below code is drafted, it takes the entire set of matrix values that is being downstreamed from, thus giving rise to my issue. If I could tell it which matrix rows/columns to take in for updating, then it would work.
Maybe the answer in this case is sticking with renderUI if parsing/subsetting is not possible?
Code:
library(ggplot2)
library(shiny)
library(shinyMatrix)
interpol <- function(a, b) { # [a] = modeled periods, [b] = matrix inputs
c <- b
c[,1][c[,1] > a] <- a
d <- diff(c[,1, drop = FALSE])
d[d <= 0] <- NA
d <- c(1,d)
c <- cbind(c,d)
c <- na.omit(c)
c <- c[,-c(3),drop=FALSE]
e <- rep(NA, a)
e[c[,1]] <- c[,2]
e[seq_len(min(c[,1])-1)] <- e[min(c[,1])]
if(max(c[,1]) < a){e[seq(max(c[,1]) + 1, a, 1)] <- 0}
e <- approx(seq_along(e)[!is.na(e)], e[!is.na(e)], seq_along(e))$y # Interpolates
return(e)
}
ui <- fluidPage(
sliderInput('periods', 'Modeled periods (X):', min=1, max=10, value=10),
h5(strong("Matrix 1 is omitted for MWE")),
h5(strong("Matrix 2:")),
matrixInput("matrix2",
value = matrix(c(10, 5), 1, 2, dimnames = list(NULL,c("X","Y"))),
rows = list(extend = TRUE, names = TRUE, delete = TRUE),
class = "numeric"),
h5(strong("Matrix 3:")),
matrixInput("matrix3",
value = matrix(c(10, 5), ncol = 2, dimnames = list(NULL, rep("Scenario 1", 2))),
rows = list(extend = TRUE, delta = 1, names = TRUE, delete = TRUE),
cols = list(extend = TRUE, delta = 2, names = TRUE, delete = TRUE, multiheader = TRUE),
class = "numeric"),
plotOutput("plot")
)
server <- function(input, output, session){
observeEvent(input$matrix2, {
if(any(rownames(input$matrix2) == "")){
tmpMatrix <- input$matrix2
rownames(tmpMatrix) <- paste("Row", seq_len(nrow(input$matrix2)))
isolate(updateMatrixInput(session, inputId = "matrix2", value = tmpMatrix))
isolate(updateMatrixInput(session, inputId = "matrix3", value = tmpMatrix))
}
input$matrix2
isolate(updateMatrixInput(session, inputId = "matrix3", value = input$matrix2))
})
observeEvent(input$matrix3, {
if(any(colnames(input$matrix3) == "")){
tmpMatrix <- input$matrix3
colnames(tmpMatrix) <- paste("Scenario",rep(1:ncol(tmpMatrix),each=2,length.out=ncol(tmpMatrix)))
isolate(updateMatrixInput(session, inputId = "matrix3", value = tmpMatrix))
}
input$matrix3
})
plotData <- reactive({
req(input$periods)
tryCatch(
lapply(seq_len(ncol(input$matrix3)/2), # column counter to set matrix index as it expands
function(i){
tibble(
Scenario = colnames(input$matrix3)[i*2-1],
X = seq_len(input$periods),
Y = interpol(input$periods,input$matrix3[,(i*2-1):(i*2), drop = FALSE])
)
}) %>% bind_rows(),
error = function(e) NULL
)
})
output$plot <- renderPlot({
req(plotData())
plotData() %>% ggplot() +
geom_line(aes(x = X, y = Y, colour = as.factor(Scenario))) +
theme(legend.title=element_blank())
})
}
shinyApp(ui, server)
The "Resolved code" above posted as an edit to the original question resolves this issue (original question shown below the "Resolved code"). Basically the upstream matrix values are parsed inside an observeEvent and are then downstreamed to the applicable matrix in the same observeEvent. Regarding the question raised: "is this non-renderUI method better than the renderUI method shown in...?" My answer is absolutely yes - removing the renderUI leaves you with much cleaner, easier to follow code. I just had to get more comfortable with embedding matrix parsing code inside an observeEvent. I also understand the code will run quicker without renderUI since objects are only edited and not completely re-rendered as occurs with renderUI.
The below MWE Code 1 works fine, in calculating the sumproduct of 2 columns of numbers, with the sumproduct input matrix expanding horizontally to accommodate additional sumproduct scenarios.
MWE Code 2 below is a modification of MWE Code 1 to make the input matrix vertically expandable too, so the user can add rows of elements to be summed in the sumproduct calculation. When I run MWE Code 2, the code crashes giving me "Error in [: (subscript) logical subscript too long".
Why am I getting this error?
The images below illustrate the issue.
MWE Code 1:
library(shiny)
library(shinyMatrix)
library(dplyr)
library(ggplot2)
sumProd <- function(a, b) { # a = periods, b = matrix inputs
c <- rep(NA, a)
c[] <- sum(b[,1]) %*% sum(b[,2])
return(c)
}
ui <- fluidPage(
sliderInput('periods', 'X-axis periods:', min=1, max=10, value=10),
matrixInput(
"myMatrixInput",
label = "Two columns to sumproduct are 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 = sumProd(input$periods,input$myMatrixInput[1,(i*2-1):(i*2), drop = FALSE])
)
}) %>% 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)
MWE Code 2:
sumProd <- function(a, b) { # a = periods, b = matrix inputs
c <- rep(NA, a)
c[] <- sum(b[,1]) %*% sum(b[,2])
return(c)
}
ui <- fluidPage(
sliderInput('periods', 'X-axis periods:', min=1, max=10, value=10),
matrixInput(
"myMatrixInput",
label = "Two columns to sumproduct are 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 = TRUE, delta = 1, names = FALSE, delete = FALSE),
class = "numeric"),
plotOutput("plot")
)
server <- function(input, output, session) {
sanitizedMat <- reactiveVal() # < for vertical matrix expansion
observeEvent(input$myMatrixInput, {
if(any(colnames(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)
)
}
sanitizedMat(na.omit(input$myMatrixInput))
})
plotData <- reactive({
tryCatch(
lapply(seq_len(ncol(sanitizedMat())/2),
function(i){
tibble(
Scenario = colnames(sanitizedMat())[i*2-1],
X = seq_len(input$periods),
Y = sumProd(input$periods,sanitizedMat()[,(i*2-1):(i*2), drop = FALSE])
)
}) %>% 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)
Solution was to simply eliminate automated matrix empty column deletion under the single observeEvent() and modify UDF sumProd() to ignore NA's (added na.rm = T to sum() in sumProd()). NA's will arise in the matrix when sub-columns (groupings of 2 columns under each scenario header) are of unequal lengths, so ignoring the NA's resolves the issue. Also removed UDF sanitizedMat() and automated empty column deletion feature in MWE2 to simplify.
Revised code:
library(shiny)
library(shinyMatrix)
library(dplyr)
library(ggplot2)
sumProd <- function(a, b) { # a = periods, b = matrix inputs
c <- rep(NA, a)
c[] <- sum(b[,1], na.rm = T) %*% sum(b[,2],na.rm = T) # Added na.rm = T
return(c)
}
ui <- fluidPage(
sliderInput('periods', 'X-axis periods:', min=1, max=10, value=10),
matrixInput(
"myMatrixInput",
label = "Two columns to sumproduct are 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 = TRUE, delta = 1, names = FALSE, delete = FALSE),
class = "numeric"),
plotOutput("plot")
)
server <- function(input, output, session) {
observeEvent(input$myMatrixInput, {
if(any(colnames(input$myMatrixInput) == "")){
tmpMatrix <- input$myMatrixInput
colnames(tmpMatrix) <- paste("Scenario",rep(1:ncol(tmpMatrix),each=2,length.out=ncol(tmpMatrix)))
isolate(updateMatrixInput(session, inputId = "myMatrixInput", value = tmpMatrix))
}
input$myMatrixInput
})
plotData <- reactive({
tryCatch(
lapply(seq_len(ncol(input$myMatrixInput)/2), # column counter to set matrix index as it expands
function(i){
tibble(
Scenario = colnames(input$myMatrixInput)[i*2-1],
X = seq_len(input$periods),
Y = sumProd(input$periods,input$myMatrixInput[,(i*2-1):(i*2), drop = FALSE])
)
}) %>% 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)