I want to create a Shiny gadget using miniUI which dynamically can add additional tabs to a miniTabstripPanel. However, no panels have shown up in my experiments when I try it using renderUI and uiOutput. I think it has to do with the height of the elements not being set correctly (by me).
Anyway, below is a minimal example. If I use the uiOutput() line, it does not work. If I instead comment that out and use the miniTabPanel() line directly, which is the exact code used in renderUI(), it works fine.
library(shiny)
library(miniUI)
TSTAddin <- function() {
panno <- 0
ui <- miniPage(
actionButton('btn_newPan', 'New Panel'),
miniTabstripPanel(
# miniTabPanel('pan1', miniContentPanel(height='100%', actionButton('Test','Test'))), miniTabPanel('pan2', miniContentPanel(height='100%', actionButton('Test','Test')))
uiOutput('panelset')
)
)
server <- function(input, output, session) {
output$panelset <- renderUI({
panno <<- panno+1
txt <- paste0("miniTabPanel('pan", 1:panno, "', miniContentPanel(height='100%', actionButton('Test','Test')))", collapse=", ")
list( eval(parse(text=txt)) )
})
}
viewer <- paneViewer(300)
runGadget(ui, server, viewer = viewer)
}
TSTAddin()
EDIT:
The code suggested by mkemp6 solves the problem. However, if I now include a button on the panel, only part of it is shown. How can one correct the size of the dynamically generated panels? An example adopting mkemp6's code follows.
library(shiny)
library(miniUI)
TSTAddin2 <- function() {
ui <- miniPage(
actionButton("nTabs", "More Tabs"),
actionButton("neg_nTabs", "Less Tabs"),
uiOutput('panelset')
)
server <- function(input, output, session) {
output$panelset <- renderUI({
n <- seq(max(input$nTabs - input$neg_nTabs + 1, 1))
miniTabList <- lapply(paste("Pan", n), function(x) miniTabPanel(x, actionButton('Test','Test')))
do.call(miniTabstripPanel, miniTabList)
})
}
viewer <- paneViewer(300)
runGadget(ui, server, viewer = viewer)
}
TSTAddin2()
Try this:
library(shiny)
library(miniUI)
TSTAddin <- function() {
ui <- miniPage(
actionButton("nTabs", "More Tabs"),
actionButton("neg_nTabs", "Less Tabs"),
uiOutput('panelset')
)
server <- function(input, output, session) {
output$panelset <- renderUI({
n <- seq(max(input$nTabs - input$neg_nTabs + 1, 1))
miniTabList <- lapply(paste("Pan", n), miniTabPanel)
do.call(miniTabstripPanel, miniTabList)
})
}
viewer <- paneViewer(300)
runGadget(ui, server, viewer = viewer)
}
TSTAddin()
Regarding your second issue with the size of the panels:
Whenever you use uiOutput(), shiny puts another div tag around the rendered content. As it appears, the style of this content is by default not compatible with the miniUI elements. However, this can be corrected by adding some specific CSS code (which I have basically copied from some other classes of miniUI.css).
Also, when you put several elements (in your case: action buttons) among each other within a miniContenPanel, it is advisable to create separate containers for each of them. You can easily do that using fillCol / fillRow as described in http://shiny.rstudio.com/articles/gadget-ui.html.
This code works with me (you may want to further improve the layout):
library(shiny)
library(miniUI)
TSTAddin2 <- function() {
ui <- miniPage(
tags$head(
tags$style(HTML("
.ui-output-mini {position: absolute;top:0;right:0;bottom:0;left:0;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;}
"))
),
fillCol(flex=c(NA, NA, 1),
actionButton("nTabs", "More Tabs", width='100%'),
actionButton("neg_nTabs", "Less Tabs", width='100%'),
uiOutput('panelset', class='ui-output-mini')
)
)
server <- function(input, output, session) {
output$panelset <- renderUI({
n <- seq(max(input$nTabs - input$neg_nTabs + 1, 1))
miniTabList <- lapply(paste("Pan", n), function(x) miniTabPanel(x, actionButton('Test','Test')))
do.call(miniTabstripPanel, miniTabList)
})
}
viewer <- paneViewer(300)
runGadget(ui, server, viewer = viewer)
}
TSTAddin2()
Related
The code at the bottom is taken from an example in https://shiny.rstudio.com/articles/modules.html though I de-modularized it so I can understand something more basic. With this code, each click of the action button renders a counter which counts the number of clicks. Fine.
Instead of counting the number of clicks in the same output of verbatimTextOutput() as the code currently works, I'd like each click to be represented as a new output of verbatimTextOutput(). See illustration below which shows what I'm trying to derive, assuming the user clicks the action button 3 times. I don't know how many times the user will click the action button so there's no way to pre-set or hard-code the number of outputs and assign output names such as output$out1, output$output2, etc. Is there a way to reactively generate the outputs names, as a I naively attempted in the below code with output$"paste(out,count())" and verbatimTextOutput("paste(out,count())") (without the quote marks, I only put them in so the example code would work)? If this is possible this could be a way to achieve the results I am seeking.
Illustration:
Code:
library(shiny)
newOutput <- function(x,y){verbatimTextOutput("paste(out,count())")}
ui <- fluidPage(uiOutput("uiButton"))
server <- function(input,output,session){
count <- reactiveVal(0)
observeEvent(input$button, {count(count() + 1)})
output$"paste(out,count())" <- renderText({count()})
count
output$uiButton <-
renderUI(
tagList(
actionButton("button", label = "Click me"),
newOutput()
)
)
}
shinyApp(ui, server)
This is an alternative approach using insertUI.
Compared to #stefan's renderUI based solution it has the advantage, that the UI elements are rendered only once. Using renderUI every element is re-rendered on button click, accordingly things will slow down depending on the number of elements.
library(shiny)
ui <- fluidPage(
actionButton("add", "Add UI")
)
server <- function(input, output, session) {
observeEvent(input$add, {
output_name <- paste0("out_", input$add)
output[[output_name]] <- renderText({
isolate(input$add)
})
insertUI(
selector = ifelse(input$add == 0L, "#add", paste0("#", "out_", input$add-1)),
where = "afterEnd",
ui = verbatimTextOutput(output_name)
)
}, ignoreNULL = FALSE)
}
shinyApp(ui, server)
Also check ?removeUI.
Adapting this example to dynamically create graphs to your example you could do:
library(shiny)
library(purrr)
newOutput <- function(x) {
verbatimTextOutput(x)
}
ui <- fluidPage(
actionButton("button", label = "Click me"),
uiOutput("uiText")
)
server <- function(input, output, session) {
count <- reactiveVal(0)
observeEvent(input$button, {
count(count() + 1)
i <- count()
output_name <- paste("out", i)
output[[output_name]] <- renderText({
i
})
})
output$uiText <- renderUI({
out_list <- map(seq_len(count()), ~ {
tagList(
newOutput(paste("out", .x))
)
})
tagList(out_list)
})
}
shinyApp(ui, server)
I'm trying to create a plot with a bunch of boxes and then when a box gets clicked on it gets colored in up. I'm having two issues with this. 1. I can't figure out a way for the figure to update dynamically when I click. 2. I can't figure out how to store the values that come out of the click input variable so that I have stored all previous clicks and would be able to color in multiple boxes. You can see a few ways I've tried to solve and test either of the two issues and I'm not having any luck. Any help with either issue would be appreciated.
ui <- fluidPage(
# Application title
titlePanel("Boxes"),
sidebarLayout(
sidebarPanel(
textOutput("text")),
# Get it it's a pun
mainPanel(
plotOutput("boxPlot",click = "test")
)
)
)
# Define server logic required to draw a histogram
server <- function(input, output) {
vals <- reactiveValues(x=NA,y=NA,test=NA)
observeEvent(input$click, {
vals$x <- c(vals$x,input$test$x)
vals$y <- c(vals$y,input$test$y)
vals$test <- input$test$x
})
output$boxPlot <- renderPlot({
par(mai=c(0,0,0,0))
plot(1,ylim=c(2,15),xlim=c(2,15),type='n',yaxs='i',xaxs='i',ylab='',xlab='',axes=F)
for (i in 2:15) {
abline(v=i)
abline(h=i)
}
observeEvent(input$click, { rect(floor(input$test$x),floor(input$test$y),ceiling(input$test$x),ceiling(input$test$y),col='blue')})
# if (length(vals$x) > 0) {
# rect(floor(vals$x[1]),floor(vals$y[1]),ceiling(vals$x[1]),ceiling(vals$y[1]),col='blue')
# }
})
# output$text <- renderText(vals$x[length(vals$x)])
output$text <- renderText(vals$test)
}
# Run the application
shinyApp(ui = ui, server = server)
This might be a simple solution.
You should only have one single observeEvent for your click event. In that observeEvent, store your x and y values as reactiveValues as you current are doing.
Then, your renderPlot should plot the grid lines and filled in rectangles based on your reactiveValues. By including input$boxPlot_click (as I called it) in renderPlot the plot will be redrawn each click.
library(shiny)
ui <- fluidPage(
titlePanel("Boxes"),
sidebarLayout(
sidebarPanel(
textOutput("text")),
mainPanel(
plotOutput("boxPlot", click = "boxPlot_click")
)
)
)
server <- function(input, output) {
vals <- reactiveValues(x=NA,y=NA)
observeEvent(input$boxPlot_click, {
vals$x <- c(vals$x,input$boxPlot_click$x)
vals$y <- c(vals$y,input$boxPlot_click$y)
})
output$boxPlot <- renderPlot({
input$boxPlot_click
par(mai=c(0,0,0,0))
plot(1,ylim=c(2,15),xlim=c(2,15),type='n',yaxs='i',xaxs='i',ylab='',xlab='',axes=F)
for (i in 2:15) {
abline(v=i)
abline(h=i)
}
for (i in seq_along(length(vals$x))) {
rect(floor(vals$x),floor(vals$y),ceiling(vals$x),ceiling(vals$y),col='blue')
}
})
output$text <- renderText(paste0(vals$x, ', ' , vals$y, '\n'))
}
shinyApp(ui = ui, server = server)
I am trying to create a Shiny App which can be used in the R workspace to create a user friendly front end to some code- so that someone can just type in and click some boxes instead of creating lists and dataframes themselves- and then what they input will be stored in the workspace in R to do the code. I have basically adapted someone else's code but can't work out how I save the dynamically created UI called col - which makes text inputs so if people type something in this is saved.
When I try to add some way of saving it I get an error Operation not allowed without an active reactive context. (You tried to do something that can only be done from inside a reactive expression or observer.). The code is below, is there a way I can save the information from the text input?
CrossBreakUI <- function(id, number) {
ns <- NS(id)
tagList(
fluidRow(
column(4, numericInput(ns("n"), "Number of Groups in Cross-Break", value=5, min=1), uiOutput(ns("col")))
)
)
}
variables <- function(input, output, session, variable.number){
output$textInput <- renderUI({
ns <- session$ns
textInput(ns("textInput"),
label = "")
})
col_names <- reactive(paste0("col", seq_len(input$n)))
output$col <- renderUI({
ns <- session$ns
map(col_names(), ~ textInput(ns(.x), NULL))
})
reactive({
# Create Pair: variable and its value
df <- data.frame(
"variable.number" = variable.number,
stringsAsFactors = FALSE
)
})
}
ui <- fixedPage(
div(
CrossBreakUI("var1", 1)
))
server <- function(input, output) {
add.variable <- reactiveValues()
add.variable$df <- data.frame(
"n" = numeric(0),
"col" = character(0),
stringsAsFactors = FALSE
)
var1 <- callModule(variables, paste0("var", 1), 1)
observeEvent(input[[NS(paste0("var", 1), "n")]], {
add.variable$df[1,1] <- input[[NS(paste0("var", 1), "n")]]
})
**#THIS IS THE ERROR- IT DOES NOT SAVE THE TEXT INPUT FROM THIS VARIABLE**
observeEvent(input[[NS(paste0("var", 1), "col")]], {
add.variable$df[1,2] <- input[[NS(paste0("var", 1), "col")]]
})
observe({
assign(x ="CrossBreak", value=add.variable$df, envir= .GlobalEnv) })
}
Second revision
If my understanding is correct, I think this gets close to what you want. You have a numericInput. The UI presents a series of textInputs. The number of textInputs changes in response to changes in the numericInput's value. The values of the textInputs are saved to a variable in the global environment (and the value of this global variable is printed to the console as the app terminates). Values already entered in the textInputs are preserved when the UI updates.
My solution differs from yours in that you have one module attempting to control every textInput and a main server that attempts to interrogate each textInput to obtain its value. I use multiple instances of a single module, one for each textInput. Each module instance manages the persistence of its textInput's value independently of all the other instances.
library(shiny)
groupList <- list()
# Module to define a self-contained "write-my-value" textInput
writeMyValueTextInputUI <- function(id, idx) {
ns <- NS(id)
textInput(ns("groupName"), paste0("Group ", idx))
}
writeMyValueTextInput <- function(input, output, session, id) {
ns <- session$ns
# Initialise
observe ({
id <- as.numeric(id)
if (id <= length(groupList)) updateTextInput(session, "groupName", value=groupList[[id]])
})
observeEvent(input$groupName, {
req(input$groupName)
# Note global assignment
groupList[[id]] <<- input$groupName
})
rv <- reactive ({
input$groupName
})
return(rv)
}
ui <- fluidPage(
titlePanel("Crossbreak demo"),
sidebarLayout(
sidebarPanel(
numericInput("groupCount", "Number of groups in cross-break:", min=1, value=5),
),
mainPanel(
textOutput("groupCount"),
uiOutput("groupList")
)
)
)
server <- function(input, output, session) {
onStop(function() cat(paste0(groupList, collapse=", ")))
ns <- session$ns
controllers <- list()
output$groupList <- renderUI({
req(input$groupCount)
textInputs <- lapply(
1:input$groupCount,
function(x) {
id <- ns(paste0("text", x))
controllers[[x]] <- callModule(writeMyValueTextInput, id, x)
return(writeMyValueTextInputUI(id, x))
}
)
do.call(tagList, textInputs)
})
}
shinyApp(ui = ui, server = server)
=========================
I haven't tried running your code (it's not really a simple self-contained example), but the following is just one way of running an app from the console. (is that what you mean when you say "from the global environment?)...
myList <- list(
ui = bootstrapPage(
numericInput('n', 'Number of obs', 100),
plotOutput('plot')
),
server = function(input, output) {
output$plot <- renderPlot({ hist(runif(input$n)) })
}
)
if (interactive()) runApp(myList)
I based my code on this page which also has other examples...
Note that you can't do this if you're running an R script in a batch job, as the batch job has no context in which to display the app. Hence if (interactive())...
OK. Responding to OP's clarification, here's a really crude demonstraion of one way of doing what she wants. Note the use of the global assignment operator (<<-) in the observeEvent.
x <- NA
print(paste0("globalValue is currently: ", x))
myList <- list(
ui = bootstrapPage(
numericInput('n', 'Please give me a number', 100)
),
server = function(input, output) {
observeEvent(input$n, {x <<- input$n})
}
)
if (interactive()) runApp(myList)
print(paste0("globalValue is now: ", x))
On my system, stepping through these statements in the console gives:
> x <- NA
> print(paste0("globalValue is currently: ", x))
[1] "globalValue is currently: NA"
> myList <- list(
+ ui = bootstrapPage(
+ numericInput('n', 'Please give me a number', 100)
+ ),
+ server = function(input, output) {
+ observeEvent(input$n, {x <<- input$n})
+ }
+ )
> if (interactive()) runApp(myList)
Listening on http://127.0.0.1:4429
> print(paste0("globalValue is now: ", x))
[1] "globalValue is now: 104"
>
Obviously, this isn't a realistic production solution. Possible solutions might include:
Writing to a temporary Rds file in the app and then reading it in once the app terminates.
Using session$userData to store the required information whilst the app is running and then using onStop to do custom processing as the app terminates.
I'm sure there will be others.
[OP: As an aside, look at the length of my code compared to yours. Put yourself in the shoes of someone who's willing to provide solutions. Whose question are they most likely to answer: yours or mine? Providing compact, relevant code makes it far more likely you'll get a useful reply.]
I want to add a download link in each row of a datatable in shiny.
So far I have
server <- function(input, output) {
v<-eventReactive(input$button,{
temp<-data.frame(TBL.name=paste("Data ",1:10))
temp<-cbind(
temp,
#Dynamically create the download and action links
Attachments=sapply(seq(nrow(temp)),function(i){as.character(downloadLink(paste0("downloadData_",i),label = "Download Attachments"))})
)
})
# Table of selected dataset ----
output$table <- renderDataTable({
v()
}, escape = F)}
ui <- fluidPage(
sidebarPanel(
actionButton("button", "eventReactive")
),
mainPanel(
dataTableOutput("table")
)
)
I have the download links in the table for each row. Now I want to add a different file location for each row. For example, each download link will result in a download of a different zip-folder. Can I use downloadHandler for this?
I do not believe you can embed downloadButtons/downloadLinks directly in a datatable. However, you can create hidden downloadLinks that get triggered by links embedded in your table. This produces the same end result. To do so you must:
Dynamically generate downloadLinks/downloadButtons.
Use css to set their visibility to hidden.
Embed normal links/buttons in the table
Set the onClick field of these links to trigger the corresponding hidden downloadLink.
Here is code from an example using the mtcars dataset.
library(tidyverse)
library(shiny)
ui <- fluidPage(
tags$head(
tags$style(HTML("
.hiddenLink {
visibility: hidden;
}
"))
),
dataTableOutput("cars_table"),
uiOutput("hidden_downloads")
)
server <- function(input, output, session) {
data <- mtcars
lapply(1:nrow(data), function(i) {
output[[paste0("downloadData", i)]] <- downloadHandler(
filename = function() {
paste("data-", i, ".csv", sep="")
},
content = function(file) {
write.csv(data, file)
}
)
})
output$hidden_downloads <- renderUI(
lapply(1:nrow(data), function(i) {
downloadLink(paste0("downloadData", i), "download", class = "hiddenLink")
}
)
)
output$cars_table <- renderDataTable({
data %>%
mutate(link = lapply(1:n(),
function(i)
paste0('<a href="#" onClick=document.getElementById("downloadData',i, '").click() >Download</a>')
))
}, escape = F)
}
shinyApp(ui, server)
Since each downloadLink label must correspond to a name in output, I don't think there is a way to create an arbitrary set of downloads using the standard Shiny download* functions.
I solved this using DT and javascript. DT allows javascript to be associated with a datatable. The javascript can then tell Shiny to send a file to the client and the client can force the data to be downloaded.
I created a minimal example gist. Run in RStudio with:
runGist('b77ec1dc0031f2838f9dae08436efd35')
Safari is not supporting .click() anymore since v12.0. Hence, I adapted the hidden link solution from abanker with the dataTable/actionButton described by P Bucher, and the .click() workaround described here. Here is the final code:
library(shiny)
library(shinyjs)
library(DT)
# Random dataset
pName <- paste0("File", c(1:20))
shinyApp(
ui <- fluidPage( useShinyjs(),
DT::dataTableOutput("data"),
uiOutput("hidden_downloads") ),
server <- function(input, output) {
# Two clicks are necessary to make the download button to work
# Workaround: duplicating the first click
# 'fClicks' will track whether click is the first one
fClicks <- reactiveValues()
for(i in seq_len(length(pName)))
fClicks[[paste0("firstClick_",i)]] <- F
# Creating hidden Links
output$hidden_downloads <- renderUI(
lapply(seq_len(length(pName)), function(i) downloadLink(paste0("dButton_",i), label="")))
# Creating Download handlers (one for each button)
lapply(seq_len(length(pName)), function(i) {
output[[paste0("dButton_",i)]] <- downloadHandler(
filename = function() paste0("file_", i, ".csv"),
content = function(file) write.csv(c(1,2), file))
})
# Function to generate the Action buttons (or actionLink)
makeButtons <- function(len) {
inputs <- character(len)
for (i in seq_len(len)) inputs[i] <- as.character(
actionButton(inputId = paste0("aButton_", i),
label = "Download",
onclick = 'Shiny.onInputChange(\"selected_button\", this.id, {priority: \"event\"})'))
inputs
}
# Creating table with Action buttons
df <- reactiveValues(data=data.frame(Name=pName,
Actions=makeButtons(length(pName)),
row.names=seq_len(length(pName))))
output$data <- DT::renderDataTable(df$data, server=F, escape=F, selection='none')
# Triggered by the action button
observeEvent(input$selected_button, {
i <- as.numeric(strsplit(input$selected_button, "_")[[1]][2])
shinyjs::runjs(paste0("document.getElementById('aButton_",i,"').addEventListener('click',function(){",
"setTimeout(function(){document.getElementById('dButton_",i,"').click();},0)});"))
# Duplicating the first click
if(!fClicks[[paste0("firstClick_",i)]])
{
click(paste0('aButton_', i))
fClicks[[paste0("firstClick_",i)]] <- T
}
})
}
)
I've got an issue with my current shiny code.
I have to generate a dynamic number of tabs depending on the results of a given function (that part works fine). Then, I want to generate the input of these tabs in other loops of for example renderText. However, the final output of the textOutput for my generated renderText is always the one of the last renderText of the loops.
Here is a small example of the idea:
library(shiny)
library(shinydashboard)
ui <- pageWithSidebar(
headerPanel("xxx"),
sidebarPanel(),
mainPanel(
uiOutput("multipleUI")
)
)
server <- function(input, output) {
output$multipleUI <- renderUI({
tabs <- list(NULL)
for(i in 1:5){
tabs[[i]] <- tabPanel(title = paste0("tab ",i),
textOutput(paste0("out",i)), # prints j as 5 in all tabs
paste0("local out ",i)) # prints i as current loop value for each tab)
}
do.call(tabBox,tabs)
})
observe({
for(j in 1:5){
txt = paste0("generated out ", j)
print(txt) # print with current j
output[[paste0("out",j)]] <- renderText({txt})
}
})
}
shinyApp(ui, server)
While it might not be that important for renderText where I can just work around the issue, I intend to render a lot of plots and tables and couldn't think of a workaround there.
I'd appreciate any help!
EDIT: I've updated the code to show a small working example
Here's a solution that seems to work. I'm using lapply to create the tabs. Let me know if it works for what you need.
library(shiny)
ui <- pageWithSidebar(
headerPanel("xxx"),
sidebarPanel(),
mainPanel(
do.call(tabsetPanel, c(id='tab',lapply(1:5, function(i) {
tabPanel(
title=paste0('tab ', i),
textOutput(paste0('out',i))
)
})))
)
)
server <- function(input, output) {
lapply(1:5, function(j) {
output[[paste0('out',j)]] <- renderPrint({
paste0('generated out ', j)
})
})
}
shinyApp(ui, server)