Shiny- Tooltip for each check-able box (basic) - r

I have a check-able list of ~60 different inputs, and I was hoping to set a tooltip for each individual box. Currently, using bsTooltip, I can only set a tooltip for the entire panel. Here is the relevant script...
library(shiny)
library(shinyBS)
library(networkD3)
ui <- fluidPage(
tabPanel("Analyze By Experiment",
sidebarLayout(
sidebarPanel(
width = 2,
fluid = FALSE,
bsTooltip("data1", "PDRF = Parkinsons Disease Risk Factors", placement = "right", trigger = "hover"),
checkboxGroupInput ( inputId = "data1", label = "High Throughput Experiment",
choices = unique(unlist(data$Present_In)))),
mainPanel(simpleNetworkOutput("coolplot", height = "800px"),
width = 10))
)
)
server <- function(input, output, session) { }
shinyApp(ui, server)

Building on the brilliant answer by #K. Rohde from this answer, we can do the same for your example. The issue you have is that you have 60 checkboxinputs, so writing out the tipify and bsButtonright for each one becomes tedious and longwinded. However, since if you notice from the two calls to tipify in the second part of his answer, only the id and help text changes, the rest stays the same. So we can write a function that takes id and help text and produces html code for a help info using that code. Then we can use lapply to create 60 or even more of these items, by simply passing a list of ids and help text to with our function to lapply. I've used the euStockMarkets dataset for this. It has 1720 unique rows, which with your code will give 1720 checkbox inputs. This is of course ridiculous but it demonstrates that the code works and hence will likely work on much fewer checkboxes
I've generated the help text using R but you'll probably type yours out.
Here is the full code below:
library(shiny)
library(shinyBS)
library(networkD3)
extendedCheckboxGroup <- function(..., extensions = list()) {
cbg <- checkboxGroupInput(...)
nExtensions <- length(extensions)
nChoices <- length(cbg$children[[2]]$children[[1]])
if (nExtensions > 0 && nChoices > 0) {
lapply(1:min(nExtensions, nChoices), function(i) {
# For each Extension, add the element as a child (to one of the checkboxes)
cbg$children[[2]]$children[[1]][[i]]$children[[2]] <<- extensions[[i]]
})
}
cbg
}
bsButtonRight <- function(...) {
btn <- bsButton(...)
# Directly inject the style into the shiny element.
btn$attribs$style <- "float: right;"
btn
}
data("EuStockMarkets")
eustocks <- as.data.frame(EuStockMarkets)
choiceNames <- paste0("cb", 1:length(unique(unlist(eustocks$FTSE))))
txt <- paste(rep("Help", length(unique(unlist(eustocks$FTSE)))), seq(1:length(unique(unlist(eustocks$FTSE)))))
txt[1] <- "PDRF = Parkinsons Disease Risk Factors"
ids <- paste0("pB", rep(1:length(unique(unlist(eustocks$FTSE)))))
inputData <- data.frame(cbid = ids, helpInfoText = txt)
inputData$cbid <- sapply(inputData$cbid, as.character)
inputData$helpInfoText <- sapply(inputData$helpInfoText, as.character)
checkBoxHelpList <- function(id, Text){
extensionsList <- tipify(bsButtonRight(id, "?", style = "inverse", size = "extra-small"), Text)
return(extensionsList)
}
# checkBoxHelpList(id = x["cbid"], Text = x["helpInfoText"])
helpList <- split(inputData, f = rownames(inputData))
checkboxExtensions <- lapply(helpList, function(x) checkBoxHelpList(x[1], as.character(x[2])))
server <- function(input, output, session) {
output$rendered <- renderUI({
extendedCheckboxGroup("qualdim", label = "High Throughput Experiment", choiceNames = choiceNames, choiceValues = unique(unlist(eustocks$FTSE)), selected = c("check2"),
extensions = checkboxExtensions)
})
}
ui <- fluidPage(
tabPanel("Analyze By Experiment",
sidebarLayout(
sidebarPanel(
width = 2,
fluid = FALSE,
uiOutput("rendered")),
mainPanel(simpleNetworkOutput("coolplot", height = "800px"),
width = 10))
)
)
shinyApp(ui, server)
As you can see below, the tool tip works.

