--- title: "Getting started with EZRShiny" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Getting started with EZRShiny} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE) ``` EZRShiny helps you build multi-page 'shiny' apps that all look and work the same way. Instead of assembling pages, navigation bars, cards and sidebars from 'bslib' yourself, you describe the app as a set of tabs and fill them with inputs and outputs, each built with one short function call. This guide covers: 1. [Starting a new app](#starting-a-new-app) 2. [How an app is laid out](#how-an-app-is-laid-out) 3. [The page](#the-page) 4. [Tabs](#tabs) 5. [Inputs](#inputs) 6. [Outputs](#outputs) 7. [The server](#the-server) 8. [The example app](#the-example-app) 9. [Moving an existing app to EZRShiny](#moving-an-existing-app-to-ezrshiny) ## Starting a new app `createEZApp()` makes a folder with a working app in it, ready to edit: ```{r} library(EZRShiny) createEZApp("myApp", appName = "My App") shiny::runApp("myApp") ``` The app has an upload page and a results tab with a table. Upload any CSV and click **Run** to see it work, then replace the pieces with your own. ## How an app is laid out Every EZRShiny app uses the same folder layout: ``` myApp/ ├── app.R packages, UI and server ├── Functions/ your helper functions, one or more .R files ├── Necessary_Files/ files the app needs at start, such as a data template └── www/ images for the page, such as logos ``` `app.R` is split into numbered sections, so every app reads in the same order: ```{r} # App Startup ---- ## 1.0 Load Libraries ---- library(shiny) library(bslib) library(shinyjs) library(EZRShiny) ## 2.0 Load Basics ---- options(shiny.maxRequestSize = 300 * 1024^2) # allow uploads up to 300 MB sourceFunctions("Functions") # load every .R file in Functions/ ## 3.0 Universal Vars ---- appName <- "My App" # UI ---- ui <- UINav(...) # Server ---- server <- function(input, output, session) { ... } # Run App ---- shinyApp(ui = ui, server = server) ``` `sourceFunctions()` loads every `.R` file in a folder, so adding a helper is just a matter of saving a new file in `Functions/`. The files load in alphabetical order, which is why numbering them (`1-Read_In_Data.R`, `2-Plots.R`) is a good habit. ## The page `UINav()` builds the whole page. Everything else in the UI goes inside it: ```{r} ui <- UINav( logoFile = "logo.png", # from the www folder; leave out for no logo appName = "My App", # leave out to use a global appName variable barColor = "#1F4E79", # navigation bar color; leave out for the theme default singleTab("Upload", ...), biLevelTab("Results", ...) ) ``` The navigation bar shows, from left to right: the logos, the app name, one entry per tab, and a dark mode switch. | Argument | What it does | Default | |--------------|--------------|---------| | `logoFile` | Image file names in `www/`, shown in order. Several logos are allowed: `c("institute.png", "lab.svg")`. | no logo | | `appName` | App name shown after the logos. | the global `appName` variable, if there is one | | `logoHeight` | Height of each logo, as a CSS size. | `"40vh"` | | `barColor` | Navigation bar color. Text switches between light and dark to stay readable. | theme default | | `theme` | A `bslib::bs_theme()` for the whole app, for example `bs_theme(preset = "cosmo", primary = "#005596")`. | `bs_theme()` | ## Tabs Tabs come in two kinds: **top-level tabs**, which go straight into `UINav()`, and **sub tabs**, which go inside a top-level tab. ### Top-level tabs | Function | Holds | Put inside it | |---------------------|-------|---------------| | `singleTab()` | one page | inputs and outputs | | `sidebarLevelTab()` | one page with a sidebar | inputs and outputs | | `biLevelTab()` | a row of sub tabs | `subTab()`, `subSidebarTab()` | | `triLevelTab()` | a drop-down menu, where each entry has its own row of sub tabs | `triSubTab()`, `triSubSidebarTab()` | ### Sub tabs | Function | Holds | |----------------------|-------| | `subTab()` | inputs and outputs | | `subSidebarTab()` | a sidebar, plus inputs and outputs | | `triSubTab()` | a row of `subTab()`s or `subSidebarTab()`s (inside `triLevelTab()` only) | | `triSubSidebarTab()` | a sidebar, plus a row of sub tabs (inside `triLevelTab()` only) | `subTwoColPage(leftSide, rightSide)` isn't a tab: it splits any page into two equal columns. ### Putting them together Sidebars take their inputs as a `list()` in `sidebarElements`. Everything after that fills the main part of the page: ```{r} ui <- UINav( appName = "Tab Tour", # One page singleTab("Upload", navUpload("dataUpload", "Upload a CSV") ), # One page with a sidebar sidebarLevelTab("Table", sidebarElements = list( navSelect("columns", "Columns to show", multiple = TRUE) ), navOutputTable("dataTable") ), # A row of sub tabs biLevelTab("Plots", subSidebarTab("Scatter", sidebarElements = list(navButton("makeScatter", "Make plot")), navOutputPlot("scatterPlot") ), subTab("Side by Side", subTwoColPage(navOutputPlot("leftPlot"), navOutputPlot("rightPlot")) ) ), # A menu of tabs, each with its own sub tabs triLevelTab("Models", triSubTab("Linear", subTab("Fit", navOutputText("linearFit")), subTab("Residuals", navOutputPlot("linearResiduals")) ), triSubSidebarTab("Custom", sidebarElements = list(navText("formula", "Model formula")), subTab("Fit", navOutputText("customFit")) ) ) ) ``` ### Tab IDs Each row of sub tabs has an `id`, and each sub tab has a `value`. You use them in the server to show, hide or select tabs. Both default to the title with spaces removed: `biLevelTab("Explore Data", ...)` has the ID `"ExploreData"`, and `subTab("Box Plots", ...)` has the value `"BoxPlots"`. The server helpers remove spaces too, so you can write the titles as they appear on screen. The navigation bar itself has the ID `"root"`. Top-level tabs keep their titles exactly, spaces included. Each tab's contents sit in a card 85% of the window tall. To change the height of one tab, use its `height` argument. To change it for every tab, set the option before building the UI: ```{r} options(EZRShiny.cardHeight = "70vh") ``` ## Inputs Every input fills the width of its sidebar or page and takes an optional `tooltipText`, which adds an info icon to the label: ```{r} navButton("runData", "Run analysis", tooltipText = "Upload your data first.") ``` Every input starts with `inputId` and `label`, the same as in 'shiny'. The other arguments use 'shiny's names too: | Function | Makes | Other arguments (defaults) | |-----------------|-------|----------------------------| | `navButton()` | a button that shows a spinner while its code runs | | | `navSelect()` | a drop-down list | `choices`, `selected` (first choice), `multiple = FALSE`, `create = FALSE` | | `navUpload()` | a file upload | `multiple = FALSE` | | `navDownload()` | a download button | | | `navCheckbox()` | an on/off switch | `value = FALSE` (starts off) | | `navText()` | a text box | `value = ""` | | `navNumeric()` | a number box | `value = 1`, `min = NA`, `max = NA` (no limits) | | `navColor()` | a color picker | `value = "white"` | | `navDate()` | a date picker | `range = FALSE` (one date) | `multiple = TRUE` allows more than one choice or file. `create = TRUE` lets the user type in options that aren't in `choices`. ```{r} navSelect("pcX", "PC for x-axis", choices = paste0("PC", 1:10)) navSelect("groups", "Groups to compare", choices = groupNames, multiple = TRUE) navSelect("tags", "Tags", choices = c("a", "b"), multiple = TRUE, create = TRUE) ``` Choices for `navSelect()` can be left out of the UI and filled in from the server once data is loaded: ```{r} # UI navSelect("groups", "Pick groups", multiple = TRUE) # Server updateSelectizeInput(session, "groups", choices = unique(theData$Group)) ``` `navSpanText()` adds a line of text with an info icon, for explaining a page. ## Outputs Each output is a placeholder the server fills in with the matching render function: | UI | Server | |----------------------|--------| | `navOutputTable()` | `output$id <- DT::renderDT(...)` | | `navOutputPlot()` | `output$id <- renderPlot(...)` | | `navOutputPlotly()` | `output$id <- plotly::renderPlotly(...)` | | `navOutputGirafe()` | `output$id <- ggiraph::renderGirafe(...)` | | `navOutputPic()` | `output$id <- renderImage(...)` | | `sideNavOutputPic()` | `output$id <- renderImage(...)`, 40% wide, for beside another output | | `navOutputText()` | `output$id <- renderText(...)` | Like inputs, outputs can have a `label` shown above them and a `tooltipText` that adds an info icon after the label. Use it to explain how to read a plot or table. With a tooltip and no label, only the icon is shown. ```{r} navOutputPlotly("pcaPlot", label = "PCA of all samples", tooltipText = "Each point is one sample, colored by group.") ``` ## The server The server of an EZRShiny app follows a few habits that keep it predictable. **Keep the user's data in one place.** Store anything that needs to carry across the app in one `reactiveValues()` list: ```{r} global <- reactiveValues( datasets = list( rawData = NULL ) ) ``` **Set up the page on start.** Hide tabs and switch off buttons and downloads that can't be used yet: ```{r} observe({ startSection("Run on Start") hideNavTabs(rootID = "Results", tabIDs = c("Table", "Plot")) deactivateItems(c("runAnalysis", "resultsDownload")) endSection("Run on Start") }) ``` **Tie work to buttons.** Code in `observeEvent(input$button, ...)` runs only when the button is clicked. Code that reads inputs anywhere else reruns every time any of those inputs change. ```{r} observeEvent(input$runAnalysis, { startSection("Run Analysis") ## Load globals rawData <- global$datasets$rawData ## Inputs alpha <- input$alpha ## Do things results <- runMyAnalysis(rawData, alpha) ## App interactions output$resultsTable <- DT::renderDT({ results }) activateItems(c("resultsDownload")) showNavTabs(rootID = "Results", tabIDs = c("Table", "Plot")) nav_select("root", "Results") ## Save globals global$datasets$results <- results endSection("Run Analysis") }) ``` | Helper | What it does | |--------|--------------| | `activateItems(ids)`, `deactivateItems(ids)` | Switch inputs, buttons or downloads on or off. | | `showNavTabs(rootID, tabIDs)`, `hideNavTabs(rootID, tabIDs)` | Show or hide tabs. `showNavTabs()` also selects the first tab given. | | `bslib::nav_select("root", "Tab Title")` | Move the user to a top-level tab. | | `startSection(name)`, `endSection(name)` | Write markers to the log, so you can see which part of the server is running. | ## The example app EZRShiny comes with a complete example app, "Titanic Explorer", that explores who survived the Titanic using R's built-in `Titanic` data: ```{r} runExampleApp() ``` It uses every piece described above: | Tab | Built with | Shows | |-----|------------|-------| | Load Data | `singleTab()` | `navDownload()`, `navCheckbox()`, `navUpload()`, `navButton()`, `navSpanText()` | | Passengers | `sidebarLevelTab()` | `navSelect()` filled in from the server, `navOutputTable()` | | Survival | `biLevelTab()` with `subSidebarTab()` and `subTab()` | `navColor()`, `navText()`, `navOutputPlot()`, `navOutputPlotly()`, sub tabs revealed with `showNavTabs()` | | Compare Groups | `triLevelTab()` with `triSubTab()` and `triSubSidebarTab()` | `navOutputGirafe()`, `subTwoColPage()` | Every tab except Load Data is hidden until data is loaded, and each download is switched off until there's something to download. Its files are a good starting point for your own app: ```{r} exampleFolder <- system.file("examples", "titanicExplorer", package = "EZRShiny") list.files(exampleFolder, recursive = TRUE) file.copy(exampleFolder, "myCopy", recursive = TRUE) ``` ## Moving an existing app to EZRShiny If your app sources a copy of the standards file from its `Functions` folder: 1. Delete the standards file from `Functions/`. The package replaces it. 2. Replace its `source(...)` line with `library(EZRShiny)`. Keep `sourceFunctions("Functions")` to load your other helpers. 3. Move `options(shiny.maxRequestSize = ...)` into `app.R` if you need uploads over 5 MB. The package doesn't change options for you. 4. Logos and colors are now arguments of `UINav()` instead of being built in: ```{r} ui <- UINav( logoFile = c("institute_logo.png", "app_logo.png"), logoHeight = c("35vh", "40vh"), appName = appName, barColor = "#005596", ... ) ``` 5. `cardHeight` is no longer a global variable. Use `options(EZRShiny.cardHeight = "85vh")` or each tab's `height` argument. 6. Inputs take `TRUE`/`FALSE` instead of short words, and their arguments use 'shiny's names. Calls with only an ID, a label and `tooltipText` don't change. The rest change like this: | Before | After | |--------|-------| | `navSelect("id", "Label", "Single", "Locked", choices)` | `navSelect("id", "Label", choices)` | | `navSelect("id", "Label", "Multi", "Locked", choices)` | `navSelect("id", "Label", choices, multiple = TRUE)` | | `navSelect("id", "Label", "Single", "Create", choices)` | `navSelect("id", "Label", choices, create = TRUE)` | | `navUpload("id", "Label", "Single")` | `navUpload("id", "Label")` | | `navUpload("id", "Label", "Multi")` | `navUpload("id", "Label", multiple = TRUE)` | | `navCheckbox("id", "Label", "T")` | `navCheckbox("id", "Label", value = TRUE)` | | `navCheckbox("id", "Label", "F")` | `navCheckbox("id", "Label")` | | `navNumeric("id", "Label", 5)` | unchanged, but `min` and `max` now default to no limit instead of 0 and 1 | | `navNumeric(..., theValue = 5)` | `navNumeric(..., value = 5)` | | `navColor(..., colorValue = "red")` | `navColor(..., value = "red")` | | `navDate("id", "Label", TRUE)` | `navDate("id", "Label", range = TRUE)` | Passing an old short word such as `"Single"` where `TRUE` or `FALSE` is expected stops with an error that explains the change.