I created a dynamic form using {gt} and {purrr} combined with a function that extracts the HTML of shiny::selectizeInput.
I need to ensure that inputs created dynamically with {purrr} are available for others operations.
How can I use shiny::req() for all inputs created dynamically?
# libraries
library(shiny)
library(magrittr)
# functions
selectizeInput_gt <- function(value, inputid, ...) {
as.character(shiny::selectizeInput(paste0(value, inputid),
...)) %>%
gt::html()
}
# datasets
number_tests <- 5
df <- data.frame("test_number" = 1:number_tests)
# UI
ui <- fluidPage(gt::gt_output(outputId = "table"))
# server
server <- function(input, output, session) {
output$table <- gt::render_gt({
req(df)
df %>%
tibble::rownames_to_column() %>%
dplyr::rowwise() %>%
dplyr::mutate(
rowname = as.numeric(rowname),
selectinput_column = purrr::map(
rowname,
.f = ~ selectizeInput_gt(
.x,
"_selectinput",
label = "",
choices = c("A", "B", "C")
)
)
) %>%
gt::gt()
})
}
# runApp
shinyApp(ui, server)
After a lot of fiddling, I figured out a way to use purrr::walk to pass the inputs to req(). To generate the inputs, I use purrr::map.
As a small example, I use the req() to prevent an error in a simple output that uses the values of the inputs.
# libraries
library(shiny)
library(magrittr)
# functions
selectizeInput_gt <- function(value, inputid, ...) {
as.character(shiny::selectizeInput(paste0(value, inputid),
...)) %>%
gt::html()
}
# datasets
number_tests <- 5
df <- data.frame("test_number" = 1:number_tests)
# UI
ui <- fluidPage(gt::gt_output(outputId = "table"),
textOutput("selections"))
# server
server <- function(input, output, session) {
output$table <- gt::render_gt({
req(df)
df %>%
tibble::rownames_to_column() %>%
dplyr::rowwise() %>%
dplyr::mutate(
rowname = as.numeric(rowname),
selectinput_column = purrr::map(
rowname,
.f = ~ selectizeInput_gt(
.x,
"_selectinput",
label = "",
choices = c("A", "B", "C")
)
)
) %>%
gt::gt()
})
output$selections <- renderText({
purrr::walk(purrr::map(paste0(df$test_number, "_selectinput"), ~input[[.]]), req)
paste(purrr::map_chr(paste0(df$test_number, "_selectinput"), ~input[[.]]))
})
}
# runApp
shinyApp(ui, server)
Related
This is my code
library(shiny)
library(tidyverse)
mylist <- c(1,2,3) %>% as.list()
ui <- fluidPage(
plotOutput('chart_1'),
plotOutput('chart_2'),
plotOutput('chart_3')
)
server <- function(input, output, session) {
lapply(1:3, function(x){
output[[paste0("chart_", x)]] <-
renderPlot({ ggplot(gapminder::gapminder %>% filter(country == 'Chile'), aes(x = year, y = pop))+ geom_line()})
})
}
shinyApp(ui, server)
How can I achieve the same result using purrr::map function instead lapply?
I am doing something like this, but I have a error message:
mylist %>% map(~
output[[glue::glue("chart_{.x}")]] <-
renderPlot({ ggplot(gapminder::gapminder %>% filter(country == 'Chile'),
aes(x = year, y = pop))+ geom_line()})
)
I am writing a Shiny program which manipulates a dataset the user uploads.
The dataset has fixed column names and I create several UI elements (selectInputs) to filter that dataset.
Reprex looks like this:
ui <- fluidPage(
fluidRow(selectInput("filter_a","label",choices = c("a","b","c"),multiple = T),
selectInput("filter_b","label",choices = c("x","z","y"),multiple = T),
dataTableOutput("o1"),
br(),
dataTableOutput("o2")
)
)
server <- function(input, output) {
df <- reactive({
df <- data.frame(a = c("a","b","c"),
b = c("x","z","y"))
})
filter_function_1 <- reactive({
req(data)
df <- df()
if(!is.null(input$filter_a)){
df <- df %>%
filter(df$a %in% input$filter_a)
}
if(!is.null(input$filter_b)){
df <- df %>%
filter(df$b %in% input$filter_b)
}
return(df)
})
output$o1 <- renderDataTable({filter_function_1()})
While this works it looks like very bad practice. In my actual program I have a set of 14 filters and wrapping it 14 times and applying the same just doesnt look right to me.
Wanting to simplify I came up with this. I have a feeling that this is also not best practice (addressing the input$filter_a by concatenating strings doesnt seem right).
filter_func <- function(df, arg) {
filter_arg <- paste0("filter_", arg)
filter <- paste0("input$", filter_arg)
if (!is.null(eval(parse(text = filter)))) {
df <- df %>%
filter(df[[arg]] %in% input[[filter_arg]])
}
return(df)
}
filter_function_2 <- reactive({
df <- df()
df <- df %>%
filter_func(arg="a") %>%
filter_func(arg="b")
return(df)
})
output$o2 <- renderDataTable({filter_function_2()})
}
Now, this looks cleaner to me, but I still want to modulize the code even more and have the filter function and code resign in a file. There are more data prep steps involved and I want to be able to debug them easily, hence the separate files / functions.
Code might look now like this:
filter_data.R
filter_func <- function(df, arg) {
filter_arg <- paste0("filter_", arg)
filter <- paste0("input$", filter_arg)
if (!is.null(eval(parse(text = filter)))) {
df <- df %>%
filter(df[[arg]] %in% input[[filter_arg]])
}
return(df)
}
This is the point where it doesn't work anymore, since it can't find the input while in the function scope - that would be at least my best guess. I though of rewriting function in several ways, these are my ideas:
Have the filer_data.R function take in named arguments for all columns I want to filter. This seems straight-forward but also very redundant to me
Access shiny input variable on the server side, collect all "columns" that start with "filter_" and pass them onto the filter function. The filter function then applies the necessary filters.
I'm pretty sure I mess up somewhere, but I haven't been able to figure it out. What's not working here?
First, lets solve the problem how to call several filter in a row based on multiple inputs. We can use purrr:reduce2 for this:
In the example below reduce2 takes a custom function called myfilter with three arguments: the initial data.frame the column name and the value we want to filter. When calling reduce2 it is important to supply the data.frame to the .init argument.
library(shiny)
library(tidyverse)
myfilter <- function(df, col, vals) {
if(!is.null(vals)) {
filter(df, !!sym(col) %in% vals)
} else {
df
}
}
shinyApp(ui = fluidPage(
fluidRow(selectInput("filter_a","label", choices = c("a","b","c"), multiple = TRUE),
selectInput("filter_b","label", choices = c("x","z","y"), multiple = TRUE),
dataTableOutput("o1"),
br(),
dataTableOutput("o2")
)
),
server = function(input, output) {
df <- reactive({
df <- data.frame(a = c("a","b","c"),
b = c("x","z","y"))
})
filter_function_1 <- reactive({
req(data)
filter_ls <- map(set_names(grep("^filter", names(input), value = TRUE)), ~ input[[.x]])
col_nms <- gsub("^filter_", "", names(filter_ls))
reduce2(col_nms,
filter_ls,
myfilter,
.init = df())
})
output$o1 <- renderDataTable({filter_function_1()})
})
Then we could create a separate function filter_function_1 with two arguments: react_dat and input.
library(shiny)
library(tidyverse)
myfilter <- function(df, col, vals) {
if(!is.null(vals)) {
filter(df, !!sym(col) %in% vals)
} else {
df
}
}
filter_function_1 <- function(reac_dat, input) {
reactive({
filter_ls <- map(set_names(grep("^filter", names(input), value = TRUE)),
~ input[[.x]])
col_nms <- gsub("^filter_", "", names(filter_ls))
reduce2(col_nms,
filter_ls,
myfilter,
.init = reac_dat)
})
}
shinyApp(ui = fluidPage(
fluidRow(selectInput("filter_a","label", choices = c("a","b","c"), multiple = TRUE),
selectInput("filter_b","label", choices = c("x","z","y"), multiple = TRUE),
dataTableOutput("o1"),
br(),
dataTableOutput("o2")
)
),
server = function(input, output) {
df <- reactive({
df <- data.frame(a = c("a","b","c"),
b = c("x","z","y"))
})
filter_dat <- filter_function_1(df(), input = input)
output$o1 <- renderDataTable({filter_dat()})
})
And another way cleaning the code by putting it in external function / files would be to use shiny modules. There are many ways to set this up depending on how this module interacts with other parts of your app. One way of doing this is putting everything into the module:
library(shiny)
library(tidyverse)
myfilter <- function(df, col, vals) {
if(!is.null(vals)) {
filter(df, !!sym(col) %in% vals)
} else {
df
}
}
filterFunUI <- function(id) {
tagList(
fluidRow(selectInput(NS(id, "filter_a"),"label", choices = c("a","b","c"), multiple = TRUE),
selectInput(NS(id, "filter_b"),"label", choices = c("x","z","y"), multiple = TRUE),
dataTableOutput(NS(id, "o1")),
br(),
dataTableOutput(NS(id, "o2")))
)
}
filterFunServer <- function(id) {
moduleServer(id, function(input, output, session) {
df <- reactive({
df <- data.frame(a = c("a","b","c"),
b = c("x","z","y"))
})
filter_dat <- reactive({
filter_ls <- map(set_names(grep("^filter", names(input), value = TRUE)),
~ input[[.x]])
col_nms <- gsub("^filter_", "", names(filter_ls))
reduce2(col_nms,
filter_ls,
myfilter,
.init = df())
})
output$o1 <- renderDataTable({filter_dat()})
})
}
ui <- fluidPage(filterFunUI("first"))
server <- function(input, output, session) {
filterFunServer("first")
}
shinyApp(ui = ui, server = server)
Please have a look at the reprex at the end of this post.
I have a tibble with mix of numerical and non-numerical columns.
The numerical values are all rounded up to two decimals.
I use formatStyle and I manually select the numerical columns which I want to be right aligned.
Unfortunately, in the real-life shiny app I do not know each time how many numerical columns I will have, so I need a way to select them automatically in order to align them.
It must be a one-liner, but so far I have been unsuccessful.
Anyone can help me here?
Many thanks!
library(shiny)
library(tidyverse)
library(DT)
#>
#> Attaching package: 'DT'
#> The following objects are masked from 'package:shiny':
#>
#> dataTableOutput, renderDataTable
round_all <- function(df, n){
res <- df %>% mutate(across(where(is.numeric), ~round(.x,n) ))
return(res)
}
set.seed(1234)
df <- tibble(x=letters[1:5], y=LETTERS[10:14],
w=rnorm(5), z=rnorm(5)) %>%
round_all(2)
ui <- fluidPage(
mainPanel(DTOutput("table"))
)
server <- function(input, output) {
output$table <- renderDT({datatable(df)} %>%
formatStyle(columns=c("w", "z"),
textAlign = 'right')
)
}
shinyApp(ui = ui, server = server)
#>
#> Listening on http://127.0.0.1:7374
Created on 2021-09-21 by the reprex package (v2.0.1)
You can write e.g. numeric_cols <- df %>% keep(is.numeric) %>% colnames():
library(shiny)
library(tidyverse)
library(DT)
round_all <- function(df, n) {
res <- df %>% mutate(across(where(is.numeric), ~ round(.x, n)))
return(res)
}
set.seed(1234)
df <- tibble(
x = letters[1:5], y = LETTERS[10:14],
w = rnorm(5), z = rnorm(5)
) %>%
mutate_if(is.numeric, ~ .x %>% round(2))
ui <- fluidPage(
mainPanel(DTOutput("table"))
)
server <- function(input, output) {
numeric_cols <- df %>% keep(is.numeric) %>% colnames()
output$table <- renderDT({
datatable(df)
} %>%
formatStyle(
columns = numeric_cols,
textAlign = "right"
))
}
shinyApp(ui = ui, server = server)
I'm trying to get a function work within a shiny app, but it doesn't work as expected.
Outside of the app it works fine,
But within the app, it doesn't work:
Is it because the input$var isn't working as expected? (The checkbox also doesn't work and I'm still trying to figure that out.) My main question is about the function.
Code:
library(shiny)
if (interactive()) {
one <- function(.data, var, na = TRUE) {
if (na == FALSE)
return({
.data %>%
group_by({{var}}) %>%
drop_na() %>%
tally() %>%
mutate(`%` = 100*n/sum(n))
})
if (na == TRUE)
return({
.data %>%
group_by({{var}}) %>%
# drop_na() %>%
tally() %>%
mutate(`%` = 100*n/sum(n))
})
}
ui <- fluidPage(
selectInput("var", label = "Select Variable", choices = c(" ", names(mtcars))),
checkboxInput("check", "Display missing", FALSE),
tableOutput("value")
)
server <- function(input, output) {
output$value <- renderTable({
req(input$var)
if (input$check) ({
mtcars %>%
one(input$var, na = TRUE)
})
if(!input$check) ({
mtcars %>%
one(input$var, na = FALSE)
})
})
}
shinyApp(ui, server)
}
Dataset with missing values:
df <- data.frame(col1 = c(1:3, NA),
col2 = c("this", NA,"is", "text"),
col3 = c(TRUE, FALSE, TRUE, TRUE),
col4 = c(2.5, 4.2, 3.2, NA),
stringsAsFactors = FALSE)
input$var is a character value whereas one function is written for unquoted variables. You can change your function to work for character values.
Other changes that I did in the code are -
Replace na == FALSE and na == TRUE to !na and na respectively.
Since you want to keep the first value in selectInput as blank, used if(input$var != '') instead of req(input$var) because input$var would always have a value.
library(shiny)
library(dplyr)
if (interactive()) {
one <- function(.data, var, na = TRUE) {
if (!na)
return({
.data %>%
group_by(.data[[var]]) %>%
filter(!is.na(.data[[var]])) %>%
tally() %>%
mutate(`%` = 100*n/sum(n))
})
if (na)
return({
.data %>%
group_by(.data[[var]]) %>%
tally() %>%
mutate(`%` = 100*n/sum(n))
})
}
ui <- fluidPage(
selectInput("var", label = "Select Variable", choices = c("", names(df))),
checkboxInput("check", "Display missing", FALSE),
tableOutput("value")
)
server <- function(input, output) {
output$value <- renderTable({
if(input$var != '') {
data <- df %>% one(input$var, na = input$check)
return(data)
}
})
}
shinyApp(ui, server)
}
Use get() to accomplish your needs. Also, you can use .data[[!!input$var]] to get the appropriate name in the header of the displayed table.
one <- function(.data, var, na = TRUE) {
if (na == FALSE)
return({
.data %>%
group_by({{var}}) %>%
filter(!is.na({{var}})) %>%
tally() %>%
mutate(`%` = 100*n/sum(n))
})
if (na == TRUE)
return({
.data %>%
group_by({{var}}) %>%
# drop_na() %>%
tally() %>%
mutate(`%` = 100*n/sum(n))
})
}
ui <- fluidPage(
selectInput("var", label = "Select Variable", choices = c(" ",names(mtcars))),
checkboxInput("check", "Display missing", FALSE),
tableOutput("value")
)
server <- function(input, output) {
output$value <- renderTable({
if (!is.null(input$var)) {
if (input$var == " " | is.na(input$var)) {
df <- mtcars ## choose what you want to display when input$var is missing; NULL if you want to show nothing
}else {
df <- mtcars %>% one(.data[[!!input$var]], na = req(input$check))
}
}else df <- NULL
df
})
}
shinyApp(ui, server)
I am trying to get the suffix of an output$suffix name in R Shiny and incorporate it into the input$suffix_rows_selected function. The drilldown table is coming empty. Would someone have any idea of what am I doing wrong?
Function that I am trying to build:
f.drilldata <- function(base.summary, base.drilldown, suffix.output, group_var){
group = enquo(group_var)
base.summary = base.summary %>% mutate(var = !!group)
base.drilldown = base.drilldown %>% mutate(var = !!group)
#input = expr(!!glue("input${suffix.output}_rows_selected"))
input = paste0(suffix.output,'_rows_selected')
validate(need(length(input[[input]]) > 0, ''))
selected_rows <- base.summary[as.integer(input[[input]]), ]$var
base.drilldown[base.drilldown$var %in% selected_rows, ]
}
Error Example:
library("dplyr")
library("shiny")
library("DT")
tbl.summary <- group_by(iris, Species) %>% summarise(Count = n())
tbl.drilldown <- iris
ui <- fluidPage(
DTOutput("output.summary.name")
, DTOutput("output.drilldown.name"))
server <- function(input, output){
# display the data that is available to be drilled down
output$output.summary.name <- renderDT(tbl.summary)
# subset the records to the row that was clicked through f.drilldata function
drilldata <- reactive({ f.drilldata(tbl.summary, tbl.drilldown, 'output.summary.name', Species) })
# display the subsetted data
output$output.drilldown.name <- renderDT(drilldata())}
shinyApp(ui, server)
Example that works but out of the f.drilldata function
library("dplyr")
library("shiny")
library("DT")
tbl.summary <- group_by(iris, Species) %>% summarise(Count = n())
tbl.drilldown <- iris
ui <- fluidPage(
DTOutput("output.summary.name")
, DTOutput("output.drilldown.name"))
server <- function(input, output){
output$output.summary.name <- renderDT(tbl.summary)
drilldata <- reactive({ validate( need(length(input$output.summary.name_rows_selected) > 0, "Select rows to drill down!"))
selected_species <-
tbl.summary[as.integer(input$output.summary.name_rows_selected), ]$Species
tbl.drilldown[tbl.drilldown$Species %in% selected_species, ] })
output$output.drilldown.name <- renderDT(drilldata())}
shinyApp(ui, server)
I have found a simple solution by just adding the entire input (input$output.summary.name_rows_selected) as an argument of the function as below.
library("dplyr")
library("shiny")
library("DT")
f.drilldata <- function(base.summary, base.drilldown, input, group_var){
group = enquo(group_var)
base.summary = base.summary %>% mutate(var = !!group)
base.drilldown = base.drilldown %>% mutate(var = !!group)
validate(need(length(input) > 0, ''))
selected_rows <- base.summary[as.integer(input), ]$var
base.drilldown[base.drilldown$var %in% selected_rows, ]
}
tbl.summary <- group_by(iris, Species) %>% summarise(Count = n())
tbl.drilldown <- iris
ui <- fluidPage(
DTOutput("output.summary.name")
, DTOutput("output.drilldown.name"))
server <- function(input, output){
output$output.summary.name <- renderDT(tbl.summary)
drilldata <- reactive({ f.drilldata(tbl.summary, tbl.drilldown,
input$output.summary.name_rows_selected, Species) })
output$output.drilldown.name <- renderDT(drilldata())}
shinyApp(ui, server)