# 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()