I've been developing a Shiny app that showcases a plot function, accepts inbuilt data or user-input CSV, produces custom plot and can output this to user as a PDF. All modules have worked fine independently of each other in development, but as a whole the app becomes unstable and regularly refuses to react to inputs. Sometimes it needs refreshing a few times just to start. All the functionality does work intermittently, so I think any bugs must relate to the complexities of the Shiny/browser interface. But as there's no feedback from Shiny (to R) or in the browser console it's almost impossible to diagnose, and it's starting to feel like a serious disincentive to using this otherwise very promising platform.
I've made the situation reproducible with a reduced script, which is also executable with runGist('db479811c6237a0741fe', launch.browser=F). I'd be really grateful for assistance from anyone who has experience of this type of issue or who understands Shiny under the hood. Advice also appreciated on ways to streamline or rework the code structure. Any comments/discussion not suitable for SO please post to reddit.
server.R
require(shiny)
# inbuilt dataset
diamonds = ggplot2::diamonds[,c(1,5,7)]
# csv datasets to input via front-end
for(i in 1:3){
dat <- diamonds[sample(1:nrow(diamonds), 200),]
write.table(dat, paste0('dat',i,'.csv'), sep=',',row.names=F, col.names=T)
}
diamonds = diamonds[sample(1:nrow(diamonds),200),]
# global variables
inbuilt = FALSE # whether currently using inbuilt data or not
datapath = '' # to chech current against previous to see if new dataset input
pagereset = FALSE # to reset when inbuilt de-selected
# function to 'plot' welcome instructions
welcome <- function(){
plot.new(); plot.window(xlim=c(0,100), ylim=c(0,100))
text(10,80,"Please input CSV file data with 3 numerical columns", cex=2, pos=4)
text(10,65,"Use the inbuilt dataset and the csv files in the app folder..", cex=1.5, pos=4)
text(10,50,"check app's reliability and how often commands fail", cex=1.5, pos=4)
text(10,35,"output to PDF", cex=1.5, pos=4)
text(10,20,"how stable is the app for you?", cex=1.5, pos=4)
}
shinyServer(function(input, output, session) {
# REACTIVE FUNCTION
plotInput = reactive({
# import data from inbuilt (internal) or a user-input CSV
# first must check if reactive is triggered by new data or not:
newdata = FALSE # initialised
if(input$inbuilt != inbuilt){ # inbuilt data option toggled
if(input$inbuilt) { # inbuilt selected
inbuilt <<- TRUE # update global
d <<- diamonds
newdata = TRUE
} else{ # inbuilt de-selected.
inbuilt <<- FALSE # update global
d <<- NULL # return splashscreen
pagereset <<- TRUE # would now crash so refresh app instead
}
} else { # input doesn't relate to inbuilt dataset
if(!input$inbuilt){ # inbuilt unselected
if(is.null(input$file1)) { # if null no input received yet
d = NULL # so reactive will return splash-screen
} else { # data has been input before
if(input$file1$datapath != datapath){ # new dataset just received
datapath <<- input$file1$datapath # update global
d <<- read.csv(datapath, header=TRUE, sep = ',') # update global
newdata = TRUE
#Sys.sleep(2) # allow file-upload aanimation to finish
# reset file handler in page
session$sendCustomMessage(type = "resetFileInputHandler", "file1")
} else NULL # new input not dataset-related
}
}
}
# reset/null javascript command - to reset app after inbuilt
# dataset is de-selected, as the script crashes otherwise..
reset_js = ifelse(pagereset, "window.location.reload()", '')
reset_js = paste("<script>", reset_js,";</script>")
if(pagereset) {
pagereset <<- FALSE
return(list(resetpage = reset_js, plot = plot.new())) # reset and null plot
}
# no data input so return splash-screen
if(is.null(d)) return(list(resetpage = reset_js, plot = welcome()))
# NORMAL PLOT
# # stroke around polygons
if(input$border != 'none') border = input$border else border = NA
# PDF handling (save file locally to be passed forward)
if(input$returnpdf){
pdf("plot.pdf", width=as.numeric(input$w), height=as.numeric(input$h))
symbols(d[[1]], d[[2]], circles=sqrt(d[[3]]), inches=as.numeric(input$inches),
bg='#ff000020', fg=border)
dev.off()
}
# return plot and reset instruction in list
list(
resetpage = reset_js,
plot = symbols(d$carat, d$depth, circles=sqrt(d$price), inches=as.numeric(input$inches),
bg='#ff000020', fg=border)
)
}) # end reactive
# OUTPUT ELEMENTS
# PDF file
output$pdflink = downloadHandler(
filename <- "shiny_plot.pdf", # default browser save filename
content <- function(file) file.copy("plot.pdf", file) # call pre-saved pdf
)
# plot
output$plot = renderPlot({ plotInput()$plot })
# reset instruction
output$reset = renderText({ plotInput()$resetpage })
})
ui.R
require(shiny)
fluidPage(
titlePanel("Stability testing"),
sidebarLayout(
sidebarPanel(
# this css just resets the CSV upload function
tags$head(
tags$script('
Shiny.addCustomMessageHandler("resetFileInputHandler", function(x) {
var id = "#" + x + "_progress";
var idBar = id + " .bar";
$(id).css("visibility", "hidden");
$(idBar).css("width", "0%");
});
')
),
# inputs
h4('Input options'),
p("Chose inbuilt dataset or upload a CSV:"),
checkboxInput('inbuilt', 'Inbuilt dataset (app resets when de-selected)', FALSE),
fileInput('file1', '', accept = 'text/comma-separated-values'),
# PDF output
h4('PDF output'),
p("Buggy: plot disappears, but link still downloads last plot. Sometimes after download app crashes"),
checkboxInput('returnpdf', 'Save plot to PDF?', FALSE),
conditionalPanel(
condition = "input.returnpdf == true",
strong("PDF size (inches):"),
sliderInput(inputId="w", label = "width:", min=3, max=20, value=12, width=100, ticks=F),
sliderInput(inputId="h", label = "height:", min=3, max=20, value=9, width=100, ticks=F),
downloadLink('pdflink')
),
# plot layout
h4('Plot options'),
selectInput(inputId="border", label="Outline colour:", choices=list(black='black', white='white', none='none'), width=150, selected='black'),
sliderInput(inputId="inches", label = "Circle size (higher values can crash the app)", min=0.05, max=.5, value=.2, width=150)
),
mainPanel(
htmlOutput('reset'), # reset command (when inbuild dataset de-selected)
imageOutput('plot')
)
)
)
Related
I wrote a little standard application in R that is running pretty fine.
It scans the *.png files in a folder and extracts some statistics on pixel granularity and colours.
Now it is my idea to transfer this to shiny. Although I once wrote some smaller Shiny-Apps, i never used reactivity intensely.
The basic idea of the here presented code is:
to choose png files from a folder
show the number of chosen images
initiate some calculations on the chosen files after clicking a button
displaying the extracted parameters
I now have critical problems with the basic design of the reactive variables, obviously as my construct in mind is not suitable.
Is there someone who could advise me the basic outline of the shiny construct?
I tried to reduce the code to the bare minimum. I hope one can follow my thoughts.
library (shiny)
options (shiny.maxRequestSize = 250 * 1024^2)
# increase maximum data upload to 250MB
const_Number_of_Variables = 5
# just to keep the number of extracted parameters
# up to date when new parameters will be included
ui <- fluidPage(
# just a simple UI with some basic elements
fileInput ("ui_IMGfiles",
"Choose PNG files",
multiple = TRUE,
accept = c ("image/png",
".png")
),
tag$hr,
tableOutput ("ui_IMGfilelistcontents"),
tag$hr,
textOutput ("imagecount"),
tag$hr,
actionButton ("ui_btn_CalcFeatures", "Calculate image features", class = "btn-warn"),
tag$hr,
tableOutput('IMG_featuretable')
)
IMG_filelist <- reactiveValues (data = NULL)
IMG_features <- reactiveValues (val1 = NULL, val2 = NULL, val3 = NULL, val4= NULL, val5 = NULL, val6 = NULL)
IMG_data <- reactiveValues (data = NULL)
server <- function(input, output) {
observeEvent (input$ui_IMGfiles, {
# input$ui_IMGfiles will be NULL initially. After the user selects
# and uploads files, it will be a data frame with 'name',
# 'size', 'type', and 'datapath' columns. The 'datapath'
# column will contain the local filenames where the data can
# be found.
IMG_filelist$data <- input$ui_IMGfiles
})
output$ui_IMGfilelistcontents <- renderTable({
# if file list is not initialised, return NULL
if (is.null (IMG_filelist$data () ))
return(NULL)
#finally display the list of images
IMG_filelist$data ()
})
output$imagecount <- renderText({
# whenever the file list changes, identify
# the number of elements from the length of list entries
paste0 ("# of files: ", length (IMG_filelist$data () ))
}
)
IMG_data <- observeEvent (input$ui_btn_CalcFeatures, {
# check whether some images are loaded
if (is.null (IMG_filelist)) {IMG_data = NULL}
IMG_files_count = length (IMG_filelist)
# prepare the feature list that will be
# overwritten every time the button is pressed
IMG_data$data = matrix (NaN, nrow = 1, ncol = const_Number_of_Variables)
for (i in 1:IMG_files_count) {
# now read the i'th image from the list and pass it to the evaluation function
aSourceImage = readPNG (IMG_filelist$data [i]$datapath, native = FALSE, info = TRUE)
# extract the desired parameters and attach them to the list
IMG_data$data = rbind ( c ("val1", "Val2", "val3", "Val4", "etc"),
#fn_ExtractTileInformation (aSourceImage, aFileFullPath, aFileName, 128),
IMG_data$data ())
}
})
output$IMG_featuretable <- renderDataTable (img_data$data ())
}
shinyApp (ui, server)
I am working on building a shiny App. I have used some filters and rendered a data frame and the data frame changes dynamically as per the user input. But I cannot store a particular column value from a data frame into a vector. I need to store the reactive output every time into a vector so that I can use the values later again. Here the values are stored in text_vec and i need to pass that into the API but I cannot access the values from text_vec and i have to pass the updated values every time into the API
library(dplyr)
library(shiny)
shinyApp(ui = fluidPage(
sidebarLayout(
sidebarPanel(
selectInput(inputId = "cyl",
label = "Number cylinders:",
choices = c("all",sort(unique(mtcars$cyl))),
selected = "all"),
actionButton("capture",
"capture value")
), # closes sidebarPanel
mainPanel(
tableOutput("text"),
tableOutput("text2"),
tableOutput("text3"),
tableOutput("table")
) # closes mainPanel
) # closes sidebarLayout
), # closes fluidPage
server = function(input, output) {
# some example reactive data
cars_react <- reactive({
mtcars %>%
filter(cyl == input$cyl | input$cyl == "all")
})
# simply global assignment of a reactive vector
observeEvent(cars_react(), {
# here is a globally assigned vector taken from the reactive data
# reused in a render statement it will not react to change, since it is not reactive
test_vec3 <<- unique(cars_react()$hp)
})
# here a file is written to the working directory of your shiny app
# everytime cars_react() changes write (and overwrite) vector to a file
observeEvent(cars_react(), {
test_vec = unique(cars_react()$hp)
saveRDS(test_vec, file = "test_vec.Rdata")
})
# same as above but the file is gradually growing and not overwritten
# everytime cars_react() changes add vector to a (over several sessions growing) list
observeEvent(cars_react(), {
test_vec2 = unique(cars_react()$hp)
if (file.exists("test_list.Rdata")) {
temp = readRDS("test_list.Rdata")
test_list = c(temp, list(test_vec2))
} else {
test_list = list(test_vec2)
}
saveRDS(test_list, file = "test_list.Rdata")
})
# here we access the reactive data with isolate and make it non-reactive, but can update the values through a button click
text_vec <<- eventReactive(input$capture, {
isolate(unique(cars_react()$hp))
})
# output of our reactive data as table
output$table <- renderTable({
cars_react()
})
# text output of globally assigned non-reactive vector test_vec3 (not changing!)
output$text <- renderText({
test_vec3
})
# you can capture values of reactives with isolate, but then, they don't change anymore
# text output of isolated formely reactive vector unique(cars_react()$hp (not changing!)
output$text2 <- renderText({
isolate(unique(cars_react()$hp))
})
# text output of new reactive vector (changes when input$capture button is clicked)
output$text3 <- renderText({
text_vec()
})
for (i in text_vec)
{
url = "https://oscar.com/prweb/PRRestService/"
parameters<-'{
{
"Reference":"Account"
,"ReferenceValue":""
}'
b<-fromJSON(parameters)
b["ReferenceValue"]=i
r <- POST(url, body = parameters,encode = "json")
r_c<-toJSON(content(r))
print(r_c)
}
}
)
A simple way to get a data frame to persist across all environments used within your Shiny app, is to use the '<<-' assignment instead of the '<-" assignment. This is not a great programming technique, but it may be what you're hoping to find.
# To get a data frame to persist, use
a <<- b
# instead of
a <- b
** Updated answer **
Based on your updated answer, I would wrap you API call into an observeEvent which gets triggered once the action button is pressed. Since you do not provide a working example with some real code, I am not sure whether the example below is of help. I further assume that your for loop is correct and working (on my end, I cannot know without a real API and some real values).
library(dplyr)
library(shiny)
library(httr)
library(jsonlite)
shinyApp(ui = fluidPage(
selectInput(inputId = "cyl",
label = "Number cylinders:",
choices = c("all",sort(unique(mtcars$cyl))),
selected = "all"),
actionButton("capture",
"capture value")
), # closes fluidPage
server = function(input, output) {
# some example reactive data
cars_react <- reactive({
mtcars %>%
filter(cyl == input$cyl | input$cyl == "all")
})
# here we access the reactive data with isolate and make it non-reactive, but can update the values through a button click
observeEvent(input$capture, {
for (i in unique(cars_react()$hp))
{
url = "https://oscar.com/prweb/PRRestService/"
parameters<-'{
"Reference":"Account"
,"ReferenceValue":""
}'
b<-fromJSON(parameters)
b["ReferenceValue"]=i
r <- POST(url, body = parameters,encode = "json")
r_c<-toJSON(content(r))
print(r_c)
}
})
}
)
Old answer
It is not clear from your question how, where and how often you want to use the vector of your reactive data frame. But it is an important question, since the concept of reactivity and how to access it is very hard to grasp when you come from a pure non reactive R environment.
Below is a simple example app which shows how to access vectors in reactive data frames, and how they could be used.
I hope it helps to get a better understanding of reactivity in shiny.
library(dplyr)
library(shiny)
shinyApp(ui = fluidPage(
sidebarLayout(
sidebarPanel(
selectInput(inputId = "cyl",
label = "Number cylinders:",
choices = c("all",sort(unique(mtcars$cyl))),
selected = "all"),
actionButton("capture",
"capture value")
), # closes sidebarPanel
mainPanel(
tableOutput("text"),
tableOutput("text2"),
tableOutput("text3"),
tableOutput("table")
) # closes mainPanel
) # closes sidebarLayout
), # closes fluidPage
server = function(input, output) {
# some example reactive data
cars_react <- reactive({
mtcars %>%
filter(cyl == input$cyl | input$cyl == "all")
})
# simply global assignment of a reactive vector
observeEvent(cars_react(), {
# here is a globally assigned vector taken from the reactive data
# reused in a render statement it will not react to change, since it is not reactive
test_vec3 <<- unique(cars_react()$hp)
})
# here a file is written to the working directory of your shiny app
# everytime cars_react() changes write (and overwrite) vector to a file
observeEvent(cars_react(), {
test_vec = unique(cars_react()$hp)
saveRDS(test_vec, file = "test_vec.Rdata")
})
# same as above but the file is gradually growing and not overwritten
# everytime cars_react() changes add vector to a (over several sessions growing) list
observeEvent(cars_react(), {
test_vec2 = unique(cars_react()$hp)
if (file.exists("test_list.Rdata")) {
temp = readRDS("test_list.Rdata")
test_list = c(temp, list(test_vec2))
} else {
test_list = list(test_vec2)
}
saveRDS(test_list, file = "test_list.Rdata")
})
# here we access the reactive data with isolate and make it non-reactive, but can update the values through a button click
text_vec <- eventReactive(input$capture, {
isolate(unique(cars_react()$hp))
})
# output of our reactive data as table
output$table <- renderTable({
cars_react()
})
# text output of globally assigned non-reactive vector test_vec3 (not changing!)
output$text <- renderText({
test_vec3
})
# you can capture values of reactives with isolate, but then, they don't change anymore
# text output of isolated formely reactive vector unique(cars_react()$hp (not changing!)
output$text2 <- renderText({
isolate(unique(cars_react()$hp))
})
# text output of new reactive vector (changes when input$capture button is clicked)
output$text3 <- renderText({
text_vec()
})
}
)
I am having an issue with using Plotly on my shiny server. I have a clustering app where I bring in a data file and you are able to perform two variable and three variable clustering. The first tab is the two variable clustering. I use ggplot() to create the plot and then using Plotly's ggplotly() function to make it a plotly object to enable interactivity. This renders fine on the app's page.
The issue here is when plotting the three variable clustering. Instead of ggplot() and then ggploty(), I use Plotly's function plot_ly(). This allows me to pass an x, y, and z variable. It works like a charm on my local machine, but when on the shiny server I get this error Error: An error has occurred. Check your logs or contact the app author for clarification. I open my logs and do a hard refresh of the app, no logs are showing. My package versions are the same for both my shiny server and my local machine. The error is not every telling here and I have tried to google around but I am getting no where.
Here is the code:
server.R
## Needed Lib's
library(shiny)
library(ggplot2)
library(plotly)
library(DT)
library(cluster)
## Initiate the severer
shinyServer(
function(input, output, session) {
## Display the text that describes why to use this app/method for grouping
output$text <- renderText({
## Let the user know which tab they have selected
{paste0("You are viewing the \"", input$ClusterChoice, "\"")}
})
## Display the text that describes why to use this app/method for grouping
output$why_text <- renderText({
## Let the user know which tab they have selected
"This is why"
})
# Variable #1
output$varselect1 <- renderUI({
selectInput("var1", label = "Select first variable for clustering:",
choices = names(dataset()), selected = names(dataset())[1])
})
# Variable #2
output$varselect2 <- renderUI({
selectInput("var2", label = "Select second variable for clustering:",
choices = names(dataset()), selected = names(dataset())[2])
})
## Clustering with third variable
# Variable #1
output$varselect3 <- renderUI({
selectInput("var3", label = "Select third variable for clustering (only works in Multiple Variable Tiering Tab):",
choices = names(dataset()), selected = names(dataset())[3])
})
## Read in the data
dataset <- reactive({
infile <- input$datafile
if (is.null(infile)) {
return(NULL)
}
else {read.csv(infile$datapath)}
})
## Compute the K means algo
compute_kmeans <- reactive({
# Choose between simple K means or a more complex K means with third variable
if (input$ClusterChoice == 'Two Variable Tiering') {
data <- subset(dataset(), select = c(input$var1, input$var2))
# Scale the data. Using "standardized" here. This will center and then scale the data
#data <- scale(data, center = TRUE)
# Change some columns names
colnames(data) <- c('x', 'y')
data <- na.omit(data)
# Set the seed
set.seed(111)
# Cluster
Kclust <- kmeans(data, input$k)
# Save results to a list
kmean.result <- list(kmean.result = data.frame(data, cluster = as.factor(Kclust$cluster)))
return(kmean.result)
}
# K means with third variable
else { (input$ClusterChoice == 'Multiple Variable Tiering')
three_var_data <- subset(dataset(), select = c(input$var1, input$var2, input$var3))
# Scale the data. Using "standardized" here. This will center and then scale the data
#three_var_data <- scale(three_var_data, center = TRUE)
three_var_data <- na.omit(three_var_data)
# Set the seed
set.seed(111)
# Cluster
Kclust <- kmeans(three_var_data, input$k)
kmean.result <- list(kmean.result = data.frame(three_var_data, cluster = as.factor(Kclust$cluster)))
return(kmean.result)
}
})
## Create a dataframe of the K means & K means with third variable results
kmeans_results <- reactive({
# For only two variables
if (input$ClusterChoice == 'Two Variable Tiering') {
# Call the method that computes the k means
data <- compute_kmeans()
# Save the results into a df
results <- data$kmean.result
results_df <- data.frame(results)
colnames(results_df) <- c(input$var1, input$var2, 'Grouping')
return(results_df)
}
# For more than two variables
else { (input$ClusterChoice == 'Multiple Variable Tiering')
# Call the method that computes the k means with third variable
three_var_data <- compute_kmeans()
# Save the results into a df
three_var_results <- three_var_data$kmean.result
three_var_results_df <- data.frame(three_var_results)
colnames(three_var_results_df) <- c(input$var1,
input$var2,
input$var3,
'Grouping')
return(three_var_results_df)
}
})
## Results of each K means & K means with third variable
master_results <- reactive({
# Call the method that stores the raw input data
data <- dataset()
# For only two variables
if(input$ClusterChoice == 'Two Variable Tiering') {
# Call the method that stores the K means results
kmean_results <- kmeans_results()
# Merge the K means results with the raw input data
results <- merge(kmean_results, data)
results <- results[!duplicated(results), ]
return(results)
}
# For more than two variables
else { (input$ClusterChoice == 'Multiple Variable Tiering')
# Call the method that stores the K means with third variable results
kmean_three_var_results <- kmeans_results()
# Merge the K means with third variable results with the raw input data
three_var_resutls <- merge(kmean_three_var_results, data)
three_var_resutls <- three_var_resutls[!duplicated(three_var_resutls), ]
return(three_var_resutls)
}
})
## Plot the K means results
output$plot <- renderPlotly({
graphics.off()
pdf(NULL)
# For only two variables
if (input$ClusterChoice == 'Two Variable Tiering') {
# Call the K means results method
results <- master_results()
# Change the x & y variables so we can call it in the tool-tip
x_axis <- results[[input$var1]]
y_axis <- results[[input$var2]]
# Plot
plot <- ggplot(data = results,
aes(x = x_axis,
y = y_axis,
color = Grouping,
name = Markets)) +
geom_point(size = 2) +
ggtitle("Grouping Results") +
labs(x = input$var1, y = input$var2)
# Use Plotly for interactivity
plotly_plot <- ggplotly(plot)
return(plotly_plot)
}
# For more than two variables
else if (input$ClusterChoice == 'Multiple Variable Tiering')
# Call the K means and third variable
three_var_resutls <- master_results()
# Change x, y, & z variables so we can call it in the tool-tip
x_axis <- three_var_resutls[[input$var1]]
y_axis <- three_var_resutls[[input$var2]]
z_axis <- three_var_resutls[[input$var3]]
# Plot (using Plotly for interactivity)
three_var_plot <- plot_ly(three_var_resutls,
x = x_axis,
y = y_axis,
z = z_axis,
color = factor(three_var_resutls$Grouping),
text = ~paste('Markets:', three_var_resutls$Markets,
'<br>Grouping:', three_var_resutls$Grouping)) %>%
add_markers() %>%
layout(title = 'Grouping Results',
scene = list(xaxis = list(title = input$var1),
yaxis = list(title = input$var2),
zaxis = list(title = input$var3)))
return(three_var_plot)
})
## Render the K means and K means with third variable results to a data table
output$cluster_table <- DT::renderDataTable({
# For only two variables
if (input$ClusterChoice == 'Two Variable Tiering') {
# Call results method
results <- master_results()
# Render data table
datatable(results,
# Get rid of row indexes
rownames = FALSE,
# Enable downloading options
extensions = 'Buttons',
options = list(
dom = "Blfrtip",
buttons =
list("copy", list(
extend = "collection",
buttons = c("csv", "excel", "pdf"),
text = "Download")),
lengthMenu = list(c(10, 20, -1),
c(10, 20, "All")),
pageLength = 10))
}
else if (input$ClusterChoice == 'Multiple Variable Tiering') {
# Call results method
results_three_var <- master_results()
# Render data table
datatable(results_three_var,
# Get rid of row indexes
rownames = FALSE,
# Enable downloading options
extensions = 'Buttons',
options = list(
dom = "Blfrtip",
buttons =
list("copy", list(
extend = "collection",
buttons = c("csv", "excel", "pdf"),
text = "Download")),
lengthMenu = list(c(10, 20, -1),
c(10, 20, "All")),
pageLength = 10))
}
})
}
)
ui.r
## Needed Lib's
library(shiny)
library(plotly)
## Start the UI renderer
shinyUI(
pageWithSidebar(
## The TITLE!
headerPanel("Grouping Data Together"),
## This is where the up-loader and drop downs live
sidebarPanel(
fileInput('datafile',
'Choose CSV file',
accept=c('text/csv', 'text/comma-separated-values,text/plain')),
uiOutput("varselect1"),
uiOutput("varselect2"),
uiOutput("varselect3"),
numericInput('k', 'Number of clusters', value = 3, min = 1, step = 1)),
## The main panel where all the shit happens
mainPanel(
textOutput("text"),
tabsetPanel(id = 'ClusterChoice',
tabPanel("Two Variable Tiering", value = 'Two Variable Tiering'),
tabPanel("Multiple Variable Tiering", value = 'Multiple Variable Tiering'),
tabPanel("Why Do It This Way?", value = "Why Do It This Way?")
),
## Description of this app
h2("Description"),
p("This tool's main functionality is to quickly put your data into groups. The file that is being uploaded
should only be the data that you want to group together. For example, if you have data that is broken out
by Markets (DMA) and you want to group those Markets into similar groupings you should upload all data
that you think is important to those groupings. Then via the drop downs you can select the 2 or 3 variables
you think best represents the groupings."),
## The tabs
h2("The Different Tabs"),
p("Tab #1: This tab is only when you want to use 2 variables in your data to create the groupings."),
p("Tab #2: This tab is only when you want to use 3 variables in your data to create the groupings."),
## Instructions for using this app
h2("Instructions"),
p("Please follow these instructions to create your groupings."),
p("1. Upload a data file (.csv) that will be used for making your groups.
The first row in the .csv file should be the column headers of the data.
The first column in the .csv file should be where your data starts.
THERE SHOULD BE NO 'BUFFERS' AROUND YOUR DATA FILE, i.e. empty rows and columns."),
p("2. Pick the variables you want to create the groupings off of via the drop-downs"),
p("3. Indicate the desired number of groups via the last drop-down."),
## The plot instructions
h2("Visualizing Your Grouped Data"),
p("You can download the plot as a .png file by hovering over the plot and selecting the
camera icon in the upper right hand side of the plot."),
# Plotly function that links the UI to the Server
plotlyOutput('plot', height = 700),
## The grouping data table instructions
h2("The Groupings"),
p("Instructions for downloading data:"),
p("1. To download the data use the drop-down below, labeled 'Show entries', to show 'All' entries."),
p("2. Click the 'Download' button and then select the type of file (PDF, excel, csv)."),
p("3. (Optional) You can copy the data that is shown to your clipboard is paste it into an excel/csv document."),
# JS DataTable function that links the UI to the Server
DT::dataTableOutput("cluster_table"))
)
)
I am in the process of learning Shiny and developing a simple app. The start of the program will allow a user to import a CSV file and then apply a filter variable(s) if needed. They will only be able to use factors as filter variables at this stage. I apply the filters on an iterative basis. So, one can apply a filter based on a factor level and then apply another factor level and so on until completed.
The best application I could find of being able to subset a reactive data frame was to apply the data frame as a reactive value. This seems to work, but I am having a couple issues that I can't figure out how to resolve.
1) Given the filtering is an iterative process, I would like to keep track and print out each variable and level applied during the filtering process. The best way I could figure out was creating a global variable (<<-) and using renderText to print out the contents after hitting the apply filter button. The issue is renderText just flashes on the screen and quickly disappears. I included a print to console statement that verifies the text is being saved correctly. I believe this is happening from the filter being applied to the reactive data frame and the updating process, but I can't figure out how to stop the text from disappearing on the screen?
2) When I try to save out the reactive data frame at the end of the shiny code, I get the following error "Warning: Error in $: $ operator is invalid for atomic vectors". I tried a couple things, but don't really understand what is going on here because the object "file$dfSource" is not like a normal reactive data frame dfSource()?
The shiny app below uses iris data so its easier to use/test. I don't know if applying the data frame to a reactive value is the best way to program this or if there is an easier way to do all this - just trying to learn best approach here.
library(shiny)
allfilters <- c()
ui <- (fluidPage(
# Application title
titlePanel("Filter Data"),
# Input Forms
sidebarLayout(
sidebarPanel(
h3("Data"),
checkboxInput("selectFilter", label = "Apply Filter Variable", value = FALSE),
uiOutput("selectFilterVar"),
uiOutput("selectFilterGroup"),
helpText("Apply filter to data"),
uiOutput("selectFilterButton"),
helpText("Reset data to total"),
uiOutput("selectResetButton"),
h3("Download Data"),
helpText("Download Data"),
downloadButton("downloadData", "Download File")
),
# Output Forms
mainPanel(
tabsetPanel(
tabPanel("Summary",
h2("Output Summary"),
textOutput("ncases"),
textOutput("selectedfilters")))
)
)
))
server <- (function(input, output, session) {
data <- iris
file <- reactiveValues(dfSource = data)
## Select Filter Variable
output$selectFilterVar <- renderUI({
req(file$dfSource)
if (input$selectFilter){
selectInput("filterVar", "Select Filter Variable", multiple = FALSE, choices = sort(names(file$dfSource[, sapply(file$dfSource, is.factor), drop = FALSE])))
}
})
# Select Filter Group(s)
output$selectFilterGroup <- renderUI({
req(file$dfSource)
req(input$filterVar)
if (input$selectFilter){
selectInput("filterGroup", "Select Filter Group", multiple = TRUE, choices = sort(unique(file$dfSource[,input$filterVar])))
}
})
# Apply Filter Button
output$selectFilterButton <- renderUI({
req(file$dfSource)
if (input$selectFilter) {
actionButton("filterButton", "Apply Filter")
}
})
# Apply filter group to data
observeEvent(input$filterButton, {
temp <- file$dfSource[(file$dfSource[,input$filterVar] %in% c(input$filterGroup)),]
file$dfSource <- temp
})
# Reset Total Sample Button
output$selectResetButton <- renderUI({
req(file$dfSource)
if (input$selectFilter) {
actionButton("resetButton", "Reset Total")
}
})
# Reset data to total sample
observeEvent(input$resetButton, {
file$dfSource <- data
updateCheckboxInput(session, "selectFilter", value = FALSE)
allfilters <- NULL
})
## Summary number of cases
output$ncases <- renderText({
req(file$dfSource)
mainTitle <- paste("Number of cases =" , nrow(file$dfSource))
return(mainTitle)
})
## Capture selected filter variables in global object
testfilter <- eventReactive(input$filterButton, {
appliedfilter <- paste0(input$filterVar, "(", input$filterGroup,")")
if (is.null(allfilters)) {
allfilters <<- paste("Selected Filters:", appliedfilter)
} else {
allfilters <<- paste(allfilters, "&", appliedfilter)
}
return(allfilters)
})
# Print out filter variables in global object
output$selectedfilters <- renderText({
filteroutput <- testfilter()
print(filteroutput)
return(filteroutput)
})
## Save out case data file
output$downloadData <- downloadHandler(
filename = function() {
paste("data-", Sys.Date(), ".csv", sep="")
},
content = function(file) {
write.csv(file$dfSource, file)
}
)
})
shinyApp(ui, server)
1) Storing it a global variable is probably not a good idea (scope in shiny is already complicated enough!). You already have a reactiveValues object, why not use that?
This alone, however, is not enough; the problem seems to be the eventReactive - I'm not quite sure why.
This works:
# this replaces the testfilter eventReactive
observeEvent(input$filterButton, {
appliedfilter <- paste0(input$filterVar, "(", input$filterGroup,")")
if (is.null(file$allfilters)) {
file$allfilters <- paste("Selected Filters:", appliedfilter)
} else {
file$allfilters <- paste(file$allfilters, "&", appliedfilter)
}
})
# Print out filter variables in global object
output$selectedfilters <- renderText({
filteroutput <- file$allfilters
print(filteroutput)
return(filteroutput)
})
2) The error is in the content function you pass to downloadHandler. The parameter is called file, which shadows the file reactiveValues. This works:
## Save out case data file
output$downloadData <- downloadHandler(
filename = function() {
paste("data-", Sys.Date(), ".csv", sep="")
},
content = function(filetarget) {
write.csv(file$dfSource, filetarget)
}
)
PS ad 1: It might be better to store the filters, instead of storing the filtered data frame and a string listing the filters. If your users change their mind, they have to start over from the beginning, but if you store the filters you can have a table or similar that allows deleting/editing individual filters. You could just store a list of two-element vectors, then iterate though the list to filter the data.
Goal: I'm working on a bioinformatics project. I'm currently trying to implement R code that dynamically creates tabPanels (they are essentially carbon copies except for the data output).
Implementation: After doing some research I implemented this solution. It works in a way (the panels that I'm "carbon copying" are created), but the data that I need cannot be displayed.
Problem: I'm sure that the way I'm displaying my data is fine. The problem is that I can't use the same output function to display the data as seen here. So let me get to the code...
ui.R
library(shiny)
library(shinythemes)
library(dict)
library(DT)
...# Irrelevant functions removed #...
geneinfo <- read.table(file = "~/App/final_gene_info.csv",
header = TRUE,
sep = ",",
na.strings = "N/A",
as.is = c(1,2,3,4,5,6,7))
ui <- navbarPage(inverse = TRUE, "GENE PROJECT",
theme = shinytheme("cerulean"),
tabPanel("Home",
#shinythemes::themeSelector(),
fluidPage(
includeHTML("home.html")
)),
tabPanel("Gene Info",
h2('Detailed Gene Information'),
DT::dataTableOutput('table')),
tabPanel("File Viewer",
sidebarLayout(
sidebarPanel(
selectizeInput(inputId = "gene", label = "Choose a Gene", choice = genes, multiple = TRUE),
selectInput(inputId = "organism", label = "Choose an Organism", choice = orgs),
selectInput(inputId = "attribute", label = "Choose an Other", choice = attributes),
width = 2),
mainPanel(
uiOutput('change_tabs'),
width = 10))),
tabPanel("Alignment")
)
I'm using uiOutput to generate tabs dynamically on the server side....
server.R
server <- function (input, output, session) {
# Generate proper files from user input
fetch_files <- function(){
python <- p('LIB', 'shinylookup.py', python=TRUE)
system(sprintf('%s %s %s', python, toString(genie), input$organism), wait = TRUE)
print('Done with Python file generation.')
# Fetch a temporary file for data output
fetch_temp <- function(){
if(input$attribute != 'Features'){
if(input$attribute != 'Annotations'){
chosen <- toString(attribute_dict[[input$attribute]])
}
else{
chosen <- toString(input$sel)
extension <<- '.anno'
}
}
else{
chosen <- toString(input$sel)
extension <<- '.feat'
}
count = 0
oneline = ''
f <- paste(toString(genie), toString(input$organism), sep = '_')
f <- paste(f, extension, sep = '')
# Writes a temporary file to display output to the UI
target <- p('_DATA', f)
d <- dict_fetch(target)
temp_file <- tempfile("temp_file", p('_DATA', ''), fileext = '.txt')
write('', file=temp_file)
vectorofchar <- strsplit(toString(d[[chosen]]), '')[[1]]
for (item in vectorofchar){
count = count + 1
oneline = paste(oneline, item, sep = '')
# Only 60 characters per line (Find a better solution)
if (count == 60){
write(toString(oneline), file=temp_file, append=TRUE)
oneline = ''
count = 0
}
}
write(toString(oneline), file=temp_file, append=TRUE)
return(temp_file)
}
# Get the tabs based on the number of genes selected in the UI
fetch_tabs <- function(Tabs, OId, s = NULL){
count = 0
# Add a select input or nothing at all based on user input
if(is.null(s)==FALSE){
selection <- select(s)
x <- selectInput(inputId = 'sel', label = "Choose an Annotation:", choices = selection$keys())
}
else
x <- ''
for(gene in input$gene){
if(count==0){myTabs = character()}
count = count + 1
genie <<- gene
fetch_files()
file_tab <- lapply(sprintf('File for %s', gene), tabPanel
fluidRow(
titlePanel(sprintf("File for %s:", gene)),
column(5,
pre(textOutput(outputId = "file")),offset = 0))
)
addTabs <- c(file_tab, lapply(sprintf('%s for %s',paste('Specific', Tabs), gene), tabPanel,
fluidRow(
x,
titlePanel(sprintf("Attribute for %s:", gene)),
column(5,
pre(textOutput(outputId = OId), offset = 0)))
))
# Append additional tabs every iteration
myTabs <- c(myTabs, addTabs)
}
return(myTabs)
}
# Select the proper file and return a dictionary for selectInput
select <- function(ext, fil=FALSE){
f <- paste(toString(genie), toString(input$organism), sep = '_')
f <- paste(f, ext, sep = '')
f <- p('_DATA', f)
if(fil==FALSE){
return(dict_fetch(f))
}
else if(fil==TRUE){
return(toString(f))
}
}
# Output gene info table
output$table <- DT::renderDataTable(
geneinfo,
filter = 'top',
escape = FALSE,
options = list(autoWidth = TRUE,
options = list(pageLength = 10),
columnDefs = list(list(width = '600px', targets = c(6))))
)
observe({
x <- geneinfo[input$table_rows_all, 2]
if (is.null(x))
x <- genes
updateSelectizeInput(session, 'gene', choices = x)
})
# Output for the File tab
output$file <- renderText({
extension <<- '.gbk'
f <- select(extension, f=TRUE)
includeText(f)
})
# Output for attributes with ony one property
output$attributes <- renderText({
extension <<- '.kv'
f <- fetch_temp()
includeText(f)
})
# Output for attributes with multiple properties (features, annotations)
output$sub <- renderText({
f <- fetch_temp()
includeText(f)
})
# Input that creates tabs and selectors for more input
output$change_tabs <- renderUI({
# Fetch all the appropriate files for output
Tabs = input$attribute
if(input$attribute == 'Annotations'){
extension <<- '.anno'
OId = 'sub'
s <- extension
}
else if(input$attribute == 'Features'){
extension <<- '.feat'
OId = 'sub'
s <- extension
}
else{
OId = 'attributes'
s <- NULL
}
myTabs <- fetch_tabs(Tabs, OId, s = s)
do.call(tabsetPanel, myTabs)
})
}
)
Explanation: Now I'm aware that there's a lot to look at here.. But my problem exists within output$change_tabs (it's the last function), which calls fetch_tabs(). Fetch tabs uses the input$gene (a list of genes via selectizeInput(multiple=TRUE)) to dynamically create a set of 2 tabs per gene selected by the user.
What's Happening: So if the user selects 2 genes then 4 tabs are created. With 5 genes 10 tabs are created... And so on and so forth... Each tab is EXACTLY THE SAME, except for the data.
Roadblocks: BUT... for each tab I'm trying to use the same output Id (since they are EXACTLY THE SAME) for the data that I want to display (textOutput(outputId = "file")). As explained above in the second link, this simply does not work because HTML.
Questions: I've tried researching several solutions, but I would rather not have to implement this solution. I don't want to have to rewrite so much code. Is there any way I can add a reactive or observer function that can wrap or fix my output$file function? Or is there a way for me to add information to my tabs after the do.call(tabsetPanel, myTabs)? Am I thinking about this the right way?
I'm aware that my code isn't commented very well so I apologize in advance. Please feel free to critique my coding style in the comments, even if you don't have a solution. Please and thank you!
I've come up with a very VERY crude answer that will work for now...
Here is the answer from #BigDataScientist
My Issue with BigDataScientist's Answer:
I can't dynamically pass data to the outputs. The output functions are not interpreted until they are needed... So if I wanted to pass the for loop iterator that you created (iter) into the dynamically created outputs, then I wouldn't be able to do that. It can only take static data
My Solution:
I end up taking advantage of sys.calls() solution I found here in order to get the name of the function as a string. The name of the function has the info I need (in this case a number).
library(shiny)
library(shinythemes)
myTabs <<- list()
conv <- function(v1) {
deparse(substitute(v1))
}
ui <- navbarPage(inverse = TRUE, "GENE PROJECT",
theme = shinytheme("cerulean"),
tabPanel("Gene Info",
sidebarLayout(
sidebarPanel(
sliderInput("bins",
"Number of bins:",
min = 1,
max = 5,
value = 3)
),
# Show a plot of the generated distribution
mainPanel(
uiOutput('changeTab')
)
)
)
)
server <- function(input, output) {
observe({
b <<- input$bins
myTabs <<- list()
# Dynamically Create output functions
# Dynamically Create formatted tabs
# Dynamically Render the tabs with renderUI
for(iter in 1:b){
x <<- iter
output[[sprintf("tab%s", iter)]] <- renderText({
temp <- deparse(sys.calls()[[sys.nframe()-3]])
x <- gsub('\\D','',temp)
x <- as.numeric(x)
f <- sprintf('file%s.txt', x)
includeText(f)
})
addTabs <<- lapply(sprintf('Tab %s', iter), tabPanel,
fluidRow(
titlePanel(sprintf("Tabble %s:", iter)),
column(5,
pre(textOutput(outputId = sprintf('%s%s','tab', iter))))))
myTabs <<- c(myTabs, addTabs)
}
myTabs <<- c(myTabs, selected = sprintf('Tab %s', x))
output$changeTab <- renderUI({
do.call(tabsetPanel, myTabs)
})
})
}
# Run the application
shinyApp(ui = ui, server = server)
I think your being a victim of this behavior. Try:
for (el in whatever) {
local({
thisEl <- el
...
})
}
like Joe suggests in the first reply to the Github issue I linked to. This is only necessary if you're using a for loop. lapply already takes el as an argument, so you get this "dynamic evaluation" benefit (for lack of a better name) for free.
For readability, I'm going to quote most of Joe's answer here:
You're the second person at useR that I talked to that was bitten by this behavior in R. It's because all the iterations of the for loop share the same reference to el. So when any of the created reactive expressions execute, they're using whatever the final value of el was.
You can fix this either by 1) using lapply instead of a for loop; since each iteration executes as its own function call, it gets its own reference to el; or 2) using a for loop but introducing a local({...}) inside of there, and creating a local variable in there whose value is assigned to el outside of the reactive.