Related

In R Shiny, how to dynamically expand the use of a function as user inputs expand?

The following MWE code interpolates user inputs (Y values in 2-column matrix input grid in sidebar panel, id = input1) over X periods (per slider input in sidebar, id = periods). The custom interpolation function interpol() is triggered in server section by results <- function(){interpol(...)}. User has the option to add or modify scenarios by clicking on the single action button, which triggers a modal housing a 2nd expandable matrix input (id = input2). Interpolation results are presented in the plot in the main panel. So far so good, works as intended.
As drafted, the results function only processes the first matrix input including any modifications to it executed in the 2nd matrix input.
My question: how do I expand the results function to include scenarios > 1 added in the 2nd expandable matrix input, and automatically include them in the output plot? I've been wrestling with a for-loop to do this but don't quite know how. I've avoided for-loops, instead relying on lapply and related. But in practice a user will not input more than 20-30 scenarios max and perhaps a for-loop is a harmless option. But I'm open to any solution and am certainly not wedded to a for-loop!
MWE code:
library(shiny)
library(shinyMatrix)
interpol <- function(a,b){ # a = periods, b = matrix inputs
c <- rep(NA,a)
c[1] <- b[1]
c[a] <- b[2]
c <- approx(seq_along(c)[!is.na(c)],c[!is.na(c)],seq_along(c))$y # this interpolates
return(c)
}
ui <- fluidPage(
sidebarLayout(
sidebarPanel(uiOutput("panel"),actionButton("showInput2","Modify/add interpolation")),
mainPanel(plotOutput("plot1"))
)
)
server <- function(input, output, session){
results <- function(){interpol(req(input$periods),req(input$input1))}
output$panel <- renderUI({
tagList(
sliderInput('periods','Interpolate over periods (X):',min=2,max=12,value=6),
uiOutput("input1"))
})
output$input1 <- renderUI({
matrixInput("input1",
label = "Interpolation 1 (Y values):",
value = matrix(if(isTruthy(input$input2)){c(input$input2[1],input$input2[2])}
else {c(1,5)}, # matrix values
1, 2, # matrix row/column count
dimnames = list(NULL,c("Start","End")) # matrix column header
),
rows = list(names = FALSE),
class = "numeric")
})
observeEvent(input$showInput2,{
showModal(
modalDialog(
matrixInput("input2",
label = "Automatically numbered scenarios (input into blank cells to add):",
value = if(isTruthy(input$input2)){input$input2}
else if(isTruthy(input$input1)){input$input1},
rows = list(names = FALSE),
cols = list(extend = TRUE,
delta = 2,
delete = TRUE,
multiheader=TRUE),
class = "numeric"),
footer = modalButton("Close")
))
})
observe({
req(input$input2)
mm <- input$input2
colnames(mm) <- paste(trunc(1:ncol(mm)/2)+1, " (start|end)")
isolate(updateMatrixInput(session, "input2", mm))
})
output$plot1 <-renderPlot({
req(results())
plot(results(),type="l", xlab = "Periods (X)", ylab = "Interpolated Y values")
})
}
shinyApp(ui, server)
As a user can (presumably) add only one scenario at a time, I don't think a for loop is going to help. The way I handle situations like this is to bind additional data to the appropriate reactive in an observeEvent. This will then trigger updates in the necessary outputs automatically. Here's a MWE to illustrate the technique.
library(shiny)
library(tidyverse)
ui <- fluidPage(
actionButton("add", "Add scenario"),
plotOutput("plot"),
)
server <- function(input, output, session) {
v <- reactiveValues(results=tibble(Scenario=1, X=1:10, Y=runif(10)))
observeEvent(input$add, {
newData <- tibble(Scenario=max(v$results$Scenario) + 1, X=1:10, Y=runif(10))
v$results <- v$results %>% bind_rows(newData)
})
output$plot <- renderPlot({
v$results %>% ggplot() + geom_point(aes(x=X, y=Y, colour=as.factor(Scenario)))
})
}
shinyApp(ui, server)

