I want to dinamically generate plots using shiny, but each plot with a different title.
I have tried using the for bucle to generate n number of plots and show them using an observeEvent, but this is not working for me, as the main of the plot is ignored.
To ensure that each plot has its own main title, what I do is to store the title into a data.frame and access to it from the plot.
Here the code:
library(shiny)
ui <- fluidPage(
textInput("title","Title",""),
actionButton("generate","Plot"),
div(class="aux",style="width:300px;height:200px")
)
server <- function(input,output){
observeEvent(input$generate,{
insertUI(
selector= ".aux",
where="beforeBegin",
ui = plotOutput(paste0("plot",input$generate))
)
if(input$generate == 1){
data <<- data.frame(title = input$title)
}else{
aux <- data.frame(title=input$title)
data <<- rbind(data,aux)
}
})
for(i in 1:10){
output[[paste0("plot",i)]] <- renderPlot(
plot(rnorm(100),main=data[i,"title"])
)
}
}
shinyApp(ui,server)
This other code do what I really want to do, but it is not good programing to declare manually the plots:
library(shiny)
ui <- fluidPage(
textInput("title","Title",""),
actionButton("generate","Plot"),
div(class="aux",style="width:300px;height:200px")
)
server <- function(input,output){
observeEvent(input$generate,{
insertUI(
selector= ".aux",
where="beforeBegin",
ui = plotOutput(paste0("plot",input$generate))
)
if(input$generate == 1){
data <<- data.frame(title = input$title)
}else{
aux <- data.frame(title=input$title)
data <<- rbind(data,aux)
}
})
output$plot1 <- renderPlot(
plot(rnorm(100),main=data[1,"title"])
)
output$plot2 <- output$plot1 <- renderPlot(
plot(rnorm(100),main=data[2,"title"])
)
}
shinyApp(ui,server)
EDITED:
Using the recomendations of Stephane Laurent, I have put the insertUI and output[[plot]] inside the observeEvent, but this not solves the issue to be able to edit the plot title changing the data.frame title. Here the code:
library(shiny)
library(data.table)
ui <- fluidPage(
column(6,
textInput("title","Title",""),
actionButton("generate","Plot"),
div(id="aux")),
column(6,
textInput("newt","New title",""),
selectInput("row","Row",choices=c(1:10)),
actionButton("change","Change title"))
)
server <- function(input,output){
observeEvent(input$change,{
df$title <<- as.character(df$title)
df[input$row,"title"]<-input$newt
})
k <- 0
observeEvent(input$generate, {
insertUI(
selector= "#aux",
where="beforeBegin",
ui = plotOutput(paste0("plot",input$generate))
)
k <- k + input$generate
if(input$generate==1){
df <<- data.frame(title = input$title)
df$title <<- as.character(df$title)
}else{
aux <- data.frame(title = input$title)
df <<- rbind(df,aux)
df$title <<- as.character(df$title)
}
output[[paste0("plot",input$generate)]] <- renderPlot(
plot(rnorm(100), main = df[k,"title"])
)
})
}
shinyApp(ui,server)
Put the renderPlot inside the observer:
library(shiny)
ui <- fluidPage(
textInput("title","Title",""),
actionButton("generate","Plot"),
div(id="aux")
)
server <- function(input,output){
observeEvent(input$generate, {
insertUI(
selector= "#aux",
where="beforeBegin",
ui = plotOutput(paste0("plot",input$generate))
)
output[[paste0("plot",input$generate)]] <- renderPlot(
plot(rnorm(100), main = isolate(input$title))
)
})
}
shinyApp(ui,server)
EDIT
Solution for the edited question:
library(shiny)
ui <- fluidPage(
column(6,
textInput("title","Title",""),
actionButton("generate","Plot"),
div(id="aux")),
column(6,
textInput("newt","New title",""),
selectInput("row","Row",choices=c(1:10)),
actionButton("change","Change title"))
)
server <- function(input,output){
titles <- reactiveValues()
observeEvent(input$change, {
titles[[input$row]] <- input$newt
})
values <- replicate(10, rnorm(100))
for(i in 1:10){
local({
ii <- i
output[[paste0("plot",ii)]] <- renderPlot(
plot(values[,ii], main = titles[[as.character(ii)]])
)
})
}
observeEvent(input$generate, {
titles[[as.character(input$generate)]] <- input$title
insertUI(
selector = "#aux",
where = "beforeBegin",
ui = plotOutput(paste0("plot",input$generate))
)
})
}
shinyApp(ui,server)
Related
I want to do operations on data that has been split into tables. The operations should actually affect all tables eg sum of a column
Here is the code I used to split the data frame.
library(shiny)
ui <- fluidPage(
uiOutput("mytabs")
)
server <- function(input, output) {
df1 <- reactive (split(iris, iris$Species))
output$mytabs <- renderUI({
thetabs <- lapply(paste0('table_', names(df1())),
function(x) {
tabPanel(x,
tableOutput(x))
})
do.call(tabsetPanel, thetabs)
})
observe({
lapply(names(df1()), function(x) {
output[[paste0("table_", x)]] <- renderTable({ df1()[x] })
})
})
}
shinyApp(ui = ui, server = server)
We can add a bslib::value_box in the same tabPanel that the tableOutput goes.
Here's an example, notice the use of map2 instead of lapply, this is to loop through the character with the name of the tables and the tables themselves.
thetabs <- purrr::map2(
paste0("table_", names(df1())),
df1(),
function(x, y) {
tabPanel(
title = x,
value_box(
title = glue::glue("Sum of {x}"),
value = sum(y[['Sepal.Length']]),
showcase = bs_icon("plus")
),
tableOutput(x)
)
}
)
App:
library(shiny)
library(bslib)
library(bsicons)
ui <- fluidPage(
uiOutput("mytabs")
)
server <- function(input, output) {
df1 <- reactive(split(iris, iris$Species))
output$mytabs <- renderUI({
thetabs <- purrr::map2(
paste0("table_", names(df1())),
df1(),
function(x, y) {
tabPanel(
title = x,
value_box(
title = glue::glue("Sum of {x}"),
value = sum(y[['Sepal.Length']]),
showcase = bs_icon("plus")
),
tableOutput(x)
)
}
)
do.call(tabsetPanel, thetabs)
})
observe({
lapply(names(df1()), function(x) {
output[[paste0("table_", x)]] <- renderTable({
df1()[x]
})
})
})
}
shinyApp(ui = ui, server = server)
Does Shiny can detect only common R’s objects? If yes, What objects can it observe?
For example, I tried many options with no success to detect a data.tree changes in shiny.
Does anyone know why this happens?
library(shiny)
library(data.tree)
data(acme)
ui <- fluidPage(
actionButton("go", "go" ),
tags$h2("text"),
verbatimTextOutput("text"),
tags$h2("text0"),
verbatimTextOutput("text0"),
tags$h2("text1"),
verbatimTextOutput("text1"),
tags$h2("text2"),
verbatimTextOutput("text2"),
tags$h2("text3"),
verbatimTextOutput("text3"),
tags$h2("text4"),
verbatimTextOutput("text4")
)
server <- function(input, output, session) {
anum <- reactiveValues(a = 0)
a <- reactiveValues(acme = acme, f = NULL)
b <- reactiveVal(acme)
cc <- reactive(a$acme)
observeEvent(input$go, {
z = sample(x = 1:100 , size = 1)
a$cach <<- a$acme$clone()
anum$a <<- anum$a + 1
a$acme$AddChild(paste0("New", z))
a$f <<- a$acme
b(a$acme)
print("a$acme")
print(a$acme)
print("b()")
print(b())
})
### not working
output$text = renderPrint( print(a$f) )
output$text0 = renderPrint(print(b()))
output$text1 = renderPrint(print(cc()))
### working
observe({
print(identical(a$acme, a$cach))
output$text2 = renderPrint(print(b()))
})
### working
observe({
anum$a
output$text3 = renderPrint(print(a$acme))
})
### working
observeEvent(eventExpr = anum$a, handlerExpr = {
output$text4 = renderPrint(print(a$acme))
})
}
shinyApp(ui, server)
Turns out that adding:
a$f <- 0 #to force reaction
a$f <- a$acme
a$acme <- 0 #to force reaction
a$acme <- a$f
'fixed' the problem.
library(shiny)
library(data.tree)
data(acme)
ui <- fluidPage(
actionButton("go", "go" ),
verbatimTextOutput("text"),
verbatimTextOutput("text1"),
verbatimTextOutput("text2"),
verbatimTextOutput("text3"),
verbatimTextOutput("text4")
)
server <- function(input, output, session) {
anum <- reactiveValues(a = 0)
a <- reactiveValues(acme = acme, f = NULL)
b <- reactiveVal(acme)
cc <- reactive(a$acme)
observeEvent(input$go, {
z <- sample(x = 1:100 , size = 1)
a$cach <- a$acme$clone()
anum$a <- anum$a + 1
a$acme$AddChild(paste0("New", z))
a$f <- 0 #to force reaction
a$f <- a$acme
a$acme <- 0 #to force reaction
a$acme <- a$f
b(a$acme)
print("a$acme")
print(a$acme)
print("b()")
print(b())
})
### not working
output$text = renderPrint({
req(a$f)
print(a$f)})
output$text2 = renderPrint(print(cc()))
## now it works
observe({
print(identical(a$acme, a$cach)) #this is triggering the update
output$text1 = renderPrint(print(b()))
})
### working
observe({
anum$a
output$text3 = renderPrint(print(a$acme))
})
### working
observeEvent(eventExpr = anum$a, handlerExpr = {
output$text4 = renderPrint(print(a$acme))
})
}
shinyApp(ui, server)
I think there's something going on with a$acme$AddChild(paste0("New", z)) method that is not detected as a change when called.
I'm trying to save and load state of a shiny app using bookmarks. However, it doesn't work and I wonder whether it is because of inserting dynamic UI. If there are other ways to save and load dynamically rendered ui and resulting output, that would be great too. I don't know where to start and this is as far as I could come.
Simple example
library(shiny)
ui <- function(request){fluidPage(
actionButton("add", "Add UI"),
bookmarkButton()
)}
# Server logic
server <- function(input, output, session) {
observeEvent(input$add, {
insertUI(
selector = "#add",
where = "afterEnd",
ui = textInput(paste0("txt", input$add),
"Insert some text")
)
})
}
# Complete app with UI and server components
shinyApp(ui, server, enableBookmarking = "server")
Complex example
library(shiny)
one_plotUI <- function(id) {
ns <- NS(id)
plotOutput(ns("plot"))
}
one_plot <- function(id, x, y, type, breaks, break_counts) {
moduleServer(
id,
function(input, output, session) {
output$plot <- renderPlot({
if (type == "scatter") {
plot(x, y)
} else {
if (breaks == "custom") {
breaks <- break_counts
}
hist(x, breaks = breaks)
}
})
}
)
}
ui <- fluidPage(
sidebarPanel(
bookmarkButton(),
selectInput("plotType", "Plot Type",
c(Scatter = "scatter", Histogram = "hist")
),
# Only show this panel if the plot type is a histogram
conditionalPanel(
condition = "input.plotType == 'hist'",
selectInput(
"breaks", "Breaks",
c("Sturges", "Scott", "Freedman-Diaconis", "[Custom]" = "custom")
),
# Only show this panel if Custom is selected
conditionalPanel(
condition = "input.breaks == 'custom'",
sliderInput("breakCount", "Break Count", min = 1, max = 50, value = 10)
)
),
actionButton("make_plot", "Insert new plot")
),
mainPanel(
div(id = "add_here")
)
)
server <- function(input, output) {
x <- rnorm(100)
y <- rnorm(100)
counter_plots <- 1
observeEvent(input$make_plot, {
current_id <- paste0("plot_", counter_plots)
# call the logic for one plot
one_plot(id = current_id,
x = x,
y = y,
type = input$plotType,
breaks = input$breaks,
break_counts = input$breakCount)
# show the plot
insertUI(selector = "#add_here",
ui = one_plotUI(current_id))
# update the counter
counter_plots <<- counter_plots + 1
})
}
shinyApp(ui, server, enableBookmarking = "server")
edit: Found another solution emulating what insertUI does but with renderUI:
library(shiny)
library(purrr)
ui <- function(request){fluidPage(
actionButton("add", "Add UI"),
uiOutput('dynamic_ui'),
bookmarkButton()
)}
# Server logic
server <- function(input, output, session) {
input_contents <- reactive({reactiveValuesToList(input)})
observeEvent(input$add, {
# a new ui will be rendered with one extra input each time add button is pressed
output$dynamic_ui <- renderUI({
map(1:input$add, ~textInput(inputId = paste0("txt", .x), label = paste0("txt", .x) ))
})
#add the old values, otherwise all the inputs will be empty agin.
input_contents() %>%
names() %>%
map(~ updateTextInput(session = session, inputId = .x, label = .x, value = input_contents()[[.x]]))
})
}
# Complete app with UI and server components
shinyApp(ui, server, enableBookmarking = "server")
insertUI might be broken. The only way i could "fix" it was to drop function(request) of the ui, that caused that all the values in the inputs have to be saved between stances (in state$values$input_restore). Also a warning is showed in the console, but it doesn't affect the functionality.
library(shiny)
library(tidyverse)
library(stringr)
ui <- fluidPage(
actionButton("add", "Add UI"),
uiOutput('restored_ui'), #this is very important
bookmarkButton())
# Server logic
server <- function(input, output, session) {
counter <- reactiveValues()
counter$n <- c(0) #This value is only used to initialize the object.
total_ui_count <- reactiveValues()
total_ui_count$info <- 0 #because input$add will reset to zero this will count the number of uis to remember.
#When bookmark button is pressed
onBookmark(function(state) {
state$values$currentCounter <- counter$n
state$values$input_restore <- reactiveValuesToList(input)
print(names(input) %>% str_subset('^txt'))
state$values$total_uis_to_restore <- counter$n[[length(counter$n)]]
})
#rerender the previous outputs and their values
onRestore(function(state) {
#restore values from previous state
counter$n <- state$values$currentCounter
vals <- state$values$input_restore
print(str_subset(names(vals), '^txt.*$')) #for debugging
total_ui_count$info <- state$values$total_uis_to_restore
print(total_ui_count$info)
#render back a ui with the previous values.
output$restored_ui <- renderUI({
str_subset(names(vals), '^txt.*$') %>%
sort(decreasing = TRUE) %>% #to avoid order reversal of the inputs
map(~ textInput(.x, label = .x, value = vals[[.x]])) #render the last inputs
})
})
observeEvent(input$add, {
#input$add starts as 1 in the next state (because ui is not wrapped in function(request)) that's why total_ui_count is present
counter$n <- c(counter$n, input$add + total_ui_count$info)
print(counter$n) #for debugging
insertUI(
selector = "#add",
where = "afterEnd",
ui = textInput(inputId = paste0("txt", counter$n[[length(counter$n)]]),
label = "Insert some text")
)})
}
# Complete app with UI and server components
shinyApp(ui, server, enableBookmarking = "server")
I'm trying to generalise Shiny modules so different functions can be passed through, but the expected behaviour of reactivity is not working - could someone point me in the right direction? I have a reprex below that illustrates my problem.
I expect that the dynamic selection of view_id to change values in the renderShiny() function. It works on app load but changing selections do not flow through.
Is it something to do with the environment the module function is created within?
library(shiny)
create_shiny_module_funcs <- function(data_f,
model_f,
outputShiny,
renderShiny){
server_func <- function(input, output, session, view_id, ...){
gadata <- shiny::reactive({
# BUG: this view_id is not reactive but I want it to be
data_f(view_id(), ...)
})
model_output <- shiny::reactive({
shiny::validate(shiny::need(gadata(),
message = "Waiting for data"))
model_f(gadata(), ...)
})
output$ui_out <- renderShiny({
shiny::validate(shiny::need(model_output(),
message = "Waiting for model output"))
message("Rendering model output")
model_output()
}, ...)
return(model_output)
}
ui_func <- function(id, ...){
ns <- shiny::NS(id)
outputShiny(outputId = ns("ui_out"), ...)
}
list(
shiny_module = list(
server = server_func,
ui = ui_func
)
)
}
# create the shiny module
ff <- create_shiny_module_funcs(
data_f = function(view_id) mtcars[, view_id],
model_f = function(x) mean(x),
outputShiny = shiny::textOutput,
renderShiny = function(x) shiny::renderText(paste("Mean is: ", x))
)
## ui.R
ui <- fluidPage(title = "module bug Shiny Demo",
h1("Debugging"),
selectInput("select", label = "Select", choices = c("mpg","cyl","disp")),
textOutput("view_id"),
ff$shiny_module$ui("demo1"),
br()
)
## server.R
server <- function(input, output, session){
view_id <- reactive({
req(input$select)
input$select
})
callModule(ff$shiny_module$server, "demo1", view_id = view_id)
output$view_id <- renderText(paste("Selected: ", input$select))
}
# run the app
shinyApp(ui, server)
The problem was the renderShiny function needs to wrap another function that creates the actual output, so its actually two separate capabilities confused by me as one: renderShiny should take the output of another function that actually creates the thing to render. The below then works:
library(shiny)
module_factory <- function(data_f = function(x) mtcars[, x],
model_f = function(x) mean(x),
output_shiny = shiny::plotOutput,
render_shiny = shiny::renderPlot,
render_shiny_input = function(x) plot(x),
...){
ui <- function(id, ...){
ns <- NS(id)
output_shiny(ns("ui_out"), ...)
}
server <- function(input, output, session, view_id){
gadata <- shiny::reactive({
data_f(view_id(), ...)
})
model <- shiny::reactive({
shiny::validate(shiny::need(gadata(),
message = "Waiting for data"))
model_f(gadata(), ...)
})
output$ui_out <- render_shiny({
shiny::validate(shiny::need(model(),
message = "Waiting for model output"))
render_shiny_input(gadata())
})
return(model)
}
list(
module = list(
ui = ui,
server = server
)
)
}
made_module <- module_factory()
## ui.R
ui <- fluidPage(title = "module bug Shiny Demo",
h1("Debugging"),
selectInput("select", label = "Select", choices = c("mpg","cyl","disp")),
textOutput("view_id"),
made_module$module$ui("factory1"),
br()
)
## server.R
server <- function(input, output, session){
callModule(made_module$module$server, "factory1", view_id = reactive(input$select))
output$view_id <- renderText(paste("Selected: ", input$select))
}
# run the app
shinyApp(ui, server)
I think you want something like this.
library(shiny)
library(plyr)
library(dplyr)
library(DT)
library(data.table)
ui <- pageWithSidebar(
headerPanel = headerPanel('data'),
sidebarPanel = sidebarPanel(fileInput(
'mtcars', h4('Uplaodmtcardata in csv format')
),
uiOutput('tabnamesui')),
mainPanel(uiOutput("tabsets"))
)
server <- function(input, output, session) {
mtcarsFile <- reactive({
input$mtcars
})
xxmtcars <-
reactive({
read.table(
file = mtcarsFile()$datapath,
sep = ',',
header = T,
stringsAsFactors = T
)
})
tabsnames <- reactive({
names(xxmtcars())
})
output$tabnamesui <- renderUI({
req(mtcarsFile())
selectInput(
'tabnamesui',
h5('Tab names'),
choices = as.list(tabsnames()),
multiple = T
# selected = SalesGlobalDataFilter1Val()
)
})
tabnamesinput <- reactive({
input$tabnamesui
})
output$tabsets <- renderUI({
req(mtcarsFile())
tabs <-
reactive({
lapply(tabnamesinput(), function(x)
tabPanel(title = basename(x)
,fluidRow(splitLayout(cellWidths = c("50%", "50%"),
plotOutput(paste0('plot1',x)),
plotOutput(paste0('plot2',x)
))),fluidRow(splitLayout(cellWidths =
c("50%", "50%"),
plotOutput(paste0('plot3',x)),
plotOutput(paste0('plot4',x)
))),
dataTableOutput(paste0('table',x))))
})
do.call(tabsetPanel, c(tabs()))
})
# Save your sub data here
subsetdata<-reactive({
list_of_subdata<-lapply(tabnamesinput(), function(x) {
as.data.table((select(xxmtcars(),x)))
})
names(list_of_subdata)<-tabnamesinput()
return(list_of_subdata)
})
observe(
lapply(tabnamesinput(), function(x) {
output[[paste0('table',x)]] <-
renderDataTable({
subsetdata()[[x]]
})}))
observe(
lapply(tabnamesinput(), function(x) {
for(i in paste0("plot",1:4)){
output[[paste0(i,x)]] <-
renderPlot({subsetdata()[[x]]%>%plot()#CODE REPEATED
})
}
})
)
}
runApp(list(ui = ui, server = server))
Data Source:
https://gist.githubusercontent.com/seankross/a412dfbd88b3db70b74b/raw/5f23f993cd87c283ce766e7ac6b329ee7cc2e1d1/mtcars.csv
I am showing tabbed graphs for each line that is selected in my initial table. I would like those graphs to have the brush/zoom functionality found here.
Here is my code :
library(shiny)
library(DT)
library(ggplot2)
library(scales)
library(reshape2)
First the ui : the main table with a tabbed UI below that is generated in response to selection of rows in the main table
ui <- fluidPage(
mainPanel(
fluidRow(
column(12,DT::dataTableOutput(outputId = 'tableCurrencies'))
),
fluidRow(
uiOutput("selectedTabs")
)
)
)
Then the server function : the main table values are generated randomly for the sake of the example. The brush functionality is directly lifted from the link provided. I suspect my issue has to do with a reactive function within a reactive function but I'm happy to let the experts decide.
server <- function(input,output){
output$tableCurrencies <- DT::renderDataTable({datatable(data.frame(a=rnorm(10),b=rnorm(10),c=rnorm(10)))})
origTable_selected <- reactive({
ids <- input$tableCurrencies_rows_selected
return(ids)
})
rangeRates <- reactiveValues(xRate = NULL, yRate = NULL)
output$selectedTabs <- renderUI({
myTabs <- lapply(origTable_selected(),function(i) {
tabName <- paste0("test",i)
a <- renderPlot({
hist(rnorm(50))
})
output[[paste0(tabName,"rates")]] <- a
#plot of realized vol and implied vols over 5 years
observeEvent(input[[paste0(tabName,"rates_dblclick")]], {
brush <- input[[paste0(tabName,"rates_brush")]]
if (!is.null(brush)) {
rangeRates$xRate <- c(brush$xmin, brush$xmax)
rangeRates$yRate <- c(brush$ymin, brush$ymax)
} else {
rangeRates$xRate <- NULL
rangeRates$yRate <- NULL
}
})
return(tabPanel(
tabName,
fluidRow(
column(6,plotOutput(paste0(tabName,"rates")))
)
))
})
return(do.call(tabsetPanel,myTabs))
})
}
app = shinyApp(ui,server)
runApp(app,port=3250,host='0.0.0.0')
You need to assign the "doubleclick id" and the "brush id" in the plotOutput call
column(6, plotOutput(paste0(tabName, "rates"),
dblclick = paste0(tabName, "rates_dblclick"),
brush = brushOpts(
id = paste0(tabName, "rates_brush"),
resetOnNew = TRUE
)))
Now the observers trigger properly and send the right information. There is still a second issue with rangeRates not having any effect on the plots which can be solved the following way
a <- renderPlot({
if (!is.null(rangeRates$xRate))
hist(rnorm(50), xlim = rangeRates$xRate,
ylim = rangeRates$yRate)
else
hist(rnorm(50))
})
Here is the full working version
library(shiny)
library(DT)
ui <- fluidPage(
mainPanel(
fluidRow(column(12, DT::dataTableOutput(outputId = 'tableCurrencies'))),
fluidRow(uiOutput("selectedTabs"))
)
)
server <- function(input, output){
output$tableCurrencies <- DT::renderDataTable({
data.frame(a = rnorm(10), b = rnorm(10), c = rnorm(10))
})
origTable_selected <- reactive({
ids <- input$tableCurrencies_rows_selected
return(ids)
})
rangeRates <- reactiveValues(xRate = NULL, yRate = NULL)
output$selectedTabs <- renderUI({
myTabs <- lapply(
origTable_selected(),
function(i) {
tabName <- paste0("test", i)
output[[paste0(tabName, "rates")]] <- renderPlot({
if( !is.null(rangeRates$xRate) )
hist(rnorm(50), xlim = rangeRates$xRate,
ylim = rangeRates$yRate)
else
hist(rnorm(50))
})
observeEvent(input[[paste0(tabName, "rates_dblclick")]], {
brush <- input[[paste0(tabName, "rates_brush")]]
if (!is.null(brush)) {
rangeRates$xRate <- c(brush$xmin, brush$xmax)
rangeRates$yRate <- c(brush$ymin, brush$ymax)
} else {
rangeRates$xRate <- NULL
rangeRates$yRate <- NULL
}
})
tabPanel(
tabName,
fluidRow(column(6, plotOutput(
paste0(tabName, "rates"),
dblclick = paste0(tabName, "rates_dblclick"),
brush = brushOpts(
id = paste0(tabName, "rates_brush"),
resetOnNew = TRUE)
)))
)
})
return(do.call(tabsetPanel, myTabs))
})
}
shinyApp(ui, server)