I would like to render Shiny tabs (and other Shiny features) within an Rmarkdown document. Specifically, I'd like to display a table in each tab, generated with a loop.
We can add {.tabset} tag after a header and subsequent lower level headers will be separate tabs. I tried to make use of this by using cat() and the header level and a unique name at each loop for the tab name. That creates the tabs fine. Also, a table is created within each tab when I call the function with arguments separately.
However, when looping through the function, the tables are not being generated on each tab. In the final tab is output
<shiny.render.function> for each loop.
I know if I was generating a table in Rmarkdown without Shiny, I'd print each table within each loop. However, there's no similar function for printing rendered Shiny objects.
---
output: html_document
runtime: shiny
---
## Tables Work {.tabset}
{r works, results='asis'}
# Function to generate a numbered tab and table within that tab
tbl_display_func <- function(tab_number, df){
cat('### Results', tab_number,' \n\n')
DT::renderDataTable({
DT::datatable(
head(df)
)
})
}
# Works fine to call the function separately
tbl_display_func(1, cars)
tbl_display_func(2, iris)
Tables Don't Work {.tabset}
However, when I loop through the function, tables do not display.
{r do-not-work, results='asis'}
# Does not work here. Generates tabs, then list of <shiny.render.function> in tab 2
purrr::map2(
.x = list(1,2),
.y = list(cars, iris),
.f = tbl_display_func
)
I tried to create the tables as renderTableOutput output, as I might if creating multiple widgets just in shiny, and then calling those within a function, but this didn't help
Tables Don't Work {.tabset}
{r generate-tables-then-print, results='asis'}
# Function just to generate table
tbl_func <- function(df){
DT::renderDataTable({
DT::datatable(
head(df)
)
})
}
# Store rendered table as output
tbl_func_output <- purrr::map(
.x = list(cars, iris),
.f = tbl_func
)
# Loop through each tab and table
display_func <- function(tab_number){
cat('### Results', tab_number,' \n\n')
tbl_func_output[[tab_number]]
}
purrr::map(
.x = 1:2,
.f = display_func
)
How might I loop through and create a table at each tab?
You need to do this
---
output: html_document
runtime: shiny
---
```{r}
do.call(tabsetPanel,
mapply(y = list(cars, iris), x = list(1,2), function(x, y) {
tabPanel(
paste0("Results", x), DT::renderDataTable({DT::datatable(head(y))})
)
}, SIMPLIFY = FALSE)
)
```
return of purrr::map2 is a list, not shiny.tag object. I don't think there is a way to have .tabset work with cat and DT::datatable together. You would choose only flexdashboard or shiny. Here I used shiny syntax.
Related
I am wondering if it is possible to create a number of tabs in flexdashboard which change according to some user input. As a toy example, I am trying to plot histograms of every numeric numeric column of a user-selected dataset, with each numeric column getting its own tab for a histogram. If the user changes the dataset, the dashboard should update and increase/decrease the number of tabs in the tabset, if the number of numeric columns in the newly selected dataset changes. For simplicity, I only included two datasets.
---
title: "Reactive GGPlot Tabs"
output:
flexdashboard::flex_dashboard:
theme:
version: 4
bootswatch: cosmo
runtime: shiny
---
```{r}
pacman::p_load(tidyverse, shiny, flexdashboard)
```
Sidebar {.sidebar}
-------------------------------------
<h5>Required Input</h5>
```{r}
selectInput(inputId = "dataset", label = "Data:",
choices = c('mtcars', 'iris'), selected = 'iris')
dataset = reactive({
set = as.character(input$dataset)
if(set=='iris'){data = iris} else{data = mtcars}
return(data)
})
```
```{r setup}
chart = reactive({
data = dataset()
num_cols <- unlist(lapply(data, is.numeric))
data = data[,num_cols] # only keep numeric columns for histogram
labels <- data %>% names # these will serve as labels for each tab
chart <- purrr::map(.x = data, ~ggplot(data, aes(x=.x))+geom_histogram()+theme_bw()) %>% setNames(labels) # assign names to each element to use later as tab titles
return(chart)
})
```
## Column {.tabset .tabset-fade}
```{r}
out <- reactive({
lapply(seq_along(chart()), function(i) {
a1 <- knitr::knit_expand(text = sprintf("### %s\n", names(chart())[i])) # tab header, auto extracts names of `chart`, %s indicates string
a2 <- knitr::knit_expand(text = "\n```{r, message = F, warning = F}") # start r chunk
a3 <- knitr::knit_expand(text = sprintf("\nchart()[[%d]]", i)) # extract graphs by "writing" out `chart[[1]]`, `chart[[2]]` etc. to be rendered later, %d indicates integer variable
a4 <- knitr::knit_expand(text = "\n```\n") # end r chunk
paste(a1, a2, a3, a4, collapse = '\n') # collapse together all lines with newline separator
})
})
```
`r reactive({knitr::knit(text = paste(out(), collapse = '\n'))})`
When this code is run, the dashboard displays the markdown chunk code, but does not render plots or tabs.
I am able to sightly modify this code to work without reactive, but I need the number of tabs to increase/decrease according to the chosen dataset. Also, I am not allowed to use facet_wrap.
Is this possible?
I'm working on a project to make it easier to create flex/shiny dashboards from qualtrics surveys. I'd really like to be able to write a couple functions that would let co-workers who have less experience with R be able to make similar documents without having to know Rmarkdown syntax.
For example, if someone wanted to make a one page dashboard with a scatterplot, I'd like to be able to have them use a couple functions like (make_dashboard, make_page) etc:
make_dashboard(
title = "Qualtrics Report Dashboard",
page 1 = make_page(header = "Page 1", format = "column", render = "plot",
data = survey_data, variables = c("var1", "var2"))
)
which would then create a rmd file with this:
---
title: "Qualtrics Report Dashboard"
output:
flexdashboard::flex_dashboard:
orientation: columns
vertical_layout: scroll
runtime: shiny
---
Page 1
=====================================
renderPlot( {
ggplot(data = survey_data, mapping = aes_string(x = var1,
y = var2)) +
geom_point() +
labs(x = get_label(get(var1, survey_data)),
y = get_label(get(var2, survey_data)))
}
)
I haven't gotten very far with trying to write these functions / implement this logic, because I'm not even sure if I'm thinking about it in the right way - is it possible to create rmarkdown chunks with functions like this?
I've looked at other posts 1 and 2 about child documents in knitr, but I don't really want every chunk to be the same, rather have the person be able to change certain aspects (e.g. type of plot, data, etc.).
Not sure if this will be useful to anyone else, but I ended up using whisker (https://github.com/edwindj/whisker), which can render strings into documents to construct an Rmd in the style of flexdashboard.
TLDR: Essentially I made functions that create strings of text matching the building blocks of flexdashboard. With whisker, you can pass in variables by encasing words in the string with two bracket parentheses and then assigning their values with a list of var_name = value for each variable in the string, e.g.
template <- "My name is {{name}}."
d <- list(name = "Emily")
cat(whisker.render(template, data = d))
print(d)
My name is Emily
I used a combination of this and the str_c from stringr to construct strings for different elements of the flexdashboard, allowing the user to input variables like title, variable for plots, etc. that then could be rendered into the string using whisker. Then, I joined all of those strings together and render it into an Rmd file. Honestly, I am not sure this is actually easier for people who don't know R to use, and I'll probably end up doing something different, but I wanted to share in case anyone is thinking about this.
Example: running the chunk below creates a file called "test_dashboard.Rmd" with the text format for a flexdashboard with a 1 input sidebar and a single page with one plot.
```
make_dashboard(title = "Test Dashboard",
sidebar = make_sidebar(sidebar_title = "here is the input",
input_type = "multi-select",
input_name = "Interesting Var #1"),
page1 = make_page(page_title = "Cool Plots!",
element_one = make_plot(plot_title = "this is my plot",
type = "bivariate",
vars = c("cool_var1",
"cool_var2"))),
fn = "test_dashboard")
```
OUTPUT:
```
---
title: Test Dashboard
output:
flexdashboard::flex_dashboard:
orientation: columns
vertical_layout: scroll
runtime: shiny
---
\```{r setup, include=FALSE}
library(flexdashboard)
library(tidytext)
library(tidyverse)
library(janitor)
library(DT)
library(gghighlight)
library(knitr)
library(shiny)
library(qualtRics)
library(curl)
library(sjlabelled)
library(naniar)
library(scales)
library(lme4)
library(MASS)
library(snakecase)
\```
\```{r global, include=FALSE}
#setting global options for table scrolling and plot theme
options(DT.options = list(scrollY="100vh"))
theme_set(theme_minimal())
#this fetches all of your survey info
surveys <- all_surveys()
#this saves the survey responses into
docusign_survey <- fetch_survey(surveyID = surveys$id[1],
verbose = TRUE,
label = TRUE,
breakout_sets = TRUE,
force_request = TRUE)
#this saves the question text into a dataframe
questions <- survey_questions(surveyID = surveys$id[1])
rename_df <- rename_variables(docusign_survey)
#this renames all of the variables
docusign_survey <- docusign_survey %>%
rename_at(as.vector(rename_df$old_name), ~ as.vector(rename_df$new_labels))
#new variables
new_var <- rename_df$new_labels
#which are multi_select?
multi_select <- rename_df %>%
filter(ms == 1) %>%
dplyr::select(new_labels)
#relabel those NAs as No
docusign_survey <- docusign_survey %>%
purrr::modify_at(multi_select$new_labels, na_to_y)
\```
Sidebar {.sidebar}
=====================================
here is the input
\```{r}
selectInput("p_var_1", label = "Interesting Var #1",
choices = new_var,
multiple = TRUE)
\```
Cool Plots!
=====================================
Column {.tabset}
-------------------------------------
### this is my plot
\```{r}
renderPlot( {
make_bivariate_plot(docusign_survey, input$cool_var1, input$cool_var2)
})
\```
```
Functions
make_dashboard()
I saved the parts that will repeat every time, probably will want to make them editable for changes in scrolling, etc. but just trying to make proof of concept at the moment.
```
make_dashboard <- function(title, sidebar, page1, fn){
load("data/top_matter.rda")
load("data/libraries.rda")
load("data/main_chunk.rda")
initial_bit <- stringr::str_c(top_matter, libraries, main_chunk, sep = "\n\n")
intermediate <- stringr::str_c(initial_bit, sidebar, sep = "\n\n")
total <- stringr::str_c(intermediate, page1, sep = "\n\n")
data <- list(title = title)
out_fn <- paste0("./", fn, ".Rmd")
writeLines(whisker.render(total, data), con = out_fn)
}
```
make_sidebar()
```
make_sidebar <- function(sidebar_title, input_type, input_name){
top_sidebar <-
'Sidebar {.sidebar}
=====================================
'
sidebar_text <- str_c(top_sidebar, sidebar_title, sep = "\n\n")
if(input_type == "multi-select"){
ms <- "TRUE"
} else {
ms <- "FALSE"
}
input_one <- make_select_input(input_name, ms)
sidebar_total <- str_c(sidebar_text, "```{r}", input_one, "```", sep = "\n\n")
return(sidebar_total)
}
```
make_page()
```
make_page <- function(page_title, element_one){
top_page <-
'{{page_title}}
=====================================
Column {.tabset}
-------------------------------------'
add_element <- stringr::str_c(top_page, element_one, sep = "\n\n")
data <- list(page_title = page_title)
page <- whisker.render(add_element, data = data)
return(page)
}
```
make_plot()
```
make_plot <- function(plot_title, type = c("univariate", "bivariate"), vars){
top_plot_piece <-' {{plot_title}}
\```{r}
renderPlot( {
'
if(type == "univariate"){
plot_piece <-
'make_univariate_plot(docusign_survey, input${{vars}})
})
\```'
total_plot <- stringr::str_c(top_plot_piece, plot_piece, sep = "\n\n")
data <- list(plot_title = plot_title,
vars = vars)
plot_chunk <- whisker.render(total_plot, data = data)
} else{
plot_piece <-
'make_bivariate_plot(docusign_survey, input${{var_1}}, input${{var_2}})
})
\```'
total_plot <- stringr::str_c(top_plot_piece, plot_piece, sep = "\n\n")
data <- list(plot_title = plot_title,
var_1 = vars[1],
var_2 = vars[2])
plot_chunk <- whisker.render(total_plot, data = data)
}
return(plot_chunk)
}
```
I'm having some very strange behavior when knitting a shiny RMarkdown HTML document. In the MWE below I simply (1) include a slider input, (2) include a column in the df as the value of the input and then (3) return the df. The MWE below returns the error Error in : Column "b" must be length 1 (the number of rows), not 0. If I take out the multiplication of the input value when I assign a value to b, the document knits with no problem. MWE:
---
output: html_document
runtime: shiny
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE)
library(dplyr)
```
Testing
```{r eruptions, echo=FALSE}
# Input slider
sliderInput("test", "Test", value = 0.5, min = 0, max = 1, width = "100%")
# Pre-define df
output <- reactiveValues(data = data.frame(a = 20))
observe({
output$data <- output$data %>%
# Manipulate df
mutate(b = input$test * 2)
})
# Return df
renderDataTable({output$data})
```
Does anyone know what could be causing this strange behavior?
Here's what I suspect is going on ---
I think that the first time your observe is run through, that input$test is NULL.
If your mutate sets a column to NULL, it would just remove that column.
If you use input$test * 2 that will return an empty vector numeric(0). Trying to add a column with an empty vector would give the error.
If you add req(input$test) at the beginning of your observe function, does it work?
I want to be able to pass a reactive data.table to my Rmarkdown document that will sort the table and print the top, say 5, sorted entries.
I keep running into "Error : x must be a data.frame or data.table" at setorderv. I suspect the reactive is upsetting the is.data.table checks.
How do I simply pass a copy of the data to the Rmd document? And remove the reactivity wrapper?
Here is a simplified example cobbled from reactivity and downloadHandler stock examples. I took at look at this on SO which suggested using observe but I am not sure how to implement this. Thanks I appreciate some help on this.
RMARKDOWN - SOtestreport.Rmd
---
title: "Top in Segment Report"
output: html_document
params:
n: 5
dt: NA
top.field: NA
---
```{r setup, echo = FALSE}
library(data.table)
library(knitr) # for kable
```
Data report as at: `r format(Sys.time(), "%a %b %d %Y %X")`
```{r, echo = FALSE}
table.str <- paste0("Showing Top ", params$n ," tables for these fields: ",
params$top.field)
# is this the best way of having dynamic labels?
```
`r table.str`
```{r, echo = FALSE}
# tried this
# test.dt <- copy(dt)
# normally a function GenerateTopTable with limit sort field etc.
setorderv(dt, params$top.field, -1) # -1 is descending. THIS IS WHERE IT CRASHES
out <- dt[1:params$n,]
```
```{r toptable, echo = FALSE}
kable(out, digits = 3)
```
app.R
# SO test report
# mini example based on https://shiny.rstudio.com/gallery/reactivity.html
library(shiny)
library(datasets)
server <- function(input, output) {
datasetInput <- reactive({
switch(input$dataset,
"rock" = rock,
"pressure" = pressure,
"cars" = cars)
})
output$caption <- renderText({
input$caption
})
output$summary <- renderPrint({
dataset <- datasetInput()
summary(dataset)
})
output$view <- renderTable({
head(datasetInput(), n = input$obs)
})
# PROBLEM BIT WHERE I AM ADDING ON GALLERY EXAMPLE
# handler from https://stackoverflow.com/questions/37347463/generate-report-with-shiny-app-and
output$topreport <- downloadHandler(
filename = "topreport.html",
content = function(file) {
# Copy the report file to a temporary directory before processing it, in
# case we don't have write permissions to the current working dir (which
# can happen when deployed).
tempReport <- file.path(tempdir(), "SOtestreport.Rmd")
file.copy("SOtestreport.Rmd", tempReport, overwrite = TRUE)
# Set up parameters to pass to Rmd document
# TRIED THIS TOO
# my.dt <- as.data.table(datasetInput())
my.dt <- datasetInput()
my.params <- list(n = input$obs,
dt = my.dt,
top.field = names(my.dt)[1])
rmarkdown::render(tempReport, output_file = file,
params = my.params,
envir = new.env(parent = globalenv())
)
}
)
I fixed this example - dt in Rmd was not prefaced with params$!
setorderv(params$dt, params$top.field, -1) # -1 is descending
out <- params$dt[1:params$n,]
I still looking for answers on whether this is the right approach above (in passing parameters in Shiny to RMarkdown apps).
I also want to point out to others that these answers were also useful:
use of ReactiveValues by #NicE https://stackoverflow.com/a/32295811/4606130
What's the difference between Reactive Value and Reactive Expression?
I also made use of copying the dataset to break the reactivity, i.e.
my.dt <- copy(params$dt)
#do this before passing into render params list
Another tip in RStudio is not to use the internal Viewer Pane but to run externally in the browser. This fixed where the temporary file was saved issues. You can choose to always Run Externally under Run App in image below:
I have designed an R markdown doc that has combination of r code chunks, inline r, and various render functions.
I've put a couple of Shiny inputs in the doc, and I want to feed those inputs into two variables early in the r code, and then the various code segments later in the doc can depend on those variables. Some examples of the code are below.
I am having problems getting this doc to work. ("Operation not allowed without an active reactive context. (You tried to do something that can only be done from inside a reactive expression or observer.)")
Would be great if I could get some advice on how to edit the code to work.
# set up shiny input
``` {r echo = FALSE}
inputPanel(
textInput("fmno","Enter code", placeholder = "Code"),
selectInput("comparator", "Choose your comparator", choices = c("OP", "GE"), selected = "GE")
)
```
# feed inputs into variables in r code for use later in doc
```{r}
fmno <- reactive({get(input$fmno)})
comparator <- reactive({get(input$comparator)})
person_table <- person_table[person_table$fmno == fmno, ]
op_table <- op_table[op_table$op == op, ]
ge_table <- ge_table[ge_table$ge == ge, ]
if (comparator == "OP") {
comp_table <- op_table
comp <- op
} else {
comp_table <- ge_table
comp <- ge
}
```