Something unwanted extra, when creating Shiny UI elements (Shiny Dashboard boxes) in using a server side loop

I am testing out to way of creating Shiny UI elements dynamically with a loop from the server side in a way that the user could control how many elements are actually produced. In my case the element is Shiny Dashboard box with two dropdown menus and one button. Everything works fine, except something extra is printed out as you can see from the image:
My ui.r looks as follows:
library(shiny)
library(shinydashboard)
shinyUI(dashboardPage(
dashboardHeader(title = 'The Box Experiment'),
# Sidebar with a slider input for number of bins
dashboardSidebar(
sliderInput("numberOfBoxes",
"Number of boxes:",
min = 1,
max = 50,
value = 5)
),
dashboardBody(uiOutput("boxes"))
)
)
...and server.r looks as follows:
library(shiny)
library(shinydashboard)
shinyServer(function(input, output) {
output$boxes <- renderUI({
boxlist = c()
for(i in 1:input$numberOfBoxes) {
ddmenu1 <- selectInput(paste0("ddmenu1_in_box",i), "Animal", list('cat', 'dog', 'rabbit'))
ddmenu2 <- selectInput(paste0("ddmenu2_in_box",i), "Color", list('red', 'blue', 'green'))
button <- actionButton(paste0("justabutton_in_box",i), "Click me!")
boxlist <- c(boxlist,column(1, box(ddmenu1, ddmenu2, button)))
}
boxlist
})
})
So where does this "div col-sm-1" times the number of boxes crap come from, and how do I get rid of it?
I'd recommend working with lapply rather than using a for-loop.
Here is explained why this is advantageous. Also see this overview.
library(shiny)
library(shinydashboard)
ui <- dashboardPage(
dashboardHeader(title = 'The Box Experiment'),
dashboardSidebar(
sliderInput("numberOfBoxes",
"Number of boxes:",
min = 1,
max = 50,
value = 5)
),
dashboardBody(uiOutput("boxes"))
)
server <- function(input, output, session) {
output$boxes <- renderUI({
lapply(seq_len(input$numberOfBoxes), function(i){
box(
selectInput(paste0("ddmenu1_in_box", i), "Animal", list('cat', 'dog', 'rabbit')),
selectInput(paste0("ddmenu2_in_box", i), "Color", list('red', 'blue', 'green')),
actionButton(paste0("justabutton_in_box", i), "Click me!")
)
})
})
}
shinyApp(ui, server)
Since, the "crap" lies somewhere in the list object, I decided to take a closer look at it.
So I developed this "hack" to overwrite the text with empty string:
ui.r (No modifications)
library(shiny)
library(shinydashboard)
shinyUI(dashboardPage(
dashboardHeader(title = 'The Box Experiment'),
# Sidebar with a slider input for number of bins
dashboardSidebar(
sliderInput("numberOfBoxes",
"Number of boxes:",
min = 1,
max = 50,
value = 5)
),
dashboardBody(uiOutput("boxes"))
)
)
server.r (This time includes a loop to overwrite unwanted strings)
library(shiny)
library(shinydashboard)
shinyServer(function(input, output) {
output$boxes <- renderUI({
boxlist = list()
for(i in 1:input$numberOfBoxes) {
ddmenu1 <- selectInput(paste0("ddmenu1_in_box",i), "Animal", list('cat', 'dog', 'rabbit'))
ddmenu2 <- selectInput(paste0("ddmenu2_in_box",i), "Color", list('red', 'blue', 'green'))
button <- actionButton(paste0("justabutton_in_box",i), "Click me!")
boxlist <- append(boxlist,(column(1, box(ddmenu1, ddmenu2, button))))
}
#Let's go through every attribute
for(i in 1:length(attributes(boxlist)$names)) {
#If the attribute name is NOT "children"
if(attributes(boxlist)$names[i] != "children") {
#...and the length of corresponding variable "name" equals one (text string)...
if(length(boxlist[i]$name) == 1) {
boxlist[i]$name <- ''
}
#...and the length of corresponding variable "attribs$class" equals one (text string)...
if(length(boxlist[i]$attribs$class) == 1) {
boxlist[i]$attribs$class <- ''
}
}
}
boxlist
})
})
I honestly think that this is a wrong way of doing this, and there has to be a better way to proceed, but until someone post it here, this seems to be the way to go. At least the crap is gone:

