Tooltip and Hover-Info Explorer
tooltips-hover-info.Rmdtooltipexplorer is a Shiny application-package for
demoing and comparing tooltip and hover-info approaches in R using real
financial data from Tidy
Finance (tidyfinance) and tidyquant.
Package structure
Each module lives in a single file containing both its
_ui() and _server() functions. All other
exported objects have one file each.
R/
├── app_server.R # app_server()
├── app_set_log_threshold.R # app_set_log_threshold()
├── app_ui.R # app_ui()
├── compute_rolling_vol.R # compute_rolling_vol()
├── default_tickers.R # default_tickers (character vector)
├── get_ff3_factors.R # get_ff3_factors()
├── get_stock_prices.R # get_stock_prices()
├── get_stock_returns.R # get_stock_returns()
├── launch.R # launch()
├── mod_download.R # mod_download_ui() + mod_download_server()
├── mod_hoverinfo.R # mod_hoverinfo()
├── mod_inputs.R # mod_inputs_ui() + mod_inputs_server()
├── mod_outputs.R # mod_outputs_ui() + mod_outputs_server()
├── mod_tooltip.R # mod_tooltip()
├── summarise_performance.R # summarise_performance()
├── utils_operators.R # %||%
└── with_logging.R # with_logging()
Launching the app
tooltipexplorer::launch()launch() calls
shiny::shinyApp(app_ui(), app_server) and accepts
... forwarded to shiny::shinyApp()
(e.g. options = list(port = 4000)).
App architecture
Entry points
| File | Function | Role |
|---|---|---|
launch.R |
launch() |
Creates and runs the Shiny app |
app_ui.R |
app_ui() |
Top-level bslib::page_sidebar() layout |
app_server.R |
app_server() |
Wires all module servers together |
Reactive flow
launch()
└─ shinyApp(app_ui(), app_server)
app_ui()
├─ mod_inputs_ui("inputs") # sidebar: tickers, dates, vol window, fetch
└─ mod_outputs_ui("outputs") # main: KPI boxes + 5 demo tabs
app_server()
├─ mod_inputs_server("inputs") → inputs_r (reactive list)
├─ mod_outputs_server("outputs", inputs_r)
│ ├─ shinyhelper::observe_helpers() # registered here, per session
│ └─ returns perf_r (reactive tibble)
└─ mod_download_server("download", inputs_r, perf_r)
The download module UI (mod_download_ui("download")) is
embedded at the bottom of the inputs sidebar inside
mod_inputs_ui(); its server is wired at the top level in
app_server().
Modules
All modules follow the mod_<name>_ui() /
mod_<name>_server() convention and are co-located in
a single mod_<name>.R file.
Inputs — mod_inputs.R
mod_inputs_ui(id) — returns a
bslib::sidebar() with:
-
selectizeInput— ticker picker (multi-select, user-creatable) -
dateRangeInput— date range -
sliderInput— rolling-volatility window (5–120 trading days) -
actionButton— “Fetch data” -
mod_download_ui()embedded at the bottom
mod_inputs_server(id) — returns a
reactive list:
Outputs — mod_outputs.R
mod_outputs_ui(id) — returns a
shiny::tagList() containing:
-
shiny::uiOutput— KPI value boxes (one per ticker) -
bslib::navset_card_tab()— five demo tabs (see below)
mod_outputs_server(id, inputs_r) —
triggered by inputs_r()$fetch:
- Calls
shinyhelper::observe_helpers()once per session (registered here) - Calls
get_stock_prices()→prices_r - Calls
get_stock_returns(prices_r())→returns_r - Calls
summarise_performance(returns_r())→perf_r - Renders all outputs (value boxes + five tabs)
-
Returns
perf_rfor the download module
Download — mod_download.R
mod_download_ui(id) — a
bslib::card() with a format selector and
downloadButton.
mod_download_server(id, inputs_r, perf_r)
— renders a parameterised inst/report_template.Rmd into
HTML or PDF and serves it via shiny::downloadHandler().
Data utilities
Financial data is fetched and processed through four functions:
| File | Function | Description |
|---|---|---|
get_stock_prices.R |
get_stock_prices(tickers, from, to) |
Daily adjusted prices via tidyquant::tq_get()
|
get_stock_returns.R |
get_stock_returns(prices) |
Daily log returns: log(adjusted / lag(adjusted))
|
compute_rolling_vol.R |
compute_rolling_vol(returns, window) |
Rolling annualised volatility via
slider::slide_dbl()
|
summarise_performance.R |
summarise_performance(returns) |
Per-ticker ann. return, ann. vol, Sharpe ratio |
get_ff3_factors.R |
get_ff3_factors(start_date, end_date) |
Fama-French 3-factor data via tidyfinance
|
default_tickers.R |
default_tickers |
Character vector of default mega-cap tickers |
Example usage outside Shiny
library(tooltipexplorer)
prices <- get_stock_prices(c("AAPL", "MSFT"), from = "2024-01-01")
returns <- get_stock_returns(prices)
perf <- summarise_performance(returns)
vol <- compute_rolling_vol(returns, window = 30L)Tooltip helpers
Two helper functions provide a uniform interface for attaching tooltip and hover content across all five back-ends.
mod_tooltip()
A UI helper (in mod_tooltip.R) —
returns a shiny.tag with no server-side counterpart. Place
it anywhere inside a UI tree, including inside
renderUI().
mod_tooltip(
trigger = bsicons::bs_icon("info-circle"), # default
type = c("bslib", "shinyhelper", "prompter", "shinyalert"),
contents = "",
size = NULL, # CSS font-size for the wrapper span
style = NULL, # extra inline CSS for the wrapper span
helper_type = "inline", # "inline" | "markdown" (shinyhelper only)
helper_size = "m", # "s" | "m" | "l" (shinyhelper only)
alert_type = "info", # "info"|"success"|"warning"|"error" (shinyalert only)
... # forwarded to the back-end function
)type |
Back-end | Interaction | Extra ... args |
|---|---|---|---|
"bslib" |
bslib::popover() |
Click |
title, placement
|
"shinyhelper" |
shinyhelper::helper() |
Click |
title, colour, icon,
buttonLabel, easyClose, fade
|
"prompter" |
prompter::add_prompt() |
Hover |
position, rounded, bounce,
arrow, animate
|
"shinyalert" |
data-sa-* attrs + delegated JS |
Click |
title, confirmButtonText
|
bslib example
mod_tooltip(
type = "bslib",
contents = "Annualised log return = mean daily log return \u00d7 252.",
title = "Ann. Return"
)shinyhelper example
-
shinyhelper’s own.on('click', '.shinyhelper-icon')binding runs once at page load, so it misses icons injected later byrenderUI.- This document-level delegated handler catches every click regardless
of when the icon was inserted, stops the (now-duplicate) built-in
handler via
stopImmediatePropagation(), then sets the same input thatshinyhelper’sobserve_helpers()shiny::observeEvent()listens on.
- This document-level delegated handler catches every click regardless
of when the icon was inserted, stops the (now-duplicate) built-in
handler via
{priority: 'event'}ensures re-firing when the same ticker is clicked twice in a row (identical value would otherwise be dropped).
In app_ui.R:
shiny::tags$script(htmltools::HTML(
"$(document).on('click', '.shinyhelper-icon', function(e) {",
" e.stopImmediatePropagation();",
" var d = $(this).data();",
" Shiny.setInputValue('shinyhelper_params', {",
" size: d.modalSize,",
" type: d.modalType,",
" title: d.modalTitle,",
" content: d.modalContent,",
" label: d.modalLabel,",
" easyClose: d.modalEasyclose,",
" fade: d.modalFade",
" }, {priority: 'event'});",
"});"
))
mod_tooltip(
trigger = shiny::tags$span("Sharpe Ratio"),
type = "shinyhelper",
contents = c("Sharpe Ratio", "Assumes a zero risk-free rate."),
helper_type = "inline",
helper_size = "m",
title = "Sharpe Ratio"
)prompter example
mod_tooltip(
trigger = shiny::tags$span("Ann. Vol"),
type = "prompter",
contents = "Annualised volatility = SD of daily log returns \u00d7 \u221a252.",
position = "right"
)shinyalert example
The shinyalert back-end stores content in
data-sa-* attributes and fires on click via a delegated
jQuery handler injected once in app_ui().
In app_ui.R:
shiny::tags$script(htmltools::HTML(
"$(document).on('click', '.sa-trigger', function () {",
" var d = $(this).data();",
" swal({",
" title: d.saTitle || '',",
" text: d.saText || '',",
" type: d.saType || 'info',",
" html: true,",
" confirmButtonText: d.saBtn || 'OK'",
" });",
"});"
)),No observeEvent() or server handler is needed:
mod_tooltip(
trigger = shiny::tags$span(class = "fs-4 fw-bold", "AAPL"),
type = "shinyalert",
contents = "Ann. Return: 14.2%<br>Ann. Vol: 22.1%<br>Sharpe: 0.64",
title = "AAPL \u2013 Performance Summary",
alert_type = "info",
confirmButtonText = "Close"
)
mod_hoverinfo()
A rendering helper (in mod_hoverinfo.R)
— formats hover content for back-ends that build tooltips
programmatically inside table-cell renderers.
mod_hoverinfo(
type = "reactable", # currently the only supported back-end
contents = character(0), # tooltip text; named vector → "Name: value" pairs
display = NULL, # visible cell value (child node of the <span>)
size = NULL, # CSS font-size
style = NULL, # inline CSS (e.g. "color:#198754; cursor:help")
... # extra HTML attributes on the <span>
)Returns an htmltools <span> with a
title attribute — browsers render this as a native tooltip
on hover. Use inside
reactable::colDef(cell = ..., html = TRUE).
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"
)
)
}
)Logging utilities
| File | Function | Description |
|---|---|---|
app_set_log_threshold.R |
app_set_log_threshold(level) |
Sets logger threshold across all package
namespaces |
with_logging.R |
with_logging(expr, context, ns) |
tryCatch wrapper that logs warnings and errors |
utils_operators.R |
%||% |
Null-coalescing operator |
app_set_log_threshold() is called once in
app_server() at session start. Pass
logger::DEBUG during development for verbose output:
# In app_server(), or interactively:
app_set_log_threshold(logger::DEBUG)Decision guide
| Where does the tooltip appear? | Use | type |
|---|---|---|
| Input label / icon in sidebar | mod_tooltip() |
"bslib" |
| Metric card with help modal | mod_tooltip() |
"shinyhelper" |
| Metric label — CSS hover | mod_tooltip() |
"prompter" |
| Clickable element → modal alert | mod_tooltip() |
"shinyalert" |
| reactable table cell | mod_hoverinfo() |
"reactable" |