# xtlive.R # ::rtemislive:: # 2024 EDG rtemis.org # Shiny # shiny::runApp("app.R") # Setup library(shiny) library(bslib) library(htmltools) library(plotly) library(rtemis) library(rtemisbio) source("globals.R") # Source built-in datasets from data.R source("data.R") # Get names of all objects defined in data.R builtin_names <- readLines("data.R") |> (\(z) grep("<-", x = z, value = TRUE))() |> (\(z) gsub(" <-.*", "", x = z))() # Colors primary <- "#72CDF4" secondary <- "#704071" info <- helpcol <- "#FEB2E0" success <- "#B4DC55" #' Create timeseries visualization shiny #' Visualize timeseries data using `dplot3_xt()` #' #' Set verbosity to 1 to monitor app progress #' #' @param default_theme Character: "dark" or "light" theme #' @param verbosity Integer: 0 = silent, 1 = verbose #' #' @author EDG #' @export #' @return A shiny app xtlive <- function( default_theme = "dark", xt_plotly_height = "900px", verbosity = 1) { # Logo logo <- base64enc::dataURI( file = "./www/rtemisxt_gray.png", mime = "image/png" ) # Version platform <- sessionInfo()[["platform"]] xtl <- paste0( "xtlive v.0.0.2", " | ", "rtemis v.", utils::packageVersion("rtemis"), # doesn't work in shinylive " | R v.", version$major, ".", version$minor, " | running on ", platform ) # Shinylive info shinylive_info <- if (substr(platform, 1, 4) == "wasm") { paste0( "

This application has been compiled to ", as.character(a("WebAssembly", href = "https://webassembly.org/", target = "_blank")), " using ", as.character(a("shinylive", href = "https://posit-dev.github.io/r-shinylive/", target = "_blank")), "
and is best viewed with the latest version of Chrome." ) } # UI page_navbar ---- ui <- function(request) { bslib::page_navbar( # Title ---- title = list( logo = a( img( src = logo, width = "140px", height = "auto", alt = "rtemis" ), href = "https://rtemis.org/xtlive" ) ), id = "xtlive", selected = "Welcome", footer = span( xtl, " © 2024 EDG", style = "display: block; text-align: center; margin-top: 1em; margin-bottom: 1em;" ), # Theme ---- theme = bslib::bs_theme( bg = "#fff", fg = "#000", primary = primary, secondary = secondary, success = success, info = info, `tooltip-bg` = "#303030", `tooltip-color` = helpcol, `tooltip-opacity` = 1, `tooltip-border-radius` = "10px", `tooltip-padding-x` = "1rem", `tooltip-padding-y` = "1rem", `tooltip-font-size` = "1rem" # base_font = bslib::font_google("Inter"), # doesn't work in shinylive # code_font = bslib::font_google("Fira Code") ) |> bs_add_rules(sass::sass_file("www/rtemislive.scss")), # Window title ---- window_title = "rtemis xtlive", # Language ---- lang = "en", # Nav Panels ---- ## [] Welcome ---- bslib::nav_panel( title = "Welcome", icon = bsicons::bs_icon("stars"), bslib::card( h4("Welcome to xtlive.", style = "text-align: center;"), card_body( class = "d-inline text-center", HTML(paste0( "xtlive is a web interface for ", as.character(a("rtemis", href = "https://rtemis.org", target = "_blank")), ",
providing interactive visualization of timeseries data.", "

", rthelp_inline( "To get started, use the navigation tabs at the top.", title = "Welcome" ), shinylive_info )) ), bslib::card_image( file = "./www/rtemisxt_splash.png", alt = "rtemislive", align = "center", border_radius = "all", fill = FALSE, width = "40%", class = "mx-auto" ) ) ), # /nav_panel Welcome ## [] Timeseries Visualization ---- bslib::nav_panel( title = "Timeseries", icon = bsicons::bs_icon("body-text"), card( full_screen = TRUE, class = "p-0", # allow items side by side ---- card_header( class = "d-flex justify-content-end", # tooltip UI output ---- uiOutput("ui_xt_tooltip"), # popover UI output ---- uiOutput("ui_xt_popover") ), layout_sidebar( fillable = TRUE, # Data Load Sidebar ---- sidebar = bslib::sidebar( uiOutput("ui_xt_load_switch"), # Switch between file upload and built-in data uiOutput("ui_xt_data_load"), # File upload or built-in data selection depending on switch uiOutput("ui_xt_data_info"), # Shows dataset info uiOutput("ui_xt_plot_button"), # Click to plot # uiOutput("ui_xt_tooltip") ), # layout_sidebar( # sidebar = bslib::sidebar( # position = "right", # # xt transformations # "Timeseries Transformations" # ), # # Plot Output ---- # # for shinylive, do not use plotlyOutput directly in ui !! # # fails when no plot is rendered, unlike shiny # uiOutput("ui_dplot3_xt") # ) uiOutput("ui_dplot3_xt") ) ) ), # /nav_panel Timeseries Visualization ## [] About ---- bslib::nav_panel( title = "About", icon = bsicons::bs_icon("info-square"), bslib::card( card_image( file = "./www/rtemislive.jpeg", href = "https://rtemis.org", alt = "rtemislive", align = "center", border_radius = "all", fill = FALSE, width = "54%", class = "mx-auto" ), div( class = "d-inline text-center", HTML(paste0( "Powered by rtemis (", as.character(a("rtemis.org", href = "https://rtemis.org", target = "_blank")), ")." )), br(), br(), a( img( src = "rtemis_gray.png", alt = "rtemis", align = "center", width = "190px" ), href = "https://rtemis.org", target = "_blank" ), align = "center" ) ) ), # /nav_panel About bslib::nav_spacer(), bslib::nav_item(input_dark_mode(id = "dark_mode", mode = default_theme)), header = list( # busy indicators ---- shinybusy::add_busy_spinner( spin = "orbit", color = "#00ffff", timeout = 200, position = "bottom-left", onstart = FALSE ) ) ) # /ui /bslib::page_navbar } # /ui function # Server ---- server <- function(input, output, session) { # UI xt load switch ---- output$ui_xt_load_switch <- shiny::renderUI({ if (verbosity > 0) { message("Rendering ui_xt_load_switch") } # Radio buttons: built-in data vs upload file ---- shiny::radioButtons( inputId = "xt_load_switch", label = "Data source", choices = list( `Built-in datasets` = "builtin", `File upload` = "upload" ), selected = "builtin" ) }) # UI xt Data ---- output$ui_xt_data_load <- shiny::renderUI({ req(input$xt_load_switch) if (input$xt_load_switch == "upload") { # Upload xt JSON file if (verbosity > 0) { message("Rendering ui_xt_data_load for file upload") } shiny::fileInput( inputId = "xt_file", label = "Upload xt file", buttonLabel = "Browse local files...", ) } else { # Select built-in data stored in ./data/ directory if (verbosity > 0) { message("Rendering ui_xt_data_load for built-in data selection") } shiny::selectizeInput( inputId = "xt_builtin_data", label = "Select built-in xt dataset", choices = builtin_names, selected = builtin_names[1] ) } }) # /ui_xt_data_load # UI for xt data info ---- output$ui_xt_data_info <- shiny::renderUI({ req(xt_obj()) if (verbosity > 0) { message("Rendering ui_xt_data_info") } # inv comment bslib::card( bslib::card_title("xt Dataset Info", container = htmltools::h6), bslib::card_body( summarize_xt(xt_obj()), fillable = FALSE ) # /card_body ) # /card }) # /ui_xt_data_info # UI for plot action button ---- output$ui_xt_plot_button <- shiny::renderUI({ req(xt_obj()) if (verbosity > 0) { message("Rendering ui_xt_plot_button") } bslib::input_task_button( "xt_plot_button", "Plot dataset", icon = bsicons::bs_icon("magic"), label_busy = "Drawing...", icon_busy = bsicons::bs_icon("clock-history"), type = "primary", auto_reset = TRUE ) }) # /ui_xt_plot_button # Load Dataset ---- xt_obj <- shiny::reactive({ req(input$xt_load_switch) if (input$xt_load_switch == "upload") { # inv changed to "req(input$xt_file$datapath)": no help req(input$xt_file$datapath) if (verbosity > 0) { message("Loading xt data from file '", input$xt_file$datapath, "'") } # get file extension ext <- tools::file_ext(input$xt_file$datapath) dat <- if (ext == "json") { read.xtjson(input$xt_file$datapath) } else if (ext == "rds") { readRDS(input$xt_file$datapath) } else { stop("Unsupported file format") } if (verbosity > 0) { message("Loaded dataset of class '", class(dat)[1], "'") } return(dat) } else { # Load built-in data from data.R objects req(input$xt_builtin_data) if (verbosity > 0) { message("Loading built-in xt dataset '", input$xt_builtin_data, "'") } # Reading from data folder works fine in shiny app, not shinylive, # use objects from data.R instead # assign to dat object of name input$xt_builtin_data dat <- get(input$xt_builtin_data) if (verbosity > 0) { message("Loaded dataset of class '", class(dat)[1], "'") } return(dat) } }) # /xt_obj # dplot3 theme is "white" or "black" ---- dplot3_theme <- shiny::reactive({ req(input$dark_mode) if (input$dark_mode == "dark") { "black" } else { "white" } }) # /dplot3_theme # dplot3_theme() ---- dplot3_theme <- shiny::reactive({ req(input$dark_mode) if (input$dark_mode == "dark") { "darkgraygrid" } else { "whitegrid" } }) # /dplot3_theme # Render plotly ---- output$dplot3_xt <- plotly::renderPlotly({ req(xt_obj()) if (verbosity > 0) { message("Rendering dplot3_xt of object with class '", class(xt_obj())[1], "'") } dplot3_xt( x = xt_obj(), yline.width = input$xt.linewidth, y2line.width = input$xt.linewidth, theme = dplot3_theme(), bg = input$plot.bg, # legend.bg defaults to transparent, this sets paper bg plot.bg = input$plot.bg, legend.x = 0, legend.xanchor = "left", show.rangeslider = input$show.rangeslider, margin = list(l = 75, r = 75, b = 75, t = 75), ) }) |> # /dplot3_xt # bindEvent(input$xt_plot_button) bindEvent(input$xt_plot_button, input$xt_plot_update_button) # Create variable clicked that is TRUE after input$xt_plot_button is clicked clicked <- shiny::reactiveVal(FALSE) shiny::observeEvent(input$xt_plot_button, { clicked(TRUE) }) # UI xt tooltip ---- output$ui_xt_tooltip <- shiny::renderUI({ if (clicked()) { bslib::tooltip( trigger = span( "Plot help", bsicons::bs_icon("info-circle", class = "text-info"), style = "text-align: right;", class = "rtanihi" ), div( rthelplist( c( "Hover over plot to see spike lines", # "Use top-right gear icon to access plot settings and click on 'Update rendering' to apply.", "Use top-right gear icon to access plot settings.", htmlEscape("To enter superscripts and subscripts in labs or units, use HTML tags, e.g. '2' and 'i'.") ) ), style = "text-align: left;" ), # /div rt-tooltip placement = "bottom" ) # /tooltip } }) # /ui_xt_tooltip # UI xt popover ---- output$ui_xt_popover <- shiny::renderUI({ if (clicked()) { popover( trigger = bsicons::bs_icon("gear", class = "ms-auto"), textInput(inputId = "xt.xlab", label = "X-axis label", value = ""), textInput(inputId = "xt.ylab", label = "Left Y-axis label", value = ""), textInput(inputId = "xt.y2lab", label = "Right Y-axis label", value = ""), textInput(inputId = "xt.xunits", label = "X-axis units", value = ""), textInput(inputId = "xt.yunits", label = "Left Y-axis units", value = ""), textInput(inputId = "xt.y2units", label = "Right Y-axis units", value = ""), textInput(inputId = "xt.firsttick", label = "First X-axis tick", value = "2"), textInput(inputId = "xt.ticksevery", label = "Show X-axis ticks every this many X values", value = "2"), sliderInput(inputId = "xt.linewidth", label = "Line width", min = 1, max = 10, value = 3), bslib::input_switch( id = "show.rangeslider", label = "Show rangeslider", value = TRUE ), shinyWidgets::colorPickr( "plot.bg", label = "Plot background", selected = ifelse(input$dark_mode == "dark", "#191919", "#FFFFFF") ), bslib::input_task_button( "xt_plot_update_button", "Update rendering", icon = bsicons::bs_icon("arrow-clockwise"), label_busy = "Drawing...", icon_busy = bsicons::bs_icon("clock-history"), type = "primary", auto_reset = TRUE ), title = "Plot settings", placement = "auto" ) # /popover } }) # /ui_xt_popover # UI dplot3_xt ---- output$ui_dplot3_xt <- renderUI({ if (clicked() == FALSE || is.null(xt_obj())) { rthelp( # "Select Data Source and Click 'Plot dataset' on the left.", "Select Data Source on the left.", title = "Timeseries Visualization " ) } else { plotly::plotlyOutput( "dplot3_xt", width = "100%", height = xt_plotly_height ) } }) # /ui_dplot3_xt } # /server # Shiny app ---- shiny::shinyApp(ui = ui, server = server, enableBookmarking = "url") } # xtlive xtlive()