I'm building a Shiny app that displays various pre-rendered .png and .svg images across multiple tabs, with some of the images being chosen through different types of input. To add some pizzazz, I'd like to add animations to the images that play whenever an image is displayed, either when the tab that it's on is selected or when it is chosen through an input.
I've tried using shinyjs::show/hide and shinyjqui::jqui_effect, but those functions seem to want to respond to some input, like a button press, rather than playing automatically and repeatedly.
I've managed to put together the code below that uses shinyanimate to achieve the desired effect. However, my real app has many more tabs and images, and this method of having every animation react to any changes in the tabs or inputs seems inefficient. Is there a better way of doing this?
(N.B. I'm only using the "bounceInLeft" effect here because it makes the example clear, but I'd like to be able to use other animation effects such as "fadeIn").
library(shiny)
library(shinyanimate)
# Define UI
ui <- fluidPage(
withAnim(),
tabsetPanel(id = "tabs",
# Tab 1 ----
tabPanel("Tab 1",
fluidRow(
column(3,
imageOutput("tab1_img1")
),
column(3,
imageOutput("tab1_img2")
)
)
),
# Tab 2 ----
tabPanel("Tab 2",
selectInput("img_opts",
label = "Select image",
choices = c("img2", "img1")
),
imageOutput("tab2_imgs")
)
)
)
# Define server logic
server <- function(input, output) {
# Tab 1 image 1
output$tab1_img1 <- renderImage({
list(src = file.path("images/img1.png"), width = "95%")
}, deleteFile = FALSE)
# Tab 1 image 1 animation
observeEvent(input$tabs,
startAnim(session = getDefaultReactiveDomain(), "tab1_img1", "bounceInLeft")
)
# Tab 1 image 2
output$tab1_img2 <- renderImage({
list(src = file.path("images/img2.png"), width = "95%")
}, deleteFile = FALSE)
# Tab 1 image 2 animation
observeEvent(input$tabs,
startAnim(session = getDefaultReactiveDomain(), "tab1_img2", "bounceInLeft")
)
# Tab 2 images
output$tab2_imgs <- renderImage({
list(src = file.path(paste0("images/", input$img_opts, ".png")), width = "25%")
}, deleteFile = FALSE)
# Tab 2 animation
observeEvent(c(input$tabs, input$img_opts),
startAnim(session = getDefaultReactiveDomain(), "tab2_imgs", "bounceInLeft")
)
}
# Run the application
shinyApp(ui = ui, server = server)
You can achieve the same result with only one observer:
tabsetPanel(id = "tabs",
# Tab 1 ----
tabPanel("Tab 1",
imageOutput("tab1_img"),
value = "tab1_img"
),
# Tab 2 ----
tabPanel("Tab 2",
selectInput("img_opts",
label = "Select image",
choices = c("img2", "img1")
),
imageOutput("tab2_img"),
value = "tab2_img"
)
)
observeEvent(c(input$tabs, input$img_opts), {
startAnim(session = getDefaultReactiveDomain(), input$tabs, "bounceInLeft")
})
EDIT: using shinyjqui
library(shiny)
library(shinyjqui)
ui <- fluidPage(
tabsetPanel(
id = "tabs",
# Tab 1 ----
tabPanel(
"Tab 1",
fluidRow(
column(3,
imageOutput("tab1_img1")
),
column(3,
imageOutput("tab1_img2")
)
)
),
# Tab 2 ----
tabPanel(
"Tab 2",
selectInput("img_opts",
label = "Select image",
choices = c("img3", "img4")
),
imageOutput("tab2_imgs")
)
)
)
server <- function(input, output, session) {
# Tab 1 image 1
output$tab1_img1 <- renderImage({
list(src = "www/img1.JPG", width = "300")
}, deleteFile = FALSE)
# Tab 1 image 2
output$tab1_img2 <- renderImage({
list(src = "www/img2.JPG", width = "300")
}, deleteFile = FALSE)
# Tab 2 images
output$tab2_imgs <- renderImage({
list(src = paste0("www/", input$img_opts, ".JPG"), width = "300")
}, deleteFile = FALSE)
# animate
observeEvent(list(input$tabs, input$img_opts), {
jqui_effect(
paste0("div.tab-pane[data-value=\"", input$tabs, "\"] img"),
"shake",
options = list(direction = "right", distance = 50, times = 3),
duration = 1500
)
}, ignoreInit = FALSE)
}
shinyApp(ui = ui, server = server)
EDIT: better solution
Here is a solution using the JavaScript library jquery.animatecss and the CSS library animate.css, which is the library used by shinyanimate. The app below requires an internet connection to include these libraries (see tags$head); it's better to download them (and then to put them in the www subfolder).
library(shiny)
js <- HTML(
'$(document).on("shiny:connected", function() {',
' Shiny.addCustomMessageHandler("animate", function(tab) {',
' var $tab = $("div.tab-pane[data-value=\\\"" + tab + "\\\"]");',
' var $imgs = $tab.find(".shiny-image-output");',
' $imgs.animateCSS("bounceInLeft", {duration: 1500});',
' });',
'});'
)
ui <- fluidPage(
tags$head(
tags$link(rel = "stylesheet", href = "https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.0/animate.compat.min.css"),
tags$script(src = "https://cdnjs.cloudflare.com/ajax/libs/animateCSS/1.2.2/jquery.animatecss.min.js"),
tags$script(js)
),
tabsetPanel(
id = "tabs",
# Tab 1 ----
tabPanel(
"Tab 1",
fluidRow(
column(3,
imageOutput("tab1_img1")
),
column(3,
imageOutput("tab1_img2")
)
)
),
# Tab 2 ----
tabPanel(
"Tab 2",
selectInput("img_opts",
label = "Select image",
choices = c("img3", "img4")
),
imageOutput("tab2_imgs")
)
)
)
server <- function(input, output, session) {
# Tab 1 image 1
output$tab1_img1 <- renderImage({
list(src = "www/img1.JPG", width = "300")
}, deleteFile = FALSE)
# Tab 1 image 2
output$tab1_img2 <- renderImage({
list(src = "www/img2.JPG", width = "300")
}, deleteFile = FALSE)
# Tab 2 images
output$tab2_imgs <- renderImage({
list(src = paste0("www/", input$img_opts, ".JPG"), width = "300")
}, deleteFile = FALSE)
# animate
observeEvent(list(input$tabs, input$img_opts), {
session$sendCustomMessage("animate", input$tabs)
}, ignoreInit = FALSE)
}
# Run the application
shinyApp(ui = ui, server = server)
Here is the list of available effects:
c(
"bounce",
"flash",
"pulse",
"rubberBand",
"shakeX",
"shakeY",
"headShake",
"swing",
"tada",
"wobble",
"jello",
"heartBeat",
"backInDown",
"backInLeft",
"backInRight",
"backInUp",
"backOutDown",
"backOutLeft",
"backOutRight",
"backOutUp",
"bounceIn",
"bounceInDown",
"bounceInLeft",
"bounceInRight",
"bounceInUp",
"bounceOut",
"bounceOutDown",
"bounceOutLeft",
"bounceOutRight",
"bounceOutUp",
"fadeIn",
"fadeInDown",
"fadeInDownBig",
"fadeInLeft",
"fadeInLeftBig",
"fadeInRight",
"fadeInRightBig",
"fadeInUp",
"fadeInUpBig",
"fadeInTopLeft",
"fadeInTopRight",
"fadeInBottomLeft",
"fadeInBottomRight",
"fadeOut",
"fadeOutDown",
"fadeOutDownBig",
"fadeOutLeft",
"fadeOutLeftBig",
"fadeOutRight",
"fadeOutRightBig",
"fadeOutUp",
"fadeOutUpBig",
"fadeOutTopLeft",
"fadeOutTopRight",
"fadeOutBottomRight",
"fadeOutBottomLeft",
"flip",
"flipInX",
"flipInY",
"flipOutX",
"flipOutY",
"lightSpeedInRight",
"lightSpeedInLeft",
"lightSpeedOutRight",
"lightSpeedOutLeft",
"rotateIn",
"rotateInDownLeft",
"rotateInDownRight",
"rotateInUpLeft",
"rotateInUpRight",
"rotateOut",
"rotateOutDownLeft",
"rotateOutDownRight",
"rotateOutUpLeft",
"rotateOutUpRight",
"hinge",
"jackInTheBox",
"rollIn",
"rollOut",
"zoomIn",
"zoomInDown",
"zoomInLeft",
"zoomInRight",
"zoomInUp",
"zoomOut",
"zoomOutDown",
"zoomOutLeft",
"zoomOutRight",
"zoomOutUp",
"slideInDown",
"slideInLeft",
"slideInRight",
"slideInUp",
"slideOutDown",
"slideOutLeft",
"slideOutRight",
"slideOutUp"
)
A demo of these effects is available here.
In addition to the duration option, the JavaScript function animateCSS (used in js) also accepts a delay option, if you want to delay the animation.
You can improve this solution by allowing to set the desired effect and its options in session$sendCustomMessage:
js <- HTML(
'$(document).on("shiny:connected", function() {',
' Shiny.addCustomMessageHandler("animate", function(tab_and_options) {',
' var tab = tab_and_options.tab;',
' var o = tab_and_options.options;',
' var $tab = $("div.tab-pane[data-value=\\\"" + tab + "\\\"]");',
' var $imgs = $tab.find(".shiny-image-output");',
' $imgs.animateCSS(o.effect, {duration: o.duration, delay: o.delay});',
' });',
'});'
)
session$sendCustomMessage("animate", list(
tab = input$tabs,
options = list(
effect = "bounceInLeft",
duration = 1000,
delay = 100
)
))
EDIT
The images are visible during a very small moment before the animation starts. It seems that this code prevents this issue:
js <- HTML(
'$(document).ready(function() {',
' $("a[data-toggle=tab]").on("hide.bs.tab", function(e) {',
' var tab = $(e.target).data("value");',
' var $tab = $("div.tab-pane[data-value=\\\"" + tab + "\\\"]");',
' $tab.find(".shiny-image-output").css("visibility", "hidden");',
' });',
'});',
'$(document).on("shiny:connected", function() {',
' Shiny.addCustomMessageHandler("animate", function(tab_and_options) {',
' var tab = tab_and_options.tab;',
' var o = tab_and_options.options;',
' var $tab = $("div.tab-pane[data-value=\\\"" + tab + "\\\"]");',
' var $imgs = $tab.find(".shiny-image-output");',
' $imgs.animateCSS(o.effect, {duration: o.duration, delay: o.delay});',
' });',
'});'
)
Related
I'm trying to scaffold together a basic dashboard using the shinymaterial package (https://ericrayanderson.github.io/shinymaterial/) but having a slight issue where my dropdown menus (usually selectInput in regular shiny apps) don't show up in a nested UI module.
There should be two dropdown menus above the "Settings" button in this screenshot:
Here's the code snippet for my scaffolding so far:
library(shiny)
library(shinymaterial)
# Wrap shinymaterial apps in material_page
ui <- material_page(
title = "App Title",
nav_bar_fixed = FALSE,
nav_bar_color = "black",
background_color = "white",
# font_color = "black",
# Place side-nav in the beginning of the UI
material_side_nav(
fixed = FALSE,
# Place side-nav tabs within side-nav
material_side_nav_tabs(
side_nav_tabs = c(
"Home" = "home",
"About" = "about"
),
icons = c("home", "help")
),
background_color = "white"
),
# Define side-nav tab content
material_side_nav_tab_content(
side_nav_tab_id = "home",
material_row(
material_column(
material_card(title = NULL,
sidebarCharts("main"),
depth = NULL),
width = 2,
offset = 0
),
material_column(
material_card(title = NULL,
"Chart goes here",
depth = NULL),
width = 10,
offset = 0
)
)
),
material_side_nav_tab_content(
side_nav_tab_id = "about",
tags$h1("About")
)
)
server <- function(input, output, session) {
callModule(chartSettings, "main")
}
# Server modules
chartSettings <- function(input, output, session) {
## 'Home' tab -- Sidebar
output$selectRootSymbol <- renderUI({
.choices <- c('a','b','c')
tagList(
helpText("Root Symbol:"), # Note: helpText() looks a little cleaner versus using the 'label' parameter in selectInput() below
# selectInput(session$ns("reactiveRootSymbol"), label = NULL, choices = .choices, selected = NULL, width = '100%')
material_dropdown(session$ns("reactiveRootSymbol"), label = NULL, choices = .choices, selected = NULL, width = '100%')
)
})
output$selectSymbol <- renderUI({
req(input$reactiveRootSymbol)
.choices <- c('d', 'e', 'f')
tagList(
helpText("Symbol:"),
# selectInput(session$ns("reactiveSymbol"), label = NULL, choices = toupper(.choices), selected = NULL, width = '100%')
material_dropdown(session$ns("reactiveSymbol"), label = NULL, choices = toupper(.choices), selected = NULL, width = '100%')
)
})
}
sidebarCharts <- function(id) {
ns <- NS(id)
tagList(
uiOutput(ns("selectRootSymbol")),
uiOutput(ns("selectSymbol")),
# actionButton(ns("settings"), "Settings", icon = icon("cogs"), width = '100%', class = "btn btn-primary"),p()
material_button(ns("settings"), "Settings", icon = "settings")
)
}
shinyApp(ui = ui, server = server)
I think I have a namespace issue, but I'm not sure (since the button does show up in the nested module). What am I doing wrong?
Any help is much appreciated!
There are at least two issues here.
1. material_dropdown does not display (resolved)
This appears to be due to the unused width = 100% option inside material_dropdown(). Removing this results in some of the drop downs displaying and all of the labels displaying.
2. Consecutive material_dropdown does not display (unresolved)
Having two consecutive material_dropdown's results in only the first drop down displaying, even though both labels display. There have been previous bugs with material_dropdown in the shinymaterial package so this could be part of a related issue.
Here is the code following my exploration:
library(shiny)
library(shinymaterial)
# submodule UI
sidebarCharts <- function(id) {
ns <- NS(id)
tagList(
uiOutput(ns("selectRootSymbol")),
uiOutput(ns("selectSymbol")),
# actionButton(ns("settings"), "Settings", icon = icon("cogs"), width = '100%', class = "btn btn-primary"),p()
material_button(ns("settings"), "Settings", icon = "settings")
)
}
# submodule server
chartSettings <- function(input, output, session) {
## 'Home' tab -- Sidebar
output$selectRootSymbol <- renderUI({
.choices <- c('a','b','c')
material_dropdown(session$ns("reactiveRootSymbol"), label = "Root Symbol:", choices = .choices)
})
output$selectSymbol <- renderUI({
# req(input$reactiveRootSymbol)
.choices <- c('d', 'e', 'f')
material_dropdown(session$ns("reactiveSymbol"), label = "Symbol:", choices = .choices)
})
}
## Wrap shinymaterial apps in material_page ----
ui <- material_page(
title = "App Title",
nav_bar_fixed = FALSE,
nav_bar_color = "black",
background_color = "white",
# font_color = "black",
# Place side-nav in the beginning of the UI
material_side_nav(
fixed = FALSE,
# Place side-nav tabs within side-nav
material_side_nav_tabs(
side_nav_tabs = c(
"Home" = "home"
),
icons = c("home")
),
background_color = "white"
),
# Define side-nav tab content
material_side_nav_tab_content(
side_nav_tab_id = "home",
material_row(
material_column(
material_card(title = NULL,
sidebarCharts("main"),
depth = NULL),
width = 2,
offset = 0
),
material_column(
material_card(title = NULL,
"Chart goes here",
depth = NULL),
width = 10,
offset = 0
)
)
)
)
## main server ----
server <- function(input, output, session) {
callModule(chartSettings, "main")
}
## run ----
shinyApp(ui = ui, server = server)
when input$variable change
uiOutput(1) and uiOutput(2) are rendered at the same time?
Is there any way render step by step not synchronistically?
The following is the steps I want.
1) input variable change
2) uiOutput(1) rendering
3) uiOutput(2) rendering
when input variable change shiny page is not working properly.
but after resizing the page window, the screen appears correctly.
Each time I change the input value, i have to recalibrate the page size.
ui.R
library(shiny)
library(shinydashboard)
sidebar <- dashboardSidebar(
radioButtons(inputId = "sidebar_data_sell",
label = "",
choices = c("Sell-in" = "sell_in", "Sell-out" = "sell_out"),
selected = "sell_out",
inline = TRUE),
uiOutput("summary_sidebar_ui")
)
ui <- dashboardPage(
dashboardHeader(title = h5("Hi")),
sidebar,
uiOutput("dashboard_body")
)
server.R
convertMenuItem <- function(mi, tabName) {
mi$children[[1]]$attribs['data-toggle'] = "tab"
mi$children[[1]]$attribs['data-value'] = tabName
mi
}
out_product_main <- function() {
list(
fluidPage(h5("product")))
}
product_items <- function() {
list(
selectInput("product_product_sell",
"product",
choices = list("product1" = "prd1", "product2" = "prd2"),
selected = c("prd1"))
)
}
in_overview_main <- function() {
list(
fluidPage(h5("overview")))
}
overview_items <- function() {
list(
radioButtons("overview_period_sell",
"period",
choices = c("year","month"),
inline = TRUE,
selected = "month")
)
}
shinyServer(function(input, output) {
output$summary_sidebar_ui <- renderUI({
if (input$sidebar_data_sell == "sell_in")
{
sidebarMenu(id = "summary_item",
convertMenuItem(menuItem("Overview",
tabName = "in_overview",
icon = icon("signal", lib = 'font-awesome'),
overview_items()),
tabName = 'in_overview'))
}
else{
sidebarMenu(id = "p_analysis_item",
convertMenuItem(menuItem("Product",
tabName = "out_product",
icon = icon("product-hunt", lib='font-awesome'),
product_items()),
tabName = 'out_product')
)
}
})
output$dashboard_body <- renderUI({
if (input$sidebar_data_sell == "sell_in") {
dashboardBody(tabItems(tabItem("in_overview", in_overview_main())))
}
else {
dashboardBody(tabItems(tabItem("out_product", out_product_main())))
}
})
})
I am just learning R Shiny and have been playing around with the various examples in the gallery. For the "Image Output" example:
http://shiny.rstudio.com/gallery/image-output.html
I was wondering how I might be able to include an image headline when I select either the chainring or smiley radio button. For example after selecting "smiley" I would like the title "Smiley" to display below the image. Thank you for your help. Below is the code that is also included in the link above. (Some of it was intentionally deleted for clarity). Thank you
library(png) # For writePNG function
shinyServer(function(input, output, session) {
# A temp file to save the output.
# This file will be automatically removed later by
# renderImage, because of the deleteFile=TRUE argument.
outfile <- tempfile(fileext = ".png")
# Generate the image and write it to file
x <- matrix(rep((0:(width-1))/(width-1), height), height,
byrow = TRUE)
y <- matrix(rep((0:(height-1))/(height-1), width), height)
pic <- gauss2d(x, y, input$r)
writePNG(pic, target = outfile)
# Return a list containing information about the image
list(src = outfile,
contentType = "image/png",
width = width,
height = height,
alt = "This is alternate text")
}, deleteFile = TRUE)
# image2 sends pre-rendered images
output$image2 <- renderImage({
if (is.null(input$picture))
return(NULL)
if (input$picture == "face") {
return(list(
src = "images/face.png",
contentType = "image/png",
alt = "Face"
))
} else if (input$picture == "chainring") {
return(list(
src = "images/chainring.jpg",
filetype = "image/jpeg",
alt = "This is a chainring"
))
}
}, deleteFile = FALSE)
})
shinyUI(fluidPage(
titlePanel("Client data and query string example"),
fluidRow(
column(4, wellPanel(
sliderInput("r", "Radius :", min = 0.05, max = 1,
value = 0.2, step = 0.05),
radioButtons("picture", "Picture:",
c("chainring", "face"))
)),
column(4,
imageOutput("image1", height = 300),
imageOutput("image2")
)
)
Does this help?
ui.R
library(shiny)
shinyUI(fluidPage(
titlePanel("Image title"),
sidebarLayout(
sidebarPanel(
sliderInput("obs", "Number of observations:",
min = 0, max = 1000, value = 500)
),
mainPanel(
imageOutput("myImage"),
uiOutput("text")
)
)
))
server.R
library(shiny)
shinyServer(function(input, output) {
output$myImage <- renderImage({
outfile <- tempfile(fileext='.png')
png(outfile, width=400, height=300)
hist(rnorm(input$obs), main = "")
dev.off()
list(src = outfile,
contentType = 'image/png',
width = 400,
height = 300,
alt = "This is alternate text")
}, deleteFile = TRUE)
histogramTitle <- reactive({
paste("<h5>", input$obs, " observation used to create the histogram</h5>", sep = "")
})
output$text <- renderUI({
HTML(as.character(histogramTitle()))
})
})
This question might seem to be a duplicate, but let me explain why it's not.
I want to create a shiny navbarPage that has fixed elements and a reactive number of tabPanels, that reacts to other input elements. There are many questions about how to create reactive tabsetPanels/navbarPages but they mostly aim for what it has to look like. The most common answer (and the answer i don't seek) is to render the whole navbarPage with updated set of tabPanels. I am aware of that concept and I used it in the code below.
Here is what I want my app to look like:
library(shiny)
runApp(
shinyApp(
ui = shinyUI(
fluidPage(
uiOutput("navPage")
)
),
server = function(input, output, session){
MemoryValue1 <- 1
MemoryValue2 <- 1
makeReactiveBinding("MemoryValue1")
observeEvent(input$button, {
output[[paste0("plot_", input$number)]] <- renderPlot({
hist(rnorm(1000))
})
})
observeEvent(input$insidepanels, {
MemoryValue1 <<- input$insidepanels
})
observeEvent(input$number, {
MemoryValue2 <<- input$number
})
output$navPage <- renderUI({
OutsidePanel1 <- tabPanel("Outside1",
numericInput("insidepanels", label = "Number of panels inside NavMenu", value = isolate(MemoryValue1), step = 1, min = 1),
numericInput("number", label = "Panel to add Output-Element to", value = 1, step = isolate(MemoryValue2), min = 1),
actionButton("button", label = "Add Output-Element")
)
OutsidePanel2 <- tabPanel("Ouside2", "Outside 2")
InsidePanels <- lapply(1:MemoryValue1, function(x){tabPanel(paste0("Inside", x), plotOutput(paste0("plot_", x)))})
do.call(navbarPage, list("Nav", OutsidePanel1, OutsidePanel2, do.call(navbarMenu, c("Menu", InsidePanels))))
})
}
)
)
As you might have seen, it takes a lot of effort to store your input values if they are inside other panels and will be re-rendered = reset all the time. I find this solution to be illegible and slow, because of unnecessary rendering. It also interrupts the user who is clicking through values of input$insidepanels.
What I want the app to be like is that the Outside Panels are fixed and dont re-render. The main problem is that inside shiny, navbarPage on rendering distributes HTML elements to two different locations. Inside the navigation panel and to the body as tab content. That means a-posteori added elements will not be properly embedded.
So far, I have tried to create the navbarPage with custom tags and have dynamic output alter only parts of it. That works pretty well with the navigation panel, but not with tab contents. The reason is that all tabs (their div containers) are listed one after another and as soon as I want to inject multiple at once, I am offthrown by htmlOutput, since it (seemingly) has to have a container and cannot just deliver plain HTML. Thus, all custom tabs are not recongnized properly.
Here my code so far:
library(shiny)
runApp(
shinyApp(
ui = shinyUI(
fluidPage(
tags$nav(class = "navbar navbar-default navbar-static-top", role = "navigation",
tags$div(class = "container",
tags$div(class = "navbar-header",
tags$span(class = "navbar-brand", "Nav")
),
tags$ul(class = "nav navbar-nav",
tags$li(
tags$a(href = "#tab1", "data-toggle" = "tab", "data-value" = "Outside1", "Outside1")
),
tags$li(
tags$a(href = "#tab2", "data-toggle" = "tab", "data-value" = "Outside2", "Outside2")
),
tags$li(class = "dropdown",
tags$a(href = "#", class = "dropdown-toggle", "data-toggle" = "dropdown", "Menu1"),
htmlOutput("dropdownmenu", container = tags$ul, class = "dropdown-menu")
)
)
)
),
tags$div(class = "container-fluid",
tags$div(class = "tab-content", id = "tabContent",
tags$div(class = "tab-pane active", "data-value" = "Outside1", id = "tab1",
numericInput("insidepanels", label = "Number of panels inside NavMenu", value = 1, step = 1, min = 1),
numericInput("number", label = "Panel to add Output-Element to", value = 1, step = 1, min = 1),
actionButton("button", label = "Add Output-Element")
),
tags$div(class = "tab-pane", "data-value" = "Outside2", id = "tab2", "Content 2"),
htmlOutput("tabcontents")
)
)
)
),
server = function(input, output, session){
observeEvent(input$button, {
output[[paste0("plot_", input$number)]] <- renderPlot({
hist(rnorm(1000))
})
})
output$dropdownmenu <- renderUI({
lapply(1:input$insidepanels, function(x){tags$li(tags$a(href = paste0("#tab-menu-", x), "data-toggle" = "tab", "data-value" = paste0("Inside", x), paste("Inside", x)))})
})
output$tabcontents <- renderUI({
tagList(
lapply(1:input$insidepanels, function(x){div(class = "tab-pane", "data-value" = paste("Inside", x), id = paste0("tab-menu-", x), plotOutput(paste0("plot_", x)))})
)
})
}
)
)
Note: I also tried to create HTML with JavaScript-Chunks that is triggered from inside server. This works for simple tab content, but I want my tabPanels to still have shiny output elements. I don't see how I can fit that in with JavaScript. That is why I included the plotOutput content in my code.
Thanks to anybody who can help solve this issue!
Finally came up with an own answer. I hope this can be a useful reference to others who try to understand shiny reactiveness. The answer is JavaScript for custom elements (rebuilding standard shiny elements) and using Shiny.unbindAll() / Shiny.bindAll() to achieve the reactivity.
Code:
runApp(
shinyApp(
ui = shinyUI(
fluidPage(
tags$script('
Shiny.addCustomMessageHandler("createTab",
function(nr){
Shiny.unbindAll();
var dropdownContainer = document.getElementById("dropdown-menu");
var liNode = document.createElement("li");
liNode.setAttribute("id", "dropdown-element-" + nr);
var aNode = document.createElement("a");
aNode.setAttribute("href", "#tab-menu-" + nr);
aNode.setAttribute("data-toggle", "tab");
aNode.setAttribute("data-value", "Inside" + nr);
var textNode = document.createTextNode("Inside " + nr);
aNode.appendChild(textNode);
liNode.appendChild(aNode);
dropdownContainer.appendChild(liNode);
var tabContainer = document.getElementById("tabContent");
var tabNode = document.createElement("div");
tabNode.setAttribute("id", "tab-menu-" + nr);
tabNode.setAttribute("class", "tab-pane");
tabNode.setAttribute("data-value", "Inside" + nr);
var plotNode = document.createElement("div");
plotNode.setAttribute("id", "plot-" + nr);
plotNode.setAttribute("class", "shiny-plot-output");
plotNode.setAttribute("style", "width: 100% ; height: 400px");
tabNode.appendChild(document.createTextNode("Content Inside " + nr));
tabNode.appendChild(plotNode);
tabContainer.appendChild(tabNode);
Shiny.bindAll();
}
);
Shiny.addCustomMessageHandler("deleteTab",
function(nr){
var dropmenuElement = document.getElementById("dropdown-element-" + nr);
dropmenuElement.parentNode.removeChild(dropmenuElement);
var tabElement = document.getElementById("tab-menu-" + nr);
tabElement.parentNode.removeChild(tabElement);
}
);
'),
tags$nav(class = "navbar navbar-default navbar-static-top", role = "navigation",
tags$div(class = "container",
tags$div(class = "navbar-header",
tags$span(class = "navbar-brand", "Nav")
),
tags$ul(class = "nav navbar-nav",
tags$li(
tags$a(href = "#tab1", "data-toggle" = "tab", "data-value" = "Outside1", "Outside1")
),
tags$li(
tags$a(href = "#tab2", "data-toggle" = "tab", "data-value" = "Outside2", "Outside2")
),
tags$li(class = "dropdown",
tags$a(href = "#", class = "dropdown-toggle", "data-toggle" = "dropdown", "Menu1"),
tags$ul(id = "dropdown-menu", class = "dropdown-menu")
)
)
)
),
tags$div(class = "container-fluid",
tags$div(class = "tab-content", id = "tabContent",
tags$div(class = "tab-pane active", "data-value" = "Outside1", id = "tab1",
numericInput("insidepanels", label = "Number of panels inside NavMenu", value = 0, step = 1),
numericInput("number", label = "Panel to add Output-Element to", value = 0, step = 1),
actionButton("button", label = "Add Output-Element")
),
tags$div(class = "tab-pane", "data-value" = "Outside2", id = "tab2", "Content 2")
)
)
)
),
server = function(input, output, session){
allOpenTabs <- NULL
observeEvent(input$insidepanels, {
if(!is.na(input$insidepanels)){
localList <- 0:input$insidepanels
lapply(setdiff(localList, allOpenTabs), function(x){
session$sendCustomMessage(type = "createTab", message = x)
})
lapply(setdiff(allOpenTabs, localList), function(x){
session$sendCustomMessage(type = "deleteTab", message = x)
})
allOpenTabs <<- localList
}
})
observeEvent(input$button, {
output[[paste0("plot-", input$number)]] <- renderPlot({
hist(rnorm(1000))
})
})
}
), launch.browser = TRUE
)
It is basically adding the HTML Elements "by hand" and linking them to shiny listeners.
I'm trying to display images in my shiny app reactively. I've successfully done that in the server.R script with:
output$display.image <- renderImage({
image_file <- paste("www/",input$image.type,".jpeg",sep="")
return(list(
src = image_file,
filetype = "image/jpeg",
height = 520,
width = 696
))
}, deleteFile = FALSE)
BUT it's very slow.
However, it is VERY fast to embed one of the images into the ui.R script like so:
tabPanel("Live Images", img(src = "img_type1.jpeg"))
Why is there such a difference? Is there any way to make the reactive images appear faster?
Hi you can use conditionalPanel to do this, it embed all your images but only the one which have TRUE to the condition will be displayed :
tabPanel("Live Images",
conditionalPanel(condition = "input.image_type == 'img_type1'",
img(src = "img_type1.jpeg")
),
conditionalPanel(condition = "input.image_type == 'img_type2'",
img(src = "img_type2.jpeg")
)
)
And change the name of your input from image.type to image_type because . have special meaning in Javascript (as between input and image_type).
If you have a lot of images, you can always do something like that :
tabPanel("Live Images",
lapply(X = seq_len(10), FUN = function(i) {
conditionalPanel(condition = paste0("input.image_type == 'img_type", i, "'"),
img(src = paste0("img_type", i, ".jpeg"))
)
})
)
For example, with images from this post by tsperry (you can find it on rbloggers too), you can do :
library("shiny")
ui <- fluidPage(
tabsetPanel(
tabPanel("Live Images",
# 50 images to display
lapply(X = seq_len(50), FUN = function(i) {
# condition on the slider value
conditionalPanel(condition = paste0("input.slider == ", i),
# images are on github
img(src = paste0("https://raw.githubusercontent.com/pvictor/images/master/",
sprintf("%04d", i), "plot.png"))
)
}),
sliderInput(inputId = "slider", label = "Value", min = 1, max = 50, value = 1,
animate = animationOptions(interval = 100, loop = TRUE))
)
)
)
server <- function(input, output) {
}
shinyApp(ui = ui, server = server)