Select and scale variables in shiny app dynamically - r

I have a shiny app where I want the user to be able to select which variables to keep in the final data frame and then also select which variables to scale into a percent. I have this working, but I am running into a little puzzle. The problem is if the user decides they want to add an additional variable (or remove one), they have to redo the scaling. This could be a problem if my users have many columns they are working on. How can I keep the scaling work the user has already done, while allowing for the addition or removal of variables from the final data frame?
library(shiny)
library(tidyverse)
library(DT)
# Define UI
ui <- fluidPage(
checkboxGroupInput("select_var", label = "Select Variables"),
selectInput("scalescore", label = NULL, choices = c("")),
actionButton("scale", "Scale Scores"),
DT::dataTableOutput("table")
)
# Define server
server <- function(session, input, output) {
# define the reactive values
values <- reactiveValues(df_final = NULL)
# dynamically generate the variable names
observe({
vchoices <- names(mtcars)
updateCheckboxGroupInput(session, "select_var", choices = vchoices)
})
# dynamically generate the variables to scale
observe({
vchoices <- names(values$df_final)
updateSelectInput(session, "scalescore", choices = vchoices)
})
# select the variables based on checkbox
observe({
req(input$select_var)
df_sel <- mtcars %>% select(input$select_var)
values$df_final <- df_sel
})
observeEvent(input$scale, {
name <- rlang::sym(paste0(input$scalescore, "_scaled"))
values$df_final <- values$df_final %>% mutate(!!name := round(!!rlang::sym(input$scalescore)/max(!!rlang::sym(input$scalescore), na.rm = TRUE)*100, 1))})
output$table <- DT::renderDataTable(values$df_final)
}
# Run the application
shinyApp(ui = ui, server = server)

We will need to maintain a vector which tracks whether a variable was scaled or not. Here is how it's done,
library(shiny)
library(tidyverse)
library(DT)
# Define UI
ui <- fluidPage(
checkboxGroupInput("select_var", label = "Select Variables"),
selectInput("scalescore", label = NULL, choices = c("")),
actionButton("scale", "Scale Scores"),
DT::dataTableOutput("table")
)
server = function(input,output,session){
#Column names are static
names = colnames(mtcars)
# data scructure to store if the variable is scaled
is_scaled = logical(length(names))
names(is_scaled) = names #Set the names of the logical vector to the column names
#Update the checkbox with the column names of the dataframe
observe({
updateCheckboxGroupInput(session, "select_var", choices = names)
})
# Update the list of choices but dont include the scaled vaiables
observe({
vchoices <- names(data())
vchoices = vchoices[vchoices %in% names]
updateSelectInput(session, "scalescore", choices = vchoices)
})
#When the scle button is pressed, the vector which contains the list of scaled variables is updated
observeEvent(input$scale,{
if(is_scaled[[input$scalescore]]){
is_scaled[[input$scalescore]] <<- FALSE
}else{
is_scaled[[input$scalescore]] <<- TRUE
}
})
#Function to scale the variables
scale = function(x){
return(round(x/max(x,na.rm = T)*100,1))
}
data = reactive({
req(input$select_var)
input$scale #simply to induce reactivity
#Select the respective columns
df = mtcars%>%
select(input$select_var)
if(any(is_scaled[input$select_var])){
temp_vec = is_scaled[input$select_var] #Get a list of variables selected
true_vec = temp_vec[which(temp_vec)] #Check which ones are scaled
true_vec_names = names(true_vec) #Get the names of the variables scales
#Scale the variables respectively
df = df%>%
mutate_at(.vars = true_vec_names,.funs = funs(scaled = scale(.)))
}
return(df)
})
output$table = DT::renderDataTable(data())
}
# Run the application
shinyApp(ui = ui, server = server)
is_scaled tracks whether a particular column is scaled or not. When it is later selected, it is scaled if the value is TRUE in this vector.
Additional functionality is also added where if the scale button is pressed twice the scale column is removed.

Related

How to group options in check box group input DT datatable for R Shiny app

