I am trying to insert additional information into a reactable in R - one which has about 3600 rows. I've tried nesting a plot under each row (similar to this, but with nested plots instead of sub-tables). The only way I could make this work was to use plotly within reactable, like so:
library(reactable)
library(magrittr)
library(plotly)
my_diamonds <- diamonds
my_diamonds$cats <- cut(my_diamonds$price, 850)
my_diamonds <- my_diamonds[ order(my_diamonds$cut, my_diamonds$cats), ]
data <- unique(my_diamonds[, c("cut", "cats")])
reactable(data,
details = function(index) {
diam_data <- my_diamonds[my_diamonds$cut == data$cut[index] & my_diamonds$cats == data$cats[index], ]
plot_ly(diam_data,
x = ~1:nrow(diam_data),
y = ~y,
type = 'scatter',
mode = 'lines') # %>% toWebGL()
}
)
But sadly, for this amount of data, this takes forever to output the table, and anything I've tried to make it faster (such as toWebGL()) changes nothing. All I really care about is the speed, and having some sort of visualisation associated with each row - I don't particularly care if it's plotly or something else.
A second option would be to use an in-line HTML widget for each row (shown here). In my example, this could be done if adding:
data_parcels <- split(my_diamonds, list(my_diamonds$cats, my_diamonds$cut), drop = T)
data$nested_points <- sapply(data_parcels, '[[', 'y')
data$sparkline <- NA
library(sparkline)
reactable(data,
columns = list(
sparkline = colDef(cell = function(value, index) {
sparkline(data$nested_points[[index]])
})
))
This isn't quite as slow as the plotly option, but still very slow in the larger scheme of things. Any ideas on how to speed up either example, anyone?
PaulM and I have worked on a solution together, and managed to speed up one of the options: the one involving in-line sparklines. As it turned out based on some profiling work, what was making the process particularly slow wasn't drawing the sparklines in itself, rather the subsequent work of translating them from R so that they could be incorporated into the HTML reactable table.
So to bypass that slow translation process entirely, we wrote a code template that would get wrapped around the data points to be plotted. This is what we then served directly to reactable, alongside an html = TRUE argument, for the code to be interpreted as such, rather than as regular text.
The final hurdle after that was to ensure that the sparklines (one per row) were still on display even if a user sorted a column or navigated to a different page of results - normally the sparklines would disappear on interacting with the table in this way. For this, we ensured that that the reactable would be redrawn 10ms after any click.
Here is an example wrapped in shiny that shows all this in action, alongside the old (slow) version. For me, the sped up version renders in about 0.5s roughly, whereas the old one - about 13s.
library(reactable)
library(magrittr)
library(plotly)
library(sparkline)
library(shiny)
library(shinycssloaders)
library(shinyWidgets)
if (interactive()) {
# Init objects
t0 <- NULL
t1 <- NULL
my_diamonds <- diamonds
my_diamonds$cats <- cut(my_diamonds$price, 850)
my_diamonds <- my_diamonds[ order(my_diamonds$cut, my_diamonds$cats), ]
data <- unique(my_diamonds[, c("cut", "cats")])
data_parcels <- split(my_diamonds, list(my_diamonds$cats, my_diamonds$cut), drop = T)
data$nested_points <- sapply(data_parcels, '[[', 'y')
data$sparkline <- NA
ui <- shinyUI(
basicPage(
br(),
radioGroupButtons(
inputId = "speedChoice",
label = "Speed",
choices = c("Fast", "Slow"),
status = "danger"
),
br(),
verbatimTextOutput("timeElapsed"),
br(),
shinycssloaders::withSpinner(
reactableOutput("diamonds_table")
),
# Small JS script to re-render a reactable table so that the sparklines show
# after the user has modified the table (sorted a col or navigated to a given page of results)
tags$script('document.getElementById("diamonds_table").addEventListener("click", function(event){
setTimeout(function(){
console.log("rerender")
HTMLWidgets.staticRender()
}, 10);
})
')
)
)
server <- function(input, output, session) {
output$diamonds_table <- renderReactable({
if (input$speedChoice == "Fast") {
t0 <<- Sys.time()
part1 <- '<span id="htmlwidget-spark-' # + ID
part2 <- '" class="sparkline html-widget"></span><script type="application/json" data-for="htmlwidget-spark-' # + ID
part3 <- '">{"x":{"values":[' # + values
part4 <- '],"options":{"height":20,"width":60},"width":60,"height":20},"evals":[],"jsHooks":[]}</script>'
out <- list(length = nrow(data))
for (i in 1:nrow(data)) {
vals <- paste0(data$nested_points[[i]], collapse = ',')
out[[i]] <- paste0(part1, i, part2, i, part3, vals, part4)
}
data$sparkline <- out
tab <- reactable(data,
columns = list(
sparkline = colDef(html = TRUE,
cell = function(value, index) {
return(htmltools::HTML(value))
}
)
)
) %>%
spk_add_deps() %>%
htmlwidgets::onRender(jsCode = "
function(el, x) {
HTMLWidgets.staticRender();
console.log('render happening')
}")
t1 <<- Sys.time()
return(tab)
} else {
# Classic, but slow version:
t0 <<- Sys.time()
tab <- reactable(data,
columns = list(
sparkline = colDef(cell = function(value, index) {
data$nested_points[[index]] %>%
sparkline::sparkline()
}
)
)
)
t1 <<- Sys.time()
return(tab)
}
})
output$timeElapsed <- renderText({
input$speedChoice # Connect to reactable update cycle
return(t1 - t0)
})
}
shinyApp(ui = ui, server = server)
}
Related
What I am trying to do?
I am building a Shiny app that imports data, runs some analysis, and allows the User to make selections regarding the analysis via drop downs in a data table. The initial choices available are specific to each row in the table based on values found in the data. I want the User to be able to augment the data so new values that weren’t found in the imported data are available as choices, too. It is this last part that is giving me trouble.
I’ve created an example based on mtcars to illustrate. The construct I have for creating an editable data table is based on ID's for each cell in a column as follows (thanks to some earlier help I had on Stack to figure it out). The snippet of code below is contained in an observeEvent when I load new data. [Note the full code is at the bottom]
selectInputIDmodel <<- paste0("sel_model", 1:nrow(v$cars()$cars_meta))
v$model_applied <- reactive({match_cars(v$cars())$model_applied})
v$initTbl <-
dplyr::tibble(
car = v$cars()$cars_meta$car,
make = v$cars()$cars_meta$make,
mpg = v$cars()$cars_meta$mpg,
model = sapply(selectInputIDmodel, function(x){as.character(selectInput(inputId = ns(x), label = "",
choices = v$model_applied()$model[v$model_applied()$car == v$cars()$cars_meta$car[which(selectInputIDmodel == x)]],
selected = v$cars()$cars_meta$model[which(selectInputIDmodel == x)]
))})
)
I've set up another observeEvent for when a new model is added. I expect I need to use updateSelectInput to update the choices under the model variable. I've tried this by recreating v$initTbl under this observeEvent, but haven't figured out how to work in the updateSelectInput instead of SelectInput. The former is calling for a "session" argument, so if I just substitute "updateSelectInput" I get an error saying that I cannot convert an environment to character. If I remove the "as.character" I get a "cannot unclass an environment" error.
Further Context
Below is further context for what I am trying to do followed by the code I have.
When running the app, the Load Data button imports the mtcars data and splits the car name into make and model fields. The model field in the display table is a drop down list and has as choices the various models that are found in the data for the specific make of car. The first one in each list is the default value. The User can select from the drop downs and use the Commit button to register the choices selected. The User can go back to make changes and Commit multiple times.
There are fields to allow the User to add a new model name for a particular make of car. Save Model should apply the new model entry as a drop down choice for the relevant make of car. This is what I haven’t been able to get working.
In order to be able to confirm the updates that were committed, once the User selects Commit the first time, I am showing the resultsTbl as verbatim output at the bottom of the page. The output refreshes every time the Commit button is clicked. It is the resultsTbl that I store and will use for onward processing in another module.
Here is a sequence of steps that should be able to be completed.
Step 1: Load Data
Step 2: Change the Model in the 2nd row from “RX4” to “RX4 Wag”
Step 3: Commit and see updates reflected in the resultsTbl
Step 4: Set Select Make to “Valiant”
Step 5: Set Add Model Name to “V”
Step 6: Save Model
Step 7: “V” should appear under “Valiant” as a selection in the drop down
Step 8: Commit and “V” should appear as the model for row 6 in resultsTbl
Step 9: Change the Model in the last row from “240D” to “280”
Step10: Commit and see update reflected in the resultsTbl
What have I tried?
The Load Data button triggers an observeEvent that does the following:
Sets up the data
Determines which models are available for which makes of car (for the drop downs)
Initiates the data table (initTbl)
I use a reactive (displayTbl) to capture the updates to feed the proxy table.
I then use a reactive (resultTbl) to store the captured values.
This all works fine.
I use Save Model as another observeEvent to update which models are available for which makes of car, to add new values to the drop downs where relevant.
I have not been able to figure how to make this work.
I believe I need some way to reinitialize the data table with the refreshed choices for the drop downs, whilst preserving any previously selected values. As noted above, I am unsure how to integrate updateSelectInput into the existing code.
Any help would be greatly appreciated.
Here is the current state of my code:
#********* LIBRARIES *************************************************
library(magrittr)
library(dplyr)
library(tidyselect)
library(shiny)
library(stringr)
library(purrr)
library(shinyjs)
library(zeallot)
library(DT)
#******** FUNCTIONS ***************************************************
# Creates the new data set / cars object
create_data2 <- function(){
#simulate data import
cars_df <- head(mtcars, 10)
#simulate creating meta table
cars_meta <- dplyr::tibble(car = rownames(cars_df), make = sub("([A-Za-z]+).*", "\\1", rownames(cars_df)), cars_df)
cars_meta$model <- NA
#simulate creating cars_list
names <- rownames(cars_df)
`%<-%` <- zeallot::`%<-%`
car <- list()
car[c("head", "m1", "m2")] %<-% data.frame(stringr::str_split(names, " ", simplify = TRUE))
car$m <- paste(car$m1, car$m2)
cars_list <- list()
for(h in car$head){
cars_list[[h]] <- list(car$m[car$head==h])
}
#simulate creating the cars_object
cars_object <- list()
cars_object$cars_df <- cars_df
cars_object$cars_meta <- cars_meta
cars_object$cars_list <- cars_list
return(cars_object)
}
# Updates the cars object with resultTbl
meta_table <- function(object, table){
tbl <- table
object$cars_meta <- tbl
return(object)
}
# Matches the models and makes of the cars
match_cars <- function(cars_object){
cv <- cars_object$cars_meta
car_match <- list()
for (car in cv$car){
x <- 1
for (model in cars_object$cars_list[[cv$make[cv$car == car]]][[1]]){
car_match[[paste0(car,"#",x)]][["model"]] <- model
x <- x + 1
}
}
model_applied <-
if(nrow(dplyr::bind_rows(car_match)) >0) {
dplyr::bind_rows(car_match) %>%
mutate(car = stringr::str_replace_all(names(car_match),"#\\d",""))
} else {
data.frame(car = "", drop = FALSE)
}
model_reduced <- model_applied %>%
dplyr::group_by(car) %>%
dplyr::slice(1) %>%
dplyr::ungroup()
cv <- cv %>%
select(-model) %>%
left_join(model_reduced, by = "car") %>%
select(car, make, mpg, model)
cars_object$cars_meta <- cv
cars_object$model_applied <- model_applied
return(cars_object)
}
# Adds a new make/model combination to cars_list of the cars object
new_model <- function(cars_object, make, new){
cars_object$cars_list[[make]] <- c(new, cars_object$cars_list[[make]][[1]])
return(cars_object)
}
#******** UI ********************************************************
mod_data_ui <- function(id) {
fluidPage(
actionButton(NS(id,"new_data"), "Load Data"),
br(),
DT::dataTableOutput(NS(id, 'dt')),
br(),
actionButton(NS(id, "commit_meta"), "Commit"),
br(),
verbatimTextOutput(NS(id,"results")),
br(),
uiOutput(NS(id,"make_set")),
br(),
uiOutput(NS(id, "model_value")),
br(),
uiOutput(NS(id, "save_model")),
br(),
verbatimTextOutput(NS(id,"meta"))
)
}
shiny_ui <- function() {
navbarPage(
title = div(span("Data",
style = "position: relative; top: 50%; transform: translateY(-50%);")),
tabPanel(
"Data Management",
mod_data_ui("data")
)
)
}
#**** SERVER ***********************************************************
mod_data_server <- function(id) {
shiny::moduleServer(id, function(input, output,session){
ns <- session$ns
v <- reactiveValues()
#place holders
selectInputIDmodel <- "model"
observeEvent(input$new_data, once = TRUE, {
data <- create_data2()
v$cars <- reactive({data})
selectInputIDmodel <<- paste0("sel_model", 1:nrow(v$cars()$cars_meta))
v$model_applied <- reactive({match_cars(v$cars())$model_applied})
v$initTbl <-
dplyr::tibble(
car = v$cars()$cars_meta$car,
make = v$cars()$cars_meta$make,
mpg = v$cars()$cars_meta$mpg,
model = sapply(selectInputIDmodel, function(x){as.character(selectInput(inputId = ns(x), label = "",
choices = v$model_applied()$model[v$model_applied()$car == v$cars()$cars_meta$car[which(selectInputIDmodel == x)]],
selected = v$cars()$cars_meta$model[which(selectInputIDmodel == x)]
))})
)
})
displayTbl <- reactive({
req(input$new_data)
dplyr::tibble(
car = v$cars()$cars_meta$car,
make = v$cars()$cars_meta$make,
mpg = v$cars()$cars_meta$mpg,
model = sapply(selectInputIDmodel, function(x){as.character(selectInput(inputId = ns(x), label = "",
choices = v$model_applied()$model[v$model_applied()$car == v$cars()$cars_meta$car[which(selectInputIDmodel == x)]],
selected = input[[x]]))})
)
})
output$dt <- DT::renderDataTable({
req(input$new_data)
DT::datatable(
v$initTbl, escape = FALSE, selection = 'none', rownames = FALSE,
options = list(paging = FALSE, ordering = FALSE, scrollx = TRUE, dom = "t",
preDrawCallback = DT::JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = DT::JS('function() { Shiny.bindAll(this.api().table().node()); } ')
)
)
})
dt_table_proxy <- DT::dataTableProxy(outputId = "dt")
observeEvent({sapply(selectInputIDmodel, function(x){input[[x]]})}, {
DT::replaceData(proxy = dt_table_proxy, data = displayTbl(), rownames = FALSE)
}, ignoreInit = TRUE)
v$resultTbl <- reactive({
dplyr::tibble(
car = v$cars()$cars_meta$car,
make = v$cars()$cars_meta$make,
mpg = v$cars()$cars_meta$mpg,
model = sapply(selectInputIDmodel, function(x){as.character(input[[x]])})
)
})
observeEvent(input$commit_meta, {
cars_updated <- meta_table(v$cars(), v$resultTbl())
v$cars <- reactive({cars_updated})
})
# add model manually
output$make_set <- renderUI({
req(input$new_data)
make <- v$cars()$cars_meta$make
#make_sel <- unique(make)
selectInput(NS(id, "make_set"), "Select Make", multiple = FALSE, choices = make)
})
output$model_value <- renderUI({
req(input$new_data)
textInput(NS(id, "model_value"), "Add Model Name")
})
output$save_model <- renderUI({
req(input$new_data)
actionButton(NS(id, "save_model"), "Save Model", style="color: #fff; background-color: #337ab7; border-color: #2e6da4")
})
observeEvent(input$save_model,{
car <- meta_table(v$cars(), v$resultTbl()) # This is the same step as under commit
v$cars <- reactive({match_cars(
new_model(
cars_object = car,
make = input$make_set,
new = input$model_value
)
)
})
v$model_applied <- reactive({match_cars(v$cars())$model_applied})
updateTextInput(session, "model_value", value = "")
})
output$meta <- renderPrint({
req (input$commit_meta > 0)
tf <- v$cars()$cars_meta
tf %>% print(n = Inf)
})
return(reactive(v))
})
}
shiny_server <- function(input, output, session) {
v <- mod_data_server("data")
}
#********* APP *******************************
svyStudyapp_app <- function(...) {
app <- shiny::shinyApp(
ui = shiny_ui,
server = shiny_server
)
shiny::runApp(app, ...)
}
use updateSelectInput inside an observeEvent or observe function. Pass in the Shiny session object, the input ID of the selectInput element and a vector of new choices.
like this
observeEvent(input$saveModelButton, {
updateSelectInput(session, "sel_model6", choices = c("V", "Other models"))
})
I am building a Shiny App that does random simulations of some stuff in three ways and saves the results in a table. I want the table to (1) fill the cell green for the closest value to the correct answer, and (2) include a line on bottom tracking total number of times each test group has been the closest.
what I have:
what I want:
Here's the code I'm using:
By the way, in this example there are ties, but that won't really be possible in the real thing, so probably not necessary to deal with.
#
# This is a Shiny web application. You can run the application by clicking
# the 'Run App' button above.
#
# Find out more about building applications with Shiny here:
#
# http://shiny.rstudio.com/
#
library(shiny)
# Define UI for application that draws a histogram
ui <- fluidPage(
# Application title
titlePanel("test"),
# Sidebar with a slider input for number of bins
sidebarLayout(
sidebarPanel(
actionButton("random_select",
"Generate Random Numbers",
width = 'auto')
),
# Show a plot of the generated distribution
mainPanel(
tableOutput("results_table_output")
)
)
)
# Define server logic required to draw a histogram
server <- function(input, output) {
counter <- reactiveValues(countervalue = 0)
observeEvent(input$random_select,{
counter$countervalue = counter$countervalue + 1
}
)
results <- reactiveValues(
table = list(trial = NA,
answer =NA,
test_1 = NA,
test_2 = NA,
test_3 = NA)
)
observeEvent(counter$countervalue,{
results$table$trial[counter$countervalue] <- as.integer(counter$countervalue)
results$table$answer[counter$countervalue] <- sample(1:10,1)
results$table$test_1[counter$countervalue] <- sample(1:10,1)
results$table$test_2[counter$countervalue] <- sample(1:10,1)
results$table$test_3[counter$countervalue] <- sample(1:10,1)
})
output$results_table_output <- renderTable({
results$table
})
}
# Run the application
shinyApp(ui = ui, server = server)
Disclaimer
I would also fall back to a more advanced table rendering engine like DT. However, in the following I show another solution which works with renderTable from "base" shiny.
renderTable + JS Solution
If you don't mind using some JavaScript you can use the following snippet:
library(shiny)
library(shinyjs)
js <- HTML("function mark_cells() {
$('.mark-cell').parent('td').css('background-color', 'steelblue');
}
function add_totals() {
const ncols = $('table th').length;
const $col_totals = Array(ncols).fill().map(function(el, idx) {
const $cell = $('<td></td>');
if (idx == 1) {
$cell.text('total:');
} else if (idx > 1) {
$cell.text($('table tr td:nth-child(' + (idx + 1) + ') .mark-cell').length);
}
return $cell;
})
$('table tfoot').remove();
$('table > tbody:last-child')
.after($('<tfoot></tfoot>').append($('<tr></tr>').append($col_totals)));
}
function mark_table() {
mark_cells();
add_totals()
}
")
make_run <- function(i, answer, tests = integer(3)) {
cn <- c("trial", "answer", paste0("test_", seq_along(tests)))
if (is.null(i)) {
line <- matrix(integer(0), ncol = length(cn))
colnames(line) <- cn
} else {
line <- matrix(as.integer(c(i, answer, tests)), ncol = length(cn))
colnames(line) <- cn
}
as.data.frame(line)
}
mark_best <- function(row) {
truth <- row[2]
answers <- row[-(1:2)]
dist <- abs(answers - truth)
best <- dist == min(dist)
answers[best] <- paste0("<span class = \"mark-cell\">", answers[best], "</span>")
c(row[1:2], answers)
}
ui <- fluidPage(
useShinyjs(),
tags$head(tags$script(js)),
sidebarLayout(
sidebarPanel(
actionButton("random_select",
"Generate Random Numbers")
),
mainPanel(
tableOutput("results_table_output")
)
)
)
server <- function(input, output, session) {
results <- reactiveVal(make_run(NULL))
observeEvent(input$random_select, {
res <- results()
results(rbind(res, make_run(nrow(res) + 1, sample(10, 1), sample(10, 3, TRUE))))
})
output$results_table_output <- renderTable({
res <- results()
if (nrow(res) > 0) {
res <- as.data.frame(t(apply(res, 1, mark_best)))
session$onFlushed(function() runjs("mark_table()"))
}
res
}, sanitize.text.function = identity)
}
shinyApp(ui = ui, server = server)
Explanation
In the renderTable function, we call mark_best where we surround the "winning" cells with <span class = "mark-cell">. This helps us on the JS side to identify which cells are the winners.
In order to not escape the HTML in it, we use the argument sanitize.text.function which is responsible for, well, sanitizing strings in the cell. Because we want to print them as is, we supply the identity function.
We include 3 JavaScript functions in the <head> of the document, which
color the parent <td> of our marked cells (mark_cells())
add column totals to the table. This is done by counting the .mark-cell marked cells in each column (add_totals)
a convenience wrapper to call both functions (mark_table())
In order to be able to actually call the JS function we rely on shinyjs. This is however, merely syntactic sugar and could be achieved otherwise as well (if you mind the additional library). To make shinyjs work, we need to include a call to useShinyjs in the UI.
All what is left to do is to call mark_table in the renderTable function. To make sure that the table is rendered properly, we do not call the JS function right away but use session$onFlushed to register the call to be run after the next flush happens.
I created an R shiny application that has a dygraph based on a data table that is dynamically subsetted by a checkboxGroupInput. My problem is, when I attempt to load large amounts of data (millions of records), it loads very slowly and/or crashes.
After doing some more research, I stumbled upon a "lazy-load" technique from here. Based on my understanding, this technique essentially downsamples the data by only loading the number of data points equal to the width of the dygraph window. As the user zooms in, it will drill down and load more data within the dyRangeSelector max/min dates. I suspect this will solve my problem, because it will load significantly less data at any given dygraph interaction. However, all of the examples provided in this link were in Javascript, and I'm having trouble translating it to R.
I also attempted to treat the GraphDataProvider.js file as a dygraph plugin, but I was unable to get it to work properly.
A couple of quick notes on my implementation:
Each element of data_dict in the server is an xts object.
The do.call.cbind function call in the server is based off of this SO implementation, and it is very fast.
My current setup is essentially like this (I refactored it to make it generic):
Data Setup:
library(shiny)
library(shinydashboard)
library(dygraphs)
library(xts)
library(data.table)
start <- as.POSIXlt("2018-07-09 00:00:00","UTC")
end <- as.POSIXlt("2018-07-11 00:00:00","UTC")
x <- seq(start, end, by=0.5)
data <- data.frame(replicate(4,sample(0:1000,345601,rep=TRUE)))
data$timestamp <- x
data <- data[c("timestamp", "X1", "X2", "X3", "X4")]
data <- as.data.table(data)
filters <- c("X1","X2","X3","X4")
data_dict <- vector(mode="list", length=4)
names(data_dict) <- filters
data_dict[[1]] <- as.xts(data[,c('timestamp','X1')]); data_dict[[2]] <- as.xts(data[,c('timestamp','X2')])
data_dict[[3]] <- as.xts(data[,c('timestamp','X3')]); data_dict[[4]] <- as.xts(data[,c('timestamp','X4')])
# Needed to quickly cbind the xts objects
do.call.cbind <- function(lst){
while(length(lst) > 1) {
idxlst <- seq(from=1, to=length(lst), by=2)
lst <- lapply(idxlst, function(i) {
if(i==length(lst)) { return(lst[[i]]) }
return(cbind(lst[[i]], lst[[i+1]]))})}
lst[[1]]}
UI:
header <- dashboardHeader(title = "App")
body <- dashboardBody(
fluidRow(
column(width = 8,
box(
width = NULL,
solidHeader = TRUE,
dygraphOutput("graph")
)
),
column(width = 4,
box(
width = NULL,
checkboxGroupInput(
"data_selected",
"Filter",
choices = filters,
selected = filters[1]
),
radioButtons(
"data_format",
"Format",
choices=c("Rolling Averages","Raw"),
selected="Rolling Averages",
inline=TRUE
)
)
)
)
)
ui <- dashboardPage(
header,
dashboardSidebar(disable=TRUE),
body
)
Server:
server <- function(input, output) {
# Reactively subsets the dataset based on checkboxGroupInput filters
the_data <- reactive({
data <- do.call.cbind(data_dict[input$data_selected]) # Column bind multiple xts objects
})
output$graph <- renderDygraph({
graph <- dygraph(the_data()) %>%
dyRangeSelector(c("2018-07-10 00:00:00","2018-07-10 02:00:00")) %>%
dyOptions(useDataTimezone = TRUE,connectSeparatedPoints = TRUE)
if(input$data_format == "Rolling Averages") graph <- graph %>% dyRoller(rollPeriod = 100)
graph
})
}
Make App:
shinyApp(ui, server)
I would appreciate any help I can get on this, this has stumbled me for a while now. Thank you!
I am trying to create a variable number of plots in a shiny app, each with hover ability from ggvis using add_tooltip() to display actual data points. To create a variable number of plots I am using a for loop. See below for a toy example that can be run on its own.
For some reason in my code the hover over ability only works correctly for the final plot that is created. Does anyone know how I might be able to fix this or maybe have a suggestion for a better approach?
Thanks!
library(shiny)
library(ggvis)
# Define ui for variable amounts of plots
ui <- fluidPage(
fluidRow(
uiOutput("mydisplay")
)
)
server <- function(input, output) {
# toy data example
x = data.frame(
id = 1:30,
myname = c(rep("First 10",10),rep("Second 10",10),rep("Third 10",10)),
stringsAsFactors = F
)
# ggvis add_tooltip() function
all_values <- function(x) {
if(is.null(x)) return(NULL)
row <- mydf[mydf$id == x$id, c("id","myname") ]
paste0(names(row), ": ", format(row), collapse = "<br />")
}
# For loop to create variable number of plots
for (k in 1:length(unique(x$myname))){
mydf = subset(x,x$myname==unique(x$myname)[k])
mydf %>% ggvis(~id, ~id) %>%
layer_points(size := 30, key := ~id) %>%
add_tooltip(all_values,"hover") %>%
bind_shiny(paste0("p_",k), paste0("p_ui_",k))
}
# For displaying in the UI
output$mydisplay = renderUI({
lapply(1:length(unique(x$myname)), function(j) {
fluidRow(
column(7, ggvisOutput(paste0("p_",j)))
)
})
})
}
# Run the application
shinyApp(ui = ui, server = server)
Answered my own question thanks to this question here. The ggvis code needs to be wrapped in a reactive({}) function. Hope this helps someone.
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.