How to add warnings to UI outputs generated dynamically in Shiny - r

I am working on a shiny app that can generate a determined number of UI outputs in form of inputs based on a value defined by the user. Thanks to the help of #YBS I was able to get the app working. But now I face a new issue. Although I could define min and max value for the inputs generated, I would like to add a warning in the inputs when a value is greater than 100, I found shinyfeedback package can do this but I do not where to put properly the code or what to do in the case of dynamic inputs like the ones generated here.
This is the working app:
library(shiny)
library(shinydashboard)
library(DT)
library(shinyFeedback)
#Function
compute <- function(firstitem,seconditem)
{
Sum <- firstitem+seconditem
Difference <- firstitem+seconditem
Product <- firstitem*seconditem
Ratio <- firstitem/seconditem
Res <- data.frame(C1=Sum,C2=Difference,C3=Product,C4=Ratio)
return(Res)
}
#App
ui = shinyUI(fluidPage(
titlePanel("Compare"),
sidebarLayout(
sidebarPanel(
numericInput("numitems", label = "Number of items to compare?",
min = 1, max = 100, value = 1),
uiOutput("period_cutpoints"),
uiOutput("period_cutpoints2"),
actionButton("submit", "Submit")
),
mainPanel(
uiOutput("t1")
)
)
))
server = shinyServer(function(input, output, session) {
output$period_cutpoints<-renderUI({
req(input$numitems)
lapply(1:(input$numitems), function(i) {
numericInput(inputId=paste0("firstitem",i),
label=paste0("Enter the value of first item ", i, ":"),value = i)
})
})
output$period_cutpoints2<-renderUI({
req(input$numitems)
lapply(1:(input$numitems), function(i) {
numericInput(inputId=paste0("seconditem",i),
label=paste0("Enter the value of second item ", i, ":"),value = i+i)
})
})
seldates <- reactiveValues(x=NULL)
observeEvent(input$submit, {
seldates$x <- list()
lapply(1:(input$numitems), function(i) {
seldates$x[[i]] <- compute(firstitem = input[[paste0("firstitem", i)]],seconditem = input[[paste0("seconditem", i)]])
})
})
observeEvent(input$submit, {
lapply(1:(input$numitems), function(i) {
output[[paste0("table",i)]] <- renderDT(seldates$x[[i]])
})
output$t1 <- renderUI({
tagList(
lapply(1:(input$numitems), function(i) {
DTOutput(paste0("table",i))
})
)
})
})
})
shinyApp(ui = ui , server = server)
I tried to add some code inside the dynamic inputs in this way:
#Code demo
output$period_cutpoints<-renderUI({
req(input$numitems)
lapply(1:(input$numitems), function(i) {
numericInput(inputId=paste0("firstitem",i),
label=paste0("Enter the value of first item ", i, ":"),value = i)
})
lapply(1:(input$numitems), function(i) {
observeEvent(input[[paste0('firstitem',i)]], {
shinyFeedback::feedbackWarning(
inputId = paste0('firstitem',i),
show = input[[paste0('firstitem',i)]] > 100,
text = "Number less than 100 required.",
color="red"
)
})
})
})
Unfortunately, this action broke down the app:
And the first input was not generated as you can see.
How can I solve this issue so that I can have warnings when the value is greater than 100? Moreover, this leads to an additional fact, in the action button if working with multiple inputs generated dynamically, how could I do something like this:
#How to extend the if condition so that it can consider the number of inputs defined by the user
observeEvent(input$submit,
{
if(input$firstitem1 < 0 && input$seconditem1 < 0 && input$firstitem2<0 && input$seconditem1<0)
{
showModal(modalDialog(title ="Warning!!!", "Check fields!!!",easyClose = T))
}
else
{
showModal(modalDialog(title ="Congratulations!!!", "Computing Done!!!",easyClose = T))
}
})
How could I change the if so that it considers all the inputs that can be generated.
Many thanks!

I think you have a couple of problems here.
First, you have forgotten to add useShinyFeedback() to your UI definition.
ui = shinyUI(
fluidPage(
useShinyFeedback(),
titlePanel("Compare"),
...
Second, you've put the observeEvents that monitor your first item values inside your renderUI. That's not going to work: R's standard scoping means that these observeEvents won't be available to monitor changes in the corresponding input widgets. The solution is to create a separate observeEvent to create your observers on the inputs:
observeEvent(input$numitems, {
lapply(1:(input$numitems), function(i) {
observeEvent(input[[paste0('firstitem',i)]], {
shinyFeedback::feedbackWarning(
inputId = paste0('firstitem',i),
show = input[[paste0('firstitem',i)]] > 100,
text = "Number less than 100 required.",
color="red"
)
})
})
})
Making these changes gives me, for example,
With regard to your final question about the Submit actionButton, and as a general observation, I think your life will be much easier if you use Shiny modules to solve this problem. This will allow you to delegate the error checking to the indivudual modules and remove the need to continually loop through the indices of the dynamic inputs. This will lead to shorter, simpler, and more understandable code.
One thing to bear in mind if you do this: make sure you put a call to useShinyFeedback in the definition of the module UI.

Related

track closest values in a table Shiny r

I am building a Shiny App that does random simulations of some stuff in three ways and saves the results in a table. I want the table to (1) fill the cell green for the closest value to the correct answer, and (2) include a line on bottom tracking total number of times each test group has been the closest.
what I have:
what I want:
Here's the code I'm using:
By the way, in this example there are ties, but that won't really be possible in the real thing, so probably not necessary to deal with.
#
# This is a Shiny web application. You can run the application by clicking
# the 'Run App' button above.
#
# Find out more about building applications with Shiny here:
#
# http://shiny.rstudio.com/
#
library(shiny)
# Define UI for application that draws a histogram
ui <- fluidPage(
# Application title
titlePanel("test"),
# Sidebar with a slider input for number of bins
sidebarLayout(
sidebarPanel(
actionButton("random_select",
"Generate Random Numbers",
width = 'auto')
),
# Show a plot of the generated distribution
mainPanel(
tableOutput("results_table_output")
)
)
)
# Define server logic required to draw a histogram
server <- function(input, output) {
counter <- reactiveValues(countervalue = 0)
observeEvent(input$random_select,{
counter$countervalue = counter$countervalue + 1
}
)
results <- reactiveValues(
table = list(trial = NA,
answer =NA,
test_1 = NA,
test_2 = NA,
test_3 = NA)
)
observeEvent(counter$countervalue,{
results$table$trial[counter$countervalue] <- as.integer(counter$countervalue)
results$table$answer[counter$countervalue] <- sample(1:10,1)
results$table$test_1[counter$countervalue] <- sample(1:10,1)
results$table$test_2[counter$countervalue] <- sample(1:10,1)
results$table$test_3[counter$countervalue] <- sample(1:10,1)
})
output$results_table_output <- renderTable({
results$table
})
}
# Run the application
shinyApp(ui = ui, server = server)
Disclaimer
I would also fall back to a more advanced table rendering engine like DT. However, in the following I show another solution which works with renderTable from "base" shiny.
renderTable + JS Solution
If you don't mind using some JavaScript you can use the following snippet:
library(shiny)
library(shinyjs)
js <- HTML("function mark_cells() {
$('.mark-cell').parent('td').css('background-color', 'steelblue');
}
function add_totals() {
const ncols = $('table th').length;
const $col_totals = Array(ncols).fill().map(function(el, idx) {
const $cell = $('<td></td>');
if (idx == 1) {
$cell.text('total:');
} else if (idx > 1) {
$cell.text($('table tr td:nth-child(' + (idx + 1) + ') .mark-cell').length);
}
return $cell;
})
$('table tfoot').remove();
$('table > tbody:last-child')
.after($('<tfoot></tfoot>').append($('<tr></tr>').append($col_totals)));
}
function mark_table() {
mark_cells();
add_totals()
}
")
make_run <- function(i, answer, tests = integer(3)) {
cn <- c("trial", "answer", paste0("test_", seq_along(tests)))
if (is.null(i)) {
line <- matrix(integer(0), ncol = length(cn))
colnames(line) <- cn
} else {
line <- matrix(as.integer(c(i, answer, tests)), ncol = length(cn))
colnames(line) <- cn
}
as.data.frame(line)
}
mark_best <- function(row) {
truth <- row[2]
answers <- row[-(1:2)]
dist <- abs(answers - truth)
best <- dist == min(dist)
answers[best] <- paste0("<span class = \"mark-cell\">", answers[best], "</span>")
c(row[1:2], answers)
}
ui <- fluidPage(
useShinyjs(),
tags$head(tags$script(js)),
sidebarLayout(
sidebarPanel(
actionButton("random_select",
"Generate Random Numbers")
),
mainPanel(
tableOutput("results_table_output")
)
)
)
server <- function(input, output, session) {
results <- reactiveVal(make_run(NULL))
observeEvent(input$random_select, {
res <- results()
results(rbind(res, make_run(nrow(res) + 1, sample(10, 1), sample(10, 3, TRUE))))
})
output$results_table_output <- renderTable({
res <- results()
if (nrow(res) > 0) {
res <- as.data.frame(t(apply(res, 1, mark_best)))
session$onFlushed(function() runjs("mark_table()"))
}
res
}, sanitize.text.function = identity)
}
shinyApp(ui = ui, server = server)
Explanation
In the renderTable function, we call mark_best where we surround the "winning" cells with <span class = "mark-cell">. This helps us on the JS side to identify which cells are the winners.
In order to not escape the HTML in it, we use the argument sanitize.text.function which is responsible for, well, sanitizing strings in the cell. Because we want to print them as is, we supply the identity function.
We include 3 JavaScript functions in the <head> of the document, which
color the parent <td> of our marked cells (mark_cells())
add column totals to the table. This is done by counting the .mark-cell marked cells in each column (add_totals)
a convenience wrapper to call both functions (mark_table())
In order to be able to actually call the JS function we rely on shinyjs. This is however, merely syntactic sugar and could be achieved otherwise as well (if you mind the additional library). To make shinyjs work, we need to include a call to useShinyjs in the UI.
All what is left to do is to call mark_table in the renderTable function. To make sure that the table is rendered properly, we do not call the JS function right away but use session$onFlushed to register the call to be run after the next flush happens.

Testing of shiny modules containing other modules

In a large Shiny App, I have a lot of modules within other modules. These nested modules also sometimes have input controls, e.g. textInput() or actionButton, which trigger certain events also in the parent module.
The following MWE shows the problem.
The module summaryServer prints a summary of a value, but waits for the reactive from rangeServer, which is triggered by a button. I want a Testing specific for summaryServer with testServer() function from Shiny, but how can I "click" the Button in the contained rangeServer module to continue? Is that something about the Mock Shiny Session?
### TESTING ###
x <- reactiveVal(1:10)
testServer(summaryServer, args = list(var = x), {
cat("var active?", d_act(),"\n")
# -----------------------------
# How to click "go" here?
# -----------------------------
cat("var active?", d_act(), "\n")
})
### The app ###
summaryUI <- function(id) {
tagList(
textOutput(NS(id, "min")),
textOutput(NS(id, "mean")),
textOutput(NS(id, "max")),
rangeUI(NS(id, "range"))
)
}
summaryServer <- function(id, var) {
stopifnot(is.reactive(var))
moduleServer(id, function(input, output, session) {
d_act = reactiveVal("Haha nope")
range_val = rangeServer("range", var = var)
# waits to range_val
observeEvent(range_val(),{
d_act("TRUE")
message(range_val())
output$min <- renderText(range_val()[[1]])
output$max <- renderText(range_val()[[2]])
output$mean <- renderText(mean(var()))
})
})
}
rangeUI = function(id) {
textInput(inputId = NS(id, "go"), label = "Go")
}
rangeServer = function(id, var){
moduleServer(id, function(input, output, session){
# when button gets clicked
eventReactive(input$go,{
range(var(), na.rm = TRUE)
}, ignoreInit = TRUE, ignoreNULL = TRUE)
})
}
library(shiny)
ui <- fluidPage(
summaryUI("sum")
)
server <- function(input, output, session) {
x = reactiveVal(1:10)
summaryServer("sum", x)
}
# shinyApp(ui, server)
That is a tricky one. It works if you set both ignoreInit and ignoreNULL to FALSE but just because then you are not initially dependent on a change of go anymore, which is undesirable.
I do not think it is possible to change go inside of rangeServer when running testServer with summaryServer. You can however use {shinytest} to achieve this. Note that here you invoke and test the entire app. Therefore, when using modules, you have to call elements by their complete id, including namespaces.
(I changed go to an actionButton, everything else stays the same)
rangeUI <- function(id) {
actionButton(inputId = NS(id, "go"),label = "Go")
}
test_that("output updates when reactive input changes", {
# invoke app
app <- shinytest::ShinyDriver$new("app.R")
# initially, the button has`nt been clicked and the outputs are empty
testthat::expect_equal(app$getValue("summary-range-go"), 0)
testthat::expect_equal(app$getValue("summary-min"), "")
# click the button
app$click("summary-range-go")
testthat::expect_equal(app$getValue("summary-range-go"), 1)
# testthat::expect_equal(app$getValue("summary-min"), "1")
# for some reason, the button value increased, hence is clicked,
# but the outputs have not been triggered yet.
# another click fixes that
app$click("summary-range-go")
testthat::expect_equal(app$getValue("summary-min"), "1")
})

RenderUI with conditional selectInput that dynamically builds more selectInputs in Shiny

I am trying to build a Shiny interface with:
a main selector, which decides:
which submenu (input) to show, which decides:
how many subsequent inputs to show
Here's a minimal reproducible example.
If "First" is chosen from the main selector, then a submenu with two possibilities [1,2] exist. These possibilities result in 1 or 2 subsequent inputs being built. So these possibilities:
If "Second" is chosen from the main selector, then a submenu with two possibilities [3,4] exist. These possibilities result in 3 or 4 subsequent inputs being built.
ui <- fluidPage(
radioButtons(inputId="main_selector",label=h5('Select menu'),
choices = list('First','Second'),selected='First'),
uiOutput("ui_selected")
)
server <- function(input, output, session) {
build_inputs <- function(choices){
output = tagList()
for(i in 1:choices){
output[[i]] = tagList()
output[[i]][[1]] = numericInput(inputId = paste0(i),
label = paste0(i),
value = i)
}
}
# Are these reactive elements necessary? Should they be in the renderUI below?
first_submenu <- reactive({
input$first_submenu
})
second_submenu <- reactive({
input$second_submenu
})
output$ui_selected <- renderUI({
if (input$main_selector == 'First'){
selectInput(inputId = "first_submenu", label="First submenu",
choices=list(1,2))
choices_1 <- first_submenu()
# Build a list of inputs dependent on the choice above
output <- build_inputs(choices_1)
} else if (input$main_selector == 'Second'){
selectInput(inputId = "second_submenu", label="Second submenu",
choices=list(3,4))
choices_2 <- second_submenu()
# Build a list of inputs dependent on the choice above
output <- build_inputs(choices_2)
# Return output as output$ui_selected element
output
})
}
shinyApp(ui, server)
The error I receive is Warning: Error in :: argument of length 0. I believe this is because you can't call the outcome of first_submenu from the renderUI element - but I don't know how to structure my code correctly.
I am not sure whether this is what you are after. The main problem was that your function build_inputs does not return anything. The second problem is that choices from selectInput are not numeric, so you need to convert them beforehand. And one other minor problem, related to the error you mention, is that the elements you want to render exist at the same time, so putting a condition on input$first_submenu will trigger errors (even if it is NULL for a couple of milliseconds), so it's (almost always) good practice to take care of possibly null inputs. The last thing I did was to add another uiOutput for the last layer of dynamic inputs. Hope this helps.
ui <- fluidPage(
radioButtons(inputId="main_selector",label=h5('Select menu'),
choices = list('First','Second'),selected='First'),
uiOutput("ui_selected"),
uiOutput("ui_numeric_inputs")
)
server <- function(input, output, session) {
build_inputs <- function(choices) {
output = tagList()
for(i in 1:choices){
output[[i]] = tagList()
output[[i]][[1]] = numericInput(inputId = paste0(i),
label = paste0(i),
value = i)
}
return(output)
}
output$ui_selected <- renderUI({
if (input$main_selector == 'First'){
selectInput(inputId = "first_submenu", label="First submenu",
choices=c(1,2))
} else if (input$main_selector == 'Second'){
selectInput(inputId = "second_submenu", label="Second submenu",
choices=list(3,4))
}
})
output$ui_numeric_inputs <- renderUI({
if (input$main_selector == 'First' &&
(!is.null(input$first_submenu))) {
build_inputs(as.numeric(input$first_submenu))
} else if (input$main_selector == 'Second' &&
(!is.null(input$second_submenu))){
build_inputs(as.numeric(input$second_submenu))
}
})
}
shinyApp(ui, server)

R-Shiny UI idiom for multi-select input with number pair

I need to allow the user to select some widgets from a fixed set of widgets and then enter a quantity for each widget he has selected.
selectInput("widgets","Widgets",choices = widgets_list,multiple = TRUE)
How can I show a set of Numeric Entry boxes dynamically, one for each item selected by the user in the multi-select box above?
Eventually I want to end up with some structure like:
data.frame(widgets=c("Widget1","Widget2","Widget3"),quantities=c(23,34,23))
Any thoughts on how best to implement this?
Here is a toy program that does what you want - I think.
It uses a reactiveValues to declare a pair of vectors that you can then be changed reactively. It uses renderUI and uiOutput to render new input devices as the underlying data changes. It also uses renderDataTable to show you the data table that is being created.
library(shiny)
widgets_list = c("Widget1","Widget2","Widget3")
widgets_quan = c(23,34,23)
u <- shinyUI(fluidPage(
titlePanel("Shiny Widgets Input"),
sidebarLayout(position = "left",
sidebarPanel(h3("sidebar panel"),
uiOutput("widgname"),
uiOutput("widgquan")
),
mainPanel(h3("main panel"),
dataTableOutput("dataframe")
)
)))
s <- shinyServer(function(input,output) {
rv <- reactiveValues(wname = widgets_list,wquan = widgets_quan)
observeEvent(input$widgquan, {
rv$wquan[ which(rv$wname==input$widget) ] <- input$widgquan
})
output$widgname <- renderUI({
selectInput("widget","Widget",choices = rv$wname)
})
output$widgquan <- renderUI({
req(input$widget)
n <- rv$wquan[which(rv$wname == input$widget)]
numericInput("widgquan","Quantity:",n)
})
widgdata <- reactive({
req(input$widgquan)
df <- data.frame(Widgets = rv$wname,Quantity = rv$wquan)
})
output$dataframe <- renderDataTable({ widgdata() })
})
shinyApp(ui = u,server = s)
yielding:

Global variable in ShinyServer

I just started hacking around with Shiny a few days ago.
In my little toy app, the user types a list of comma-separated numbers into a text area and presses the submit button. It then calculates and displays the sum, mean and median of those numbers.
The shinyServer functions that calculate the sum, mean and median all call a function named my_array() that uses strsplit to separate the numbers at the commas and returns a numeric list.
Rather than call my_array() three times, I'd like to call my_array() once after the Submit button has been clicked and save the result globally. Then I'll use that global list to calculate sum, mean and median.
Can someone clue me in as to how I can call my_array() once after submit is hit, and save the result in a global variable? Nothing I'm trying works, and the examples I've seen don't seem to address what I need.
Thanks.
server.R
shinyServer(function(input, output) {
my_array <- reactive ({
number_array <- strsplit(input$text, ",")
as.numeric(number_array[[1]])
})
my_sum <- reactive({
sum(my_array())
})
my_mean <- reactive({
val <- mean(my_array(), na.rm=TRUE)
if (is.nan(val)) {
val = ""
} else {
val
}
})
my_median <- reactive({
val <- median(my_array(), na.rm=TRUE)
if (is.na(val)) {
val = ""
} else {
val
}
})
output$sum <- renderText({ my_sum() })
output$mean <- renderText({ my_mean() })
output$median <- renderText({ my_median() })
})
ui.R
shinyUI(fluidPage(
titlePanel("Average Calculator"),
tags$style(type="text/css", "textarea {width:100%}"),
tags$textarea(id = 'text', placeholder = 'Enter comma-separated numbers here', rows = 8, ""),
submitButton("Submit"),
hr(),
fluidRow(column(2, strong("Sum:"), align="right"), column(3, textOutput("sum"))),
fluidRow(column(2, strong("Mean:"), align="right"), column(3, textOutput("mean"))),
fluidRow(column(2, strong("Median:"), align="right"), column(3, textOutput("median"))),
))
As #zero323 said, the result is already cached, but if you really want to, you can use observeEvent() to fire off when you press the button:
observeEvent(input$your_button_id, {
number_array <- strsplit(input$text, ",")
as.numeric(number_array[[1]])
})
This will fire only once every time you press the button. You might also want to check for NULL or "" variables.

Resources