I am creating a shiny app that contains a DT data table that allows the user to select the columns to be displayed as there are many fields.
At the moment, the checkbox area is looking like this:
but I would like for it to look more neat and orderly, and arranged in two subgroups: "Upper case" and "Lower case" across multiple columns.
How do I go about doing this?
My code:
library(shiny)
library(DT)
# Create data frame
column_names <- c(toupper(letters[1:26]),tolower(letters[1:26]))
df <- data.frame(replicate(length(column_names),sample(0:1,1000,rep=TRUE)))
# assign column names
colnames(df) = column_names
ui <- fluidPage(
checkboxGroupInput(
"column_selection",
h3("Select fields to display"),
choices = column_names,
inline = TRUE,
selected = c('A','B','C')
),
DT::dataTableOutput("alphabet")
)
# Define server logic required to draw a histogram
server <- function(input, output) {
output$alphabet <- DT::renderDT({
columns = column_names
if (!is.null(input$column_selection)) {
columns = input$column_selection
}
datatable(
df[, columns, drop = FALSE],
class = "row-border hover stripe",
rownames = FALSE
)
})
}
# Run the application
shinyApp(ui = ui, server = server)

R Shiny: How to expand a reactive containing a list of data.frames with an uploaded data.frame?

I'm struggeling with this one for hours:
In my app a simple test dataset df gets loaded upon starting the app. The user then may add further datasets through a file upload before selecting from a dropdown menu (here selectInput) the dataset he likes to continue working with.
What I'm failing to do:
After starting the app, the reactive df_list should only contain the initial dataset df and the dropdown menu should only hold the values c("", "df"). After adding a dataset through an upload (or else) df_list should be expanded (and the dropdown accordingly). So that I have a list containing all available datasets the user can select from.
But I only manage to create two scenarios: the dropdown menu contains df but I fail to expand the df_list after adding a dataset. Or the dropdown menu stays empty until I add a dataset, so the user has first to add a dataset before he can work with the test dataset.
My code example: I 'simulate' a file upload via an actionButton that creates the data.frame df_upload. Here follows the example without trying to expand df_list with the additional dataset df_upload.
library(shiny)
# df available from start
df <- data.frame(Var = 1:10)
ui <- fluidPage(
selectInput("select", label = "Select data", choices = c("")),
actionButton("upload", "Simulate Upload"),
tableOutput("tabdata")
)
server <- function(input, output, session) {
# reactive that lists all datasets
df_list <- reactive({list(df = df)})
# 'upload' of second df
df_upload <- eventReactive(input$upload, {
data.frame(Var = 11:20)
})
# observes if df_list() gets expanded to update choices
observeEvent(df_list(), {
updateSelectInput(session = session,
inputId = "select",
choices = c("", names(df_list())))
})
# output of selected dataset
output$tabdata <- renderTable({
req(df_list())
df_list()[[input$select]]
})
}
shinyApp(ui, server)
Here one of many things I tried (this adds df_upload succesfully, but fails to show df initially in the dropdown menu after starting the app):
library(shiny)
# df available from start
df <- data.frame(Var = 1:10)
ui <- fluidPage(
selectInput("select", label = "Select data", choices = c("")),
actionButton("upload", "Simulate Upload"),
tableOutput("tabdata")
)
server <- function(input, output, session) {
# reactive that lists all datasets
df_list <- reactive({
df_list <- list(df = df)
# check if there is an uploaded df, and if yes add it to df_list
# does not work, because it does not give me df_list only containing df
# in case no dataset was added yet.
# is.null is not the proper way, because if df_upload does not exist yet,
# it does not yield NULL. I also tried it unsuccessfully
# with exists("df_upload()")
if (!is.null(df_upload())) {
df_list[[2]] <- df_upload()
names(df_list)[2] <- "df_upload"
}
return(df_list)
})
# 'upload' of second df
df_upload <- eventReactive(input$upload, {
data.frame(Var = 11:20)
})
# observes if df_list() gets expanded to update choices
observeEvent(df_list(), {
updateSelectInput(session = session,
inputId = "select",
choices = c("", names(df_list())))
})
# output of selected dataset
output$tabdata <- renderTable({
req(df_list())
df_list()[[input$select]]
})
}
shinyApp(ui, server)
A simple solution using reactiveValues based on #Limey's comment:
library(shiny)
# df available from start
df <- data.frame(Var = 1:10)
reactlog::reactlog_enable()
ui <- fluidPage(
selectInput("select", label = "Select data", choices = c("df")),
actionButton("upload", "Simulate Upload"),
tableOutput("tabdata")
)
server <- function(input, output, session) {
# empty reactiveValues rv to store all datasets in
rv <- reactiveValues()
# store the test df in rv
rv$df <- df
# 'upload' of second df and storing it in rv
observeEvent(input$upload, {
rv$df_upload <- data.frame(Var = 11:20)
})
# update selectInput choices
observe({
updateSelectInput(session = session,
inputId = "select",
choices = names(rv),
selected = "df")
})
# output of selected dataset
output$tabdata <- renderTable({
rv[[input$select]]
})
}
shinyApp(ui, server)

How to subset data by user selection for plotting in R shiny app

I have a huge shiny app and met with the below issue. I tried to provide pseudo code for the problem since it is nearly impossible for my expertize to creating working app to demonstrate the problem. I hope i have conveyed with the pseudo code. Kindly help me.
Here is the pseudo code in ui.R file which has an actionButton and a radioButton with underlying selectizeInput and checkboxGroupInput input options and plotOutput to render a plot.
###ui.R#####
tabPanel("Plots",
fluidRow(column(4,wellPanel(
actionButton("action_plot","Generate Plots"),
h6(textOutput("numheat")),
radioButtons("plot_subset",label="Chose by sample or group?",
choices=c("Sample","Group"),selected="Sample"),
conditionalPanel("input.plot_subset=='Sample'",
selectizeInput("view_sample_plot",
label = h5("Select Samples"),
choices = NULL,
multiple = TRUE,
options = list(placeholder = 'select samples to plot')
)
),
conditionalPanel("input.plot_subset=='Group'",
checkboxGroupInput("view_group_plot",
label=h5("Select Groups to View"),
choices="",
selected="")
)
)
),
column(8,
tabsetPanel(
tabPanel(title="Plot",
#textOutput("which_genes"),
h4(textOutput("plot_title")),
plotOutput("plot_rna",height="800px")
)
)
)
)
)
Below is the pseudo server.R code that observes the user input values and updates updateSelectizeInput and updateCheckboxGroupInput with choice from the default loaded R dataset. The user selected choices are used in the subsequent function to generate plot.
###server.R#####
## observed the user input and updated the selectize input and checkBoxGroup input values#####
observe({
print("server-plot-update")
# browser()
data_analyzed = inputData()
tmpgroups = data_analyzed$group_names
tmpdatlong = data_analyzed$data_long
tmpsamples = unique(tmpdatlong$sampleid)
tmpynames = tmpdatlong$
updateSelectizeInput(session,'view_sample_plot',
choices=tmpsamples, selected=NULL)
updateCheckboxGroupInput(session,'view_group_plot',
choices=tmpgroups, selected=NULL)
})
#####code to render plot based on user selection value i.e. by group or samples######
##plot_render utilizes the R functions in Plot.R file to subset the data by user input and generate plot###
plotdatReactive <- reactive({
data_analyzed = inputData
tmp <- plot_data(data_analyzed = data_analyzed,
yname="log2",
orderby="significance",
view_group=input$view_group_plot,
view_sample=input$view_sample_plot)
tmp
})
output$plot_rna <- renderPlot({
if(input$action_plot==0) return()
isolate({
tmp = plotdatReactive()
plot_render( data_analyzed=tmp,
yname = input$heatmapvaluename,
view_group=input$view_group_plot,
view_sample=input$view_sample_plot
)
})
})
Pseudo Code for R functions in plot.R file
####plot.R#####
###function to subset data based on user input samples or groups###
plot_subdat <- function(data_analyzed,
yname="log2",
orderby="significance",
view_sample=NULL,
view_group=NULL) {
if(is.null(view_sample)) view_sample=unique(data_analyzed$sampleid) ## sample names in the dataset
if(is.null(view_group)) view_group=data_analyzed$group_names ## group names in the dataset
tmpdat = data_analyzed$data_long
##subset dataset by **sampleid** if the user selected **samples** in SelectizeInput
tmpdat = tmpdat%>%filter(sampleid%in%view_sample)
subdat = filter(data_analyzed$data_long,unique_id%in%thesegenes,sampleid%in%view_sample)
#subset dataset by **group** if the user selected **group** in checkBoxGroup input
tmpdat = tmpdat%>%filter(group%in%view_group)
subdat = filter(data_analyzed$data_long,unique_id%in%thesegenes,group%in%view_group)
}
###this function generates the plot on the subset of data from the above function#####
plot_data <- function(...) {
tmpdat = plot_subdat(...)
plotdat = tmpdat$data
plotdat
}
The tmpdat and subdat are the inputs to generate the plot in plot_render function. If the user selects and inputs values through selectizeInput then the subsetting of data should be done by samples. If the user selects and input through checkBoxGroupInput then the subsetting should be done by group as commented in the code. I am not unable to subset the data based on user selection i.e. sample/group reactively in plot_subdat function. How can i do this reactively so that the output plot is generated as per the user selection.
I think you might want a reactive expression to subset your data.
Here is a basic working example that includes your inputs, and will plot subsetted data based on input selections reactively.
Edit:
The filtering of data is now in an external .R file, with input variables to filter on passed through.
library(shiny)
source("plot.R", local = TRUE)
ui <- fluidPage(
mainPanel(
tabsetPanel(
tabPanel("Plots",
fluidRow(column(4,wellPanel(
#actionButton("action_plot","Generate Plots"),
h6(textOutput("numheat")),
radioButtons("plot_subset",label="Chose by sample or group?",
choices=c("Sample","Group"),selected="Sample"),
conditionalPanel("input.plot_subset=='Sample'",
selectizeInput("view_sample_plot",
label = h5("Select Samples"),
choices = NULL,
multiple = TRUE,
options = list(placeholder = 'select samples to plot')
)
),
conditionalPanel("input.plot_subset=='Group'",
checkboxGroupInput("view_group_plot",
label=h5("Select Groups to View"),
choices="",
selected="")
)
)),
column(8,
tabsetPanel(
tabPanel(title="Plot",
#textOutput("which_genes"),
h4(textOutput("plot_title")),
plotOutput("plot_rna",height="800px")
)
)
)
)
)
)
)
)
server <- function(input, output, session) {
observe({
updateSelectizeInput(session,'view_sample_plot',
choices=unique(mtcars$gear), selected=NULL)
updateCheckboxGroupInput(session,'view_group_plot',
choices=unique(mtcars$cyl), selected=NULL)
})
plot_prepare <- reactive({
if (input$plot_subset == "Sample") {
plot_subdat(mtcars, "gear", input$view_sample_plot)
} else {
plot_subdat(mtcars, "cyl", input$view_group_plot)
}
})
output$plot_rna <- renderPlot({
plot(plot_prepare())
})
}
shinyApp(ui, server)
plot.R
# plot.R file
library(tidyverse)
plot_subdat <- function(data, variable, choices) {
data %>%
filter((!!sym(variable)) %in% choices) %>%
select(c(!!sym(variable), mpg))
}

Dynamically display column names in shiny app flashes error when dataset is changed

I have a shiny app where I want to allow the user to select a dataset based on a set of uploaded files and then specify the columns to display from the selected dataset. If I leave some columns selected and then switch datasets, an error flashes and is output to the console stating that the selected columns are unknown before the app switches datasets and displays it correctly. In my full app however, the app crashes, though I wasn't able to figure out how to reproduce the crash. I thought it might be related to some preprocessing that is done to add additional columns which are the same across datasets and which remain selected, but the error is the same without that feature.
library(shiny)
library(tidyverse)
library(DT)
ui <- fluidPage(
checkboxGroupInput("select_var", label = "Select Variables"),
selectInput("dataset", label = NULL, choices = c("mtcars", "rock")),
DT::dataTableOutput("table")
)
server <- function(session, input, output) {
# define the dataset
data <- reactive({switch(input$dataset,"rock" = rock,"mtcars" = mtcars)})
# add a common column name that is always selected
dataprocessed <- reactive({data <- data()
data$num <- seq(1:nrow(data))
return(data)})
# dynamically generate the variable names
observe({
vchoices <- names(dataprocessed())
updateCheckboxGroupInput(session, "select_var", choices = vchoices, selected = c("num"))
})
# select the variables based on checkbox
data_sel <- reactive({
req(input$select_var)
df_sel <- dataprocessed() %>% select(input$select_var)
})
output$table <- DT::renderDataTable(data_sel())
}
# Run the application
shinyApp(ui = ui, server = server)
We can add a conditional requirement using req() to test for column existence before rendering:
library(shiny)
library(tidyverse)
library(DT)
ui <- fluidPage(
checkboxGroupInput("select_var", label = "Select Variables"),
selectInput("dataset", label = NULL, choices = c("mtcars", "rock")),
DT::dataTableOutput("table")
)
server <- function(session, input, output) {
# define the dataset
data <- reactive({
switch(input$dataset,"rock" = rock,"mtcars" = mtcars)
})
# add a common column name that is always selected
dataprocessed <- reactive({
data <- data()
data$num <- seq(1:nrow(data))
return(data)
})
# dynamically generate the variable names
observe({
vchoices <- names(dataprocessed())
updateCheckboxGroupInput(session, "select_var", choices = vchoices, selected = c("num"))
})
# select the variables based on checkbox
data_sel <- reactive({
req(input$select_var)
req(names(dataprocessed()) %in% input$select_var)
a <- names(dataprocessed())[names(dataprocessed()) %in% input$select_var]
df_sel <- dataprocessed() %>% select(a)
})
output$table <- DT::renderDataTable(data_sel())
}
# Run the application
shinyApp(ui = ui, server = server)

Dynamically update position of sliderInput in Shiny

I have the following code to dynamically make either Check Boxes or Sliders.
server <- shinyServer(function(input, output, session) {
# define the data frame to use
dat <- mtcars
dat <- rownames_to_column(dat, "car")
# name of availale data frame
varNames <- names(dat)
# define defaul values as the first value in each column
defaultValues <- as.numeric(dat[1,])
# store the selected variable in a reactive variable
# dynamically creates a set of sliders
output$controls <- renderUI({
div(
fluidRow(
column(9, uiOutput("rangeUI"))
)
)
})
output$rangeUI <- renderUI({
lapply(1:length(varNames), function(k) {
fluidRow(
column(12,
if (is_character(dat[1, k])) {
# a slider range will created only is the variable is selected
checkboxGroupInput(paste0("slider_", varNames[k]), label = varNames[k], choices = unique(dat[[k]]), selected = NULL,
inline = FALSE, width = NULL, choiceNames = NULL, choiceValues = NULL)
} else {
# otherwise uses single value with a default value
sliderInput(paste0("slider_", varNames[k]), label = varNames[k],
min = 0, max = 100, value = defaultValues[k])
}
)
)
})
})
The issue I am running into is that I would like to display the sliders and check boxes side by side until they hit the screen width and then start a new row. Currently, they are all in one column.
Is there a good way to dynamically adjust offset to accomplish this, maybe something like this?
column(12, offset = match(k, colnames(dat)), # then lead into the if else statement
Any other suggestions on building the UI are welcome.
Try to put the fluidRow outside the lapply and change the size of the column from 12 to maybe 3, otherwise you are creating multiple rows with only one column, instead on one row with multiple columns.
Below is your code modified, maybe it could help you.
library(shiny)
library(tibble)
ui <- fluidPage(
uiOutput("controls")
)
server <- shinyServer(function(input, output, session) {
# define the data frame to use
dat <- mtcars
dat <- rownames_to_column(dat, "car")
# name of availale data frame
varNames <- names(dat)
# define defaul values as the first value in each column
defaultValues <- as.numeric(dat[1,])
# store the selected variable in a reactive variable
# dynamically creates a set of sliders
output$controls <- renderUI({
fluidRow(
column(offset = 3, 9, uiOutput("rangeUI"))
)
})
# to test that a dynamically created input works with an observer
observeEvent(input$slider_mpg, {
cat("slider_mpg:", input$slider_mpg, "\n")
})
output$rangeUI <- renderUI({
fluidRow(
lapply(1:length(varNames), function(k) {
column(3,
if (is.character(dat[1, k])) {
# a slider range will created only is the variable is selected
checkboxGroupInput(paste0("slider_", varNames[k]), label = varNames[k], choices = unique(dat[[k]]), selected = NULL,
inline = FALSE, width = NULL, choiceNames = NULL, choiceValues = NULL)
} else {
# otherwise uses single value with a default value
sliderInput(paste0("slider_", varNames[k]), label = varNames[k],
min = 0, max = 100, value = defaultValues[k])
}
)
})
)
})
})
shinyApp(ui = ui, server = server)
Update:
You can get the values of dynamically created inputs by using an action button as is explained here or get them automatically by using the solution explained here.

Resources