Skip to contents

tooltipexplorer is organized as a collection of Shiny modules, wired together in the app_server(). This vignette explains the naming convention, the UI/server split, how data flows between modules, and the role each module plays.


Naming convention

Every module follows the same file-and-function layout:

File UI function Server function
R/mod_inputs.R mod_inputs_ui(id) mod_inputs_server(id)
R/mod_outputs.R mod_outputs_ui(id) mod_outputs_server(id, inputs_r)
R/mod_download.R mod_download_ui(id) mod_download_server(id, inputs_r, perf_r)

The two tooltip helpers (mod_tooltip() and mod_hoverinfo()) are not modules in the Shiny sense — they have no server counterpart and no moduleServer() call.

They are UI / rendering helpers that happen to share the mod_ prefix for discoverability.


Module wiring

app_ui() composes the three UI functions into a bslib::page_sidebar() layout. The download module UI is embedded inside the inputs sidebar rather than at the top level, so the sidebar contains all user controls in one place.

app_ui <- function() {
  bslib::page_sidebar(
    sidebar = mod_inputs_ui("inputs"),   # sidebar: includes mod_download_ui()
    mod_outputs_ui("outputs")            # main panel
  )
}

app_server() wires the three server functions together. The outputs server returns a reactive (perf_r) that the download server consumes — this is the only inter-module dependency.

app_server <- function(input, output, session) {
  app_set_log_threshold(logger::INFO)

  inputs_r <- mod_inputs_server("inputs")
  perf_r   <- mod_outputs_server("outputs", inputs_r)
             mod_download_server("download", inputs_r, perf_r)
}

The reactive data flow is strictly top-down:

mod_inputs_server()  ──► inputs_r  ──► mod_outputs_server()  ──► perf_r  ──► mod_download_server()

No module reaches up into its parent or sideways into a sibling.


Inputs: mod_inputs

Files: R/mod_inputs.R
Exports: mod_inputs_ui(), mod_inputs_server()

UI function

mod_inputs_ui(id) returns a bslib::sidebar() containing:

  • selectizeInput — multi-select ticker picker (user-creatable entries)
  • dateRangeInput — date range (defaults to the past year)
  • sliderInput — rolling-volatility window, 5–120 trading days
  • actionButton — “Fetch data” trigger
  • mod_download_ui("download") — embedded at the bottom

mod_tooltip() with type = "bslib" is used on the ticker and vol-window labels to attach bslib popovers without any server-side code:

shiny::selectizeInput(
  inputId = ns("tickers"),
  label   = shiny::tags$span(
    "Tickers",
    mod_tooltip(
      trigger  = bsicons::bs_icon("info-circle"),
      type     = "bslib",
      contents = "Enter one or more stock ticker symbols (e.g. AAPL, MSFT).",
      size     = "0.85rem",
      style    = "color:#6c757d"
    )
  ),
  # ...
)

Server function

mod_inputs_server(id) logs the fetch event and validates that at least one ticker is selected, then returns a reactive list:

list(
  tickers    = character(),  # selected ticker symbols
  from       = Date,         # start of date range
  to         = Date,         # end of date range
  vol_window = integer(),    # rolling-vol window in trading days
  fetch      = integer()     # action-button counter (used as event trigger)
)

The fetch element is an integer counter incremented each time the button is pressed. Downstream modules use eventReactive(inputs_r()$fetch, ...) to re-run only when the user explicitly requests new data.


Outputs: mod_outputs

Files: R/mod_outputs.R
Exports: mod_outputs_ui(), mod_outputs_server()

UI function

mod_outputs_ui(id) returns a shiny::tagList() with two pieces:

  1. shiny::uiOutput(ns("value_boxes")) — KPI value boxes rendered server-side so the number of boxes matches the number of selected tickers.
  2. bslib::navset_card_tab() — five demo tabs, one per tooltip back-end.
Tab Back-end Interaction
bslib bslib::popover() via mod_tooltip() Click info icon
shinyhelper shinyhelper::helper() via mod_tooltip() Click circled-?
prompter prompter::add_prompt() via mod_tooltip() Hover over label
shinyalert data-sa-* attrs + delegated JS via mod_tooltip() Click ticker card
reactable htmltools <span title> via mod_hoverinfo() Hover over cell

Server function

mod_outputs_server(id, inputs_r) is the computational core of the app. It registers shinyhelper::observe_helpers() once per session (required by shinyhelper; do not call it separately in app_server()), then chains four reactives triggered by inputs_r()$fetch:

# 1 — fetch adjusted prices
prices_r <- shiny::eventReactive(inputs_r()$fetch, {
  get_stock_prices(
    tickers = inp$tickers,
    from    = inp$from,
    to      = inp$to
  )
})

# 2 — daily log returns
returns_r <- shiny::reactive({
  get_stock_returns(prices_r())
})

# 3 — annualised performance summary
perf_r <- shiny::reactive({
  summarise_performance(returns_r())
})

# 4 — five renderUI / renderReactable outputs that consume perf_r()

perf_r is returned to app_server() so mod_download_server() can embed it in the rendered report without re-computing it.

KPI value boxes

The value boxes are colored by Sharpe ratio threshold:

theme <- if (sharpe >= 1) "success" else if (sharpe >= 0) "warning" else "danger"

reactable tab and mod_hoverinfo()

The reactable tab is the only output that uses mod_hoverinfo(). Each numeric column in the performance table gets a colDef cell renderer that wraps the formatted value in an htmltools <span title="...">:

reactable::colDef(
  name = "Ann. Return (%)",
  html = TRUE,
  cell = function(value, index) {
    mod_hoverinfo(
      type     = "reactable",
      contents = glue::glue(
        "Annualised log return for {df$symbol[index]}: {value}%"
      ),
      display  = glue::glue("{value}%"),
      style    = paste0(
        "color:", if (value >= 0) "#198754" else "#dc3545",
        "; cursor:help"
      )
    )
  }
)

Downloads: mod_download

Files: R/mod_download.R
Exports: mod_download_ui(), mod_download_server()

UI function

mod_download_ui(id) returns a bslib::card() with:

  • selectInput — report format ("html" or "pdf")
  • downloadButton — triggers the handler

The card is embedded at the bottom of the inputs sidebar by mod_inputs_ui():

# inside mod_inputs_ui()
bslib::sidebar(
  # ... other inputs ...
  mod_download_ui("download")
)

This keeps all user controls in the sidebar while the download server is wired at the top level in app_server().

Server

mod_download_server(id, inputs_r, perf_r) renders inst/report_template.Rmd into a temporary directory via rmarkdown::render() and serves the output through shiny::downloadHandler().

Below are the parameters passed to the template:

params = list(
  tickers    = inp$tickers,
  from       = as.character(inp$from),
  to         = as.character(inp$to),
  vol_window = inp$vol_window,
  perf_data  = perf_r()
)

Rendering into an isolated tempfile() directory ensures the Shiny session working directory is not affected and simultaneous downloads do not collide.


Tooltip helpers

These two functions share the mod_ prefix but are not Shiny modules.

mod_tooltip()

A UI helper — returns a shiny.tag with no server counterpart. Place it anywhere inside a UI tree, including inside renderUI().

mod_tooltip(
  trigger     = bsicons::bs_icon("info-circle"),  # clickable/hoverable element
  type        = "bslib",      # "bslib" | "shinyhelper" | "prompter" | "shinyalert"
  contents    = "Help text.",
  size        = NULL,         # CSS font-size for the wrapper span
  style       = NULL,         # inline CSS for the wrapper span
  helper_type = "inline",     # shinyhelper only
  helper_size = "m",          # shinyhelper only
  alert_type  = "info",       # shinyalert only
  ...                         # forwarded to the back-end
)

mod_hoverinfo()

A rendering helper — returns an htmltools <span title="..."> for use inside reactable::colDef(cell = ..., html = TRUE).

mod_hoverinfo(
  type     = "reactable",    # only supported back-end
  contents = character(0),   # tooltip text; named vector → "Name: value" pairs
  display  = NULL,           # visible cell value
  size     = NULL,           # CSS font-size
  style    = NULL,           # inline CSS
  ...                        # extra HTML attributes on the <span>
)

Adding a new module

Follow these steps to add a fourth module to the app.

1. Create R/mod_<name>.R with mod_<name>_ui() and mod_<name>_server(), both exported with @export.

2. Add the UI call to app_ui():

# app_ui.R
bslib::page_sidebar(
  sidebar = mod_inputs_ui("inputs"),
  mod_outputs_ui("outputs"),
  mod_<name>_ui("<name>")     # add here
)

3. Wire the server in app_server():

# app_server.R
mod_<name>_server("<name>", inputs_r, perf_r)

4. Run devtools::document() to update NAMESPACE and regenerate the help page, then pkgdown::build_site() to update the reference index.