shinyngs ships three presentation features that are easy to miss but change how an app looks and how its views are shared: a colour-blind-safe categorical palette (with an in-app picker), a light/dark theme built on Bootstrap 5, and URL bookmarking that turns the current view into a shareable link. The code chunks below are illustrative and are not evaluated when this article is built.
Colour-blind-safe palette
Every categorical colour in shinyngs comes, by default, from a single
fixed palette: COLORBLIND_PALETTE, an Okabe & Ito
(2008) colour-blind-safe set of eight hues, ordered so that
colour-vision-deficiency separation stays highest between neighbouring
entries. The original palette’s black swatch is replaced with a mid grey
(#595959) so it remains visible against both the light and
dark app backgrounds.
COLORBLIND_PALETTE
#> "#E69F00" "#56B4E9" "#009E73" "#F0E442"
#> "#0072B2" "#D55E00" "#CC79A7" "#595959"Colours are handed out by the exported
make_color_scale() helper. Because it draws from the fixed
palette in order, the same position always gets the same colour, so a
group keeps its colour across every plot in the app:
library(shinyngs)
# The colour-blind-safe palette is the default (palette = "colorblind")
make_color_scale(4)
#> "#E69F00" "#56B4E9" "#009E73" "#F0E442"
# Ask for more colours than the base palette provides and shades are
# interpolated between them, with a message noting that colour-blind
# separation can no longer be guaranteed for every pair.
make_color_scale(12)Passing any other value treats it as an RColorBrewer qualitative
palette name (for example "Dark2" or "Set1"),
expanded with interpolation when more colours are requested than the
palette holds:
make_color_scale(6, palette = "Dark2")The in-app palette picker
Modules that colour by an experimental variable expose a palette
drop-down through the colormaker module
(colormakerInput() / colormaker()). The first
choice, labelled “Colour-blind safe”, selects
COLORBLIND_PALETTE; the remaining choices are the
RColorBrewer qualitative palettes, offered for users who prefer them.
The colour-blind-safe palette is always the pre-selected default.
# UI: the palette drop-down
colormakerInput(ns("groupby"))
# Server: a reactive returning the chosen palette, sized to the number of
# categories the caller reports
getPalette <- colormaker("groupby", getNumberCategories = getNumberCategories)In practice you rarely wire colormaker up yourself: the
groupby module already embeds the picker via
colormakerInput() and returns getPalette (a
reactive) alongside getGroupby. A plotting module then
passes that palette straight into its standalone plot function (for
example
interactive_boxplot(..., palette = groupby_reactives$getPalette())).
Those plot functions also accept a palette_name argument
(defaulting to the colour-blind-safe palette) so they produce consistent
colours when called directly, outside an app.
Theming: Bootstrap 5 and light/dark mode
The app shell is a bslib::page_navbar() built in
shinyngsPageNavbar(). It applies a Bootstrap 5 theme via
bslib::bs_theme() using the cosmo bootswatch,
with the package’s accent colour (SHINYNGS_ACCENT,
#2780e3) set as the Bootstrap primary. That
accent is defined once, on the theme, and everything else (CSS, spinner
colours, plot styling) derives from the resulting Bootstrap
variables.
bslib::bs_theme(version = 5, bootswatch = "cosmo", primary = "#2780e3")Light/dark mode toggle
The navbar carries a light/dark switch built from
bslib::input_dark_mode(), with the fixed id
shinyngs_dark_mode. Toggling it flips Bootstrap’s
data-bs-theme attribute, which restyles the whole page.
bslib::input_dark_mode(id = "shinyngs_dark_mode")One subtlety worth knowing: input_dark_mode only sets
data-bs-theme once its web component hydrates on the
client. To avoid a flash of the light theme on a dark-resolved load,
shinyngsPageNavbar() injects a tiny inline
<head> script that resolves the OS colour scheme and
sets data-bs-theme before the first paint.
The dark-mode toggle is deliberately not bookmarked (see below), so a shared link does not force your colour-scheme preference on whoever opens it.
Configuring the theme
Theming is centralised in shinyngsPageNavbar() rather
than exposed as a per-app option, so all app types (rnaseq,
chipseq, illuminaarray, and the single-module
simpleApp) get the same look. To re-skin the app, the
change is made in one place: swap the bootswatch name or
the primary colour on the bs_theme() call, and
the CSS and plots follow from the Bootstrap variables it produces.
Shareable views and URL bookmarking
shinyngs uses Shiny’s URL bookmarking
(enableBookmarking("url")) so that the current view, that
is, which tab is open, which experiment and assay are selected, the
row/column filters, plot options and so on, is encoded into the address
bar. Copying that URL and re-opening it restores the same view.
Bookmarking is enabled in two places: prepare_app() sets
it so the option is present wherever the app object ends up, and
configureBookmarking() sets it again in the running session
(harnesses can transport the app object to a fresh process where the
earlier option would be lost).
The “Share view” button
The navbar’s “Share view” control is an actionButton
with the id shinyngs_share_view. Clicking it calls
session$doBookmark(), which writes the state URL to the
address bar, copies it to the clipboard (via a custom
shinyngs_copy_link message handled in the package JS), and
shows a confirmation notification.
observeEvent(input$shinyngs_share_view, {
session$doBookmark()
})
onBookmarked(function(url) {
updateQueryString(url)
session$sendCustomMessage("shinyngs_copy_link", url)
showNotification("Shareable link copied to your clipboard ...")
})What is and isn’t captured
configureBookmarking() keeps a live exclude list so the
URL stays useful. Transient or bloating inputs are excluded as they
appear: DataTable internals (_rows_current,
_state, _search, …), plotly event streams
(plotly_), help-modal triggers, and the presentation
controls that shouldn’t travel with a shared link
(shinyngs_dark_mode, shinyngs_plot_format,
shinyngs_share_view). An observer watches for
newly-appeared inputs (tables and inserted contrast filters stream in
after the initial page) and pushes an updated exclude set via
setBookmarkExclude().
Two pieces of state need explicit handling because bslib navsets and server-side selectize inputs don’t bookmark themselves:
- The active navbar tab is saved in
onBookmark()and re-applied inonRestored()viaupdateTabsetPanel(). - Server-side selectize inputs hold no options client-side, so their
saved value is re-applied when their choices repopulate.
bookmarkedInputValue()reads that value back out of the restore state, trying the module’s namespaced id first and falling back to the bare id.
bookmarkedInputValue(state, session, "myField")The net effect: a colleague opening your shared link lands on the same tab, looking at the same experiment, assay, filters and plot settings you had, while their own light/dark preference is left untouched.