RShiny: Refer to data from UI in server

Example Case: I have a function in my global.R called get_data which returns a list of many items. The reason I don't just put the data in global is so the data can automatically refresh after a certain amount of time
ui.R
my_data <- uiOutput("data") # Doesn't work
### Some more generic manipulation before final use
# The output of my_data will look like the following below.
my_data <- list()
my_data$first_entry <- c("a", "b", "d")
my_data$second_entry <- c("x", "y", "z") # and so on
shinyUI(navbarPage(theme=shinytheme("flatly"),
'App Name',
tabPanel('Title',
# Sidebar with a slider input for number of bins
sidebarLayout(
sidebarPanel(
width=3,
# new box
checkboxGroupButtons(
'name',
'label:',
choices = sort(my_data$first_value),
status = 'primary',
selected = sort(my_data$first_value)[1],
size = 'xs'
# inline = TRUE
))
server.R
shinyServer(function(input, output, session) {
data <- reactive({
invalidateLater(100000,session)
get_data()
})
output$data <- renderUI({
data()
})
})
Two questions:
Is there any way of referencing my_data correctly?
If my function get_data is simply reading a (large) csv which is updated systematically. Is there a better way of doing it than I am currently doing it?
I think you're wondering how to define possible choices= for something within the UI element, when the data is both (1) undefined at the start, and (2) changing periodically. The answer to that is to define it "empty" and update it as the new data is found.
library(shiny)
library(shinyWidgets)
get_data <- function() as.list(mtcars[sample(nrow(mtcars), size=3), sample(ncol(mtcars), size=3)])
logg <- function(...) message(paste0("[", format(Sys.time()), "] ", ...))
shinyApp(
ui = fluidPage(
title = "Hello",
checkboxGroupButtons(inputId = "cb", label = "label:", choices = c("unk"), selected = NULL,
status = "primary", size = "xs"),
br(),
textOutput("txt"),
br(),
textAreaInput("txtarea", NULL, rows = 4)
),
server = function(input, output, session) {
data <- reactive({
logg("in 'data'")
invalidateLater(3000, session)
get_data()
})
observe({
logg("in 'observe'")
req(length(data()) > 0)
updateCheckboxGroupButtons(session = session, inputId = "cb", choices = names(data()))
updateTextAreaInput(session, "txtarea", value = paste(capture.output(str(data())), collapse = "\n"))
})
output$txt <- renderPrint({
logg("in 'txt'")
req(length(data()) > 0)
str(data())
})
}
)
Notice that the definition of checkboxGroupButtons starts with no real choices. I'd prefer to start it empty, but unlike selectInput and similar functions, it does not like starting with an empty vector. It is quickly (nearly-immediately) changed, so I do not see "unk" in the interface.
I demoed two options for "displaying" the data in its raw form: as an output "txt", and as an updatable input "txtarea". I like the latter because it deals well with fixed-width, but it requires an update* function (which is really not a big deal).

using pickerInput in r shiny to apply function

I would like to be able to apply a function to a given set of columns from the RLdata10000 dataset. I have been going through shiny tutorials and am attempting to learn how to use observeEvent and actionButton. However, I would like to be able to pick the columns I use so I came across pickerInput. In short, I would like to be able to pick a set of columns from RLdata10000, and apply the function via actionButton.
My problem is that I get an error: Error: unused argument (RLdata10000). My code is below. I would like to be able to do this with two data files eventually. Any help would be appreciated.
library(shiny)
library(DT)
library(shinyWidgets)
library(plyr)
library(dplyr)
library(RecordLinkage)
data(RLdata10000)
cleanup <- function(x){
x <- as.character(x) # convert to character
x <- tolower(x) # make all lowercase
x <- trimws(x, "both") # trim white space
return(x)
}
ui <- basicPage(
h2("Record Linkage Data"),
actionButton(inputId = "clean", label = "Clean Data")
pickerInput(width = "75%",
inputId = "pick_col1",
label = "Select columns to display",
choices = colnames(RLdata10000),
selected = colnames(RLdata10000),
options = list(
`actions-box` = T,
`selected-text-format` = paste("count > ", length(colnames(RLdata10000)) - 1),
`count-selected-text` = "Alle",
liveSearch = T,
liveSearchPlaceholder = T
),
multiple = T)
DT::dataTableOutput("mytable")
)
server <- function(input, output) {
observeEvent(input$clean, {
output$mytable = DT::renderDataTable({
lapply(input$pick_col1, cleanup)
})
}
}
shinyApp(ui, server)
I wasn't actually able to replicate the error you noted, but you had a few issues that were preventing you from getting what (I think) you're after.
First, you were missing commas in the UI after the actionButton and pickerInput elements.
Second, you are only giving lapply the names of columns - not the data - when you use input$pick_col1, so your cleanup function has nothing to work on. Using select from dplyr provides a simple way to name the columns and get the data too.
Last, renderDataTable wants a table format as an input (i.e., either a data frame or a matrix), but lapply produces a list. You need to convert the output of lapply into a workable class.
From these three changes, updated code would look like this:
library(shiny)
library(DT)
library(shinyWidgets)
library(plyr)
library(dplyr)
library(RecordLinkage)
data(RLdata10000)
cleanup <- function(x){
x <- as.character(x) # convert to character
x <- tolower(x) # make all lowercase
x <- trimws(x, "both") # trim white space
return(x)
}
ui <- basicPage(
h2("Record Linkage Data"),
actionButton(inputId = "clean", label = "Clean Data"),
pickerInput(width = "75%",
inputId = "pick_col1",
label = "Select columns to display",
choices = colnames(RLdata10000),
selected = colnames(RLdata10000),
options = list(
`actions-box` = T,
`selected-text-format` = paste("count > ", length(colnames(RLdata10000)) - 1),
`count-selected-text` = "Alle",
liveSearch = T,
liveSearchPlaceholder = T
),
multiple = T),
DT::dataTableOutput("mytable")
)
server <- function(input, output) {
observeEvent(input$clean, {
output$mytable = DT::renderDataTable({
data.frame(lapply(select(RLdata10000, input$pick_col1), cleanup))
})
})
}
shinyApp(ui, server)

Using eventReactive with a rendering function

I've been stuck on this problem for two days now, and I would love some help from people much smarter than me. I am using a package called "shinyTable"(https://github.com/trestletech/shinyTable), and I am having a hard time manipulating it. Basically, how can I make this table change its size based on input$rows IF I click on the "submit" button?Here is a working code w/o the "submit" button:
library(shinythemes)
library(shiny)
library(shinyTable)
ui <- fluidPage(theme = shinytheme("slate"),titlePanel(HTML("<h1> <font face=\"Rockwell Extra Bold\" color=\"#b42000\"><b><b>R/Econ</b></b></font> <font face=\"Lucida Calligraphy\" colsor=\"white\" >Model</font></h1>")),
sidebarLayout(
sidebarPanel(
numericInput("rows", label = h3("Number of Rows"), value = 20),
numericInput("cols", label = h3("Number of Columns"), value = 2)
),
mainPanel(
htable("tbl")
)
)
)
server <- function(input, output)
{
output$tbl <- renderHtable({
if (is.null(input$tbl)){
# Seed the element with some data initially
tbl <- data.frame(list(num1=1:input$rows,
num2=(1:input$rows)*20,
letter=LETTERS[1:(input$rows)]))
cachedTbl <<- tbl
print(tbl)
return(tbl)
} else{
cachedTbl <<- input$tbl
print(input$tbl)
return(input$tbl)
}
})
}
shinyApp(ui = ui, server = server)
Now, I want the table's size to change dynamically when my input$rows or input$cols changes. I cannot for the life of me figure out how to make this work. I tried the following:
myx<-eventReactive (input$submit, {
output$tbl <- renderHtable({
if (is.null(input$tbl)){
tbl <- data.frame(list(num1=1:input$rows,
num2=(1:input$rows)*20,
letter=LETTERS[1:(input$rows)]))
cachedTbl <<- tbl
print(tbl)
return(tbl)
} else{
cachedTbl <<- input$tbl
print(input$tbl)
return(input$tbl)
}
})
})
But this doesn't work. My thought process was that if the submit button is clicked, it would recreate the table. I want input$rows to change the size of the table, but neither my changing the size nor my clicking on a submit button does anything. In fact, adding eventReactive changes the table to where it has no values, and no values can be inputted. I'm honestly lost. I tried other variations of this such as this:
myx<-eventReactive (input$submit, {
if (is.null(input$tbl)){
tbl <- data.frame(list(num1=1:input$rows,
num2=(1:input$rows)*20,
letter=LETTERS[1:(input$rows)]))
cachedTbl <<- tbl
print(tbl)
return(tbl)
} else{
cachedTbl <<- input$tbl
print(input$tbl)
}
})
#-------
# myx2<-eventReactive (cachedTbl, {
# })
output$tbl <- renderHtable({
tbl<<-myx()
print(data.frame(tbl))#Tried and failed using myx()
return(data.frame(tbl))
})
In doing this, I thought I can make the table reactive and then pass it to renderHTable. All these attempts share the fact that I'm trying to make things reactive.
How can I make this table change its size based on input$rows IF I click on the "submit" button? Please help!
This should get you started. As per my comment, you should use rhandsontable. This package uses the same underlying JS library, handsontable.JS, but it is well supported and it is on Cran (disclaimer: I'm a minor contributor to this package).
The working example below is based on rhandsontable. For simplicity I've only implemented the change of the number of rows.
Please take into account that I haven't implemented any type of caching mechanism, either to a global variable, or to a reactive variable, as it wasn't necessary, but it can easily be added.
This is the only example that I know of a library working in shiny where there is an output$something linked to an input$something.
In this case the input$tbl in the code refer to the table, but to be converted to a data frame it needs to be transformed by the convenience function hot_to_r (handsontable to R).
I am sure you are already familiar with this: you use hot_to_r(input$tbl) to check if the user has changed anything in the displayed table (assuming it is not read-only). shinyTable has a much more complicated mechanism, but it is prone to races.
library(shinythemes)
library(shiny)
library(rhandsontable)
ui <- fluidPage(theme = shinytheme("slate"),titlePanel(HTML("<h1> <font face=\"Rockwell Extra Bold\" color=\"#b42000\"><b><b>R/Econ</b></b></font> <font face=\"Lucida Calligraphy\" colsor=\"white\" >Model</font></h1>")),
sidebarLayout(
sidebarPanel(
numericInput("rows", label = h3("Number of Rows"), value = 20),
numericInput("cols", label = h3("Number of Columns"), value = 2)
),
mainPanel(
rHandsontableOutput("tbl")
)
)
)
server <- function(input, output, session) {
data = reactive({
if (is.null(input$tbl)) {
DF <- data.frame(num1 = 1:input$rows, bool = TRUE, nm = LETTERS[1:input$rows],
dt = seq(from = Sys.Date(), by = "days", length.out = input$rows),
stringsAsFactors = F)
} else if(nrow(hot_to_r(input$tbl)) == input$rows) {
DF <- hot_to_r(input$tbl)
} else {
DF <- data.frame(num1 = 1:input$rows, bool = TRUE, nm = LETTERS[1:input$rows],
dt = seq(from = Sys.Date(), by = "days", length.out = input$rows),
stringsAsFactors = F)
}
DF
})
output$tbl <- renderRHandsontable({
if (is.null(input$rows) | is.null(input$cols)) return()
df = data()
if (!is.null(df))
rhandsontable(df, stretchH = "all")
})
}
shinyApp(ui = ui, server = server)
Please let me know if this works for you, else I'll do my best to change it as per your needs.

Resources