Interactive experiments in data, statistics, machine learning, and visualization.
Public site:
https://jbkunst.github.io/visual-data-lab/
app-template/: minimal skeleton for new apps.<app-folder>/: one Shiny app per top-level folder.<app-folder>/DESCRIPTION: gallery metadata and runtime configuration.<app-folder>/readme.md: short "How it works" content used by the app.<app-folder>/credits.md: visible author/signature block.<app-folder>/screenshot.png: gallery preview.R/build_site.R: builds the gallery and Shinylive exports in GitHub Actions.R/run_app.R: runs an app from a fresh copy of the repository.index.qmd: Quarto source for the gallery.
Generated files such as apps.yml and docs/ are not source files and are not versioned.
Each public app needs a DESCRIPTION file:
Title: App Title
Description: A short sentence that explains what learners can explore.
Categories: statistics, simulation
The folder name is the app slug.
Shinylive is the default runtime. It does not need to be declared explicitly.
For an app hosted on Posit Connect Cloud:
Runtime: server
AppURL: https://example.share.connect.posit.cloud/
Server apps are deployed independently from the gallery build. Their AppURL is used directly by the site.
Draft apps can remain in the repository without being published:
Status: draft
- Copy
app-template/to a new top-level folder. - Build the app in
app.R. - Fill in
DESCRIPTION. - Add
readme.mdandcredits.md. - Run the app locally and check the interaction.
- Generate and commit
screenshot.png. - Push the changes and let GitHub Actions build and validate the gallery.
Adding a new app should be self-contained in its app folder. Do not modify .github/workflows/pages.yml just to add an app. App-specific data preparation, models, assets, and other runtime files belong in the app folder and should be committed when they are part of the app.
Every app credits.md should use the standard signature:
App made by [Joshua Kunst](https://jkunst.com) with ❤️ and ☕ using Shiny for R ✨. Code [here](https://github.com/jbkunst/visual-data-lab).Generate a gallery screenshot locally with:
webshot2::appshot(
"kmeans",
file = "kmeans/screenshot.png",
delay = 3,
vwidth = 1440,
vheight = 900
)Replace kmeans with the app folder name. Screenshots are source assets and should be committed with the app. GitHub Actions reuses the committed screenshot during the site build.
If a Shinylive app cannot be exported, the build fails. Move it to the server runtime only when there is a deliberate reason to host it on Posit Connect Cloud.
Do not run R/build_site.R locally. To test a single app, remove only the
ignored Shinylive output, export the app, and serve it in the background:
app_folder <- "underfitting-overfitting"
slug <- app_folder
port <- 8000
unlink("docs/live", recursive = TRUE, force = TRUE)
shinylive::export(
appdir = app_folder,
destdir = "docs/live",
subdir = slug,
package_cache = FALSE
)
server <- httpuv::runStaticServer(
dir = "docs/live",
host = "127.0.0.1",
port = port,
background = TRUE,
browse = FALSE
)
later::later(
function() {
browseURL(sprintf("http://127.0.0.1:%s/%s/", port, slug))
},
delay = 1
)Use dir, not path, with httpuv::runStaticServer(). Passing path through
... can produce Not compatible with requested type: [type=character; target=logical] in current httpuv versions. Stop the background server with:
server$stop()If the page is blank after a successful export, close previous preview tabs and retry on a new port. A stale Service Worker may remain associated with the old local origin. Browser console errors are more informative than repeating the export blindly.
Apps use the shared vdltheme package and its bundled IBM Plex Sans font:
library(vdltheme)
apptheme <- theme_vdl()
options(highcharter.theme = highcharter_theme_vdl())Only call highcharter_theme_vdl() in apps that use Highcharts. Pokémon and
Matrix keep their own visual themes; all other apps and app-template use
theme_vdl().
The shared package is the source of truth for the common visual language:
- IBM Plex Sans is bundled in
vdltheme; apps must not depend on Google Fonts or another network font at runtime. - Bootstrap colors flow into the default Highcharts palette in this order: primary, danger, warning, success, info, and secondary.
- Highcharts legends use normal-weight text, line and scatter markers are circles, and chart tooltips use the same light treatment as input help.
- Use the shared theme defaults before adding app-specific colors or CSS. Credit-risk apps may keep their semantic palette: red increases bad-risk probability, blue decreases it, and dark blue identifies the active case.
- Prefer
highchartProxy()when an interaction only changes series data, categories, plot lines, or the active observation. Re-render the full widget only when its structure changes. - Keep labels and educational copy short. Put the app explanation in a closed
accordion before
credits.md, usingreadme.mdfor "How it works" and optionalresources.mdfor references.
For Posit Connect Cloud, install the tagged package locally before regenerating
the app manifest. Do not install packages from inside app.R:
remotes::install_github(
"jbkunst/visual-data-lab",
subdir = "vdltheme",
ref = "vdltheme-v0.0.3",
upgrade = "never"
)
rsconnect::writeManifest(appDir = "app-folder")Shinylive cannot install the regular Linux or Windows build of a repository
package. vdltheme therefore has a tagged WebAssembly build attached to each
package release. The workflow is defined in
.github/workflows/release-vdltheme-wasm.yml and uses r-wasm/actions.
The workflow's known-good build configuration is intentional:
- run on
ubuntu-24.04; - check out the
vdltheme-v<version>release tag, not the movingmasterbranch; - check out
r-wasm/actionswithref: v3into.actions; - invoke the local composite action with
uses: ./.actions/build-rwasm; - build
packages: "./vdltheme"withwebr-image: ghcr.io/r-wasm/webr:main; - grant
contents: writeand upload the generated files back to the same release tag.
Earlier v1 and reusable-workflow variants were not compatible with this
working build. Do not downgrade or simplify these pins without validating a
complete release and a Shinylive export. The tag prefix also matters: the job
runs only for tags beginning with vdltheme-v.
When changing vdltheme:
-
Bump
Versioninvdltheme/DESCRIPTIONand commit the complete package change. -
Push the commit, then publish a tag named
vdltheme-v<version>:gh release create vdltheme-v0.0.4 \ --target master \ --title vdltheme-v0.0.4 \ --generate-notes
-
Confirm that Release vdltheme WebAssembly succeeds. It attaches
library.data.gzandlibrary.js.metadatato the GitHub release. -
Install that exact tag locally before testing Shinylive or regenerating a Posit Connect manifest:
remotes::install_github( "jbkunst/visual-data-lab", subdir = "vdltheme", ref = "vdltheme-v0.0.4", upgrade = "never", force = TRUE )
Installing directly from the local
vdltheme/directory is not equivalent: it omits the GitHubRemote*metadata that Shinylive uses to locate the package's Wasm release. The Pages workflow installs the tagged GitHub package for the same reason. -
Update the matching
vdlthemeref in.github/workflows/pages.yml. Rewrite manifests for server apps whose deployment uses the new package version. -
Push the source changes and let the Pages workflow export all Shinylive apps. Do not commit its generated
docs/output.
The release workflow can also rebuild an existing tag manually:
gh workflow run release-vdltheme-wasm.yml -f tag=vdltheme-v0.0.4Keep the package release, the locally installed tag, the Pages workflow ref, and server-app manifests on the same version. A release is not ready for Shinylive until both Wasm assets exist. Package dependencies must themselves be available to webR, and runtime assets such as fonts must be bundled in the package rather than fetched from the internet.
If export reports vdltheme not available in Wasm binary repository, check in
this order:
- the package version matches the release tag;
- the release contains both Wasm assets;
- the local package was installed with
remotes::install_github()from that exact tag; .github/workflows/pages.ymlreferences the same tag;- the browser is not trying to fetch a runtime asset such as
fonts.googleapis.com—IBM Plex Sans must come fromvdltheme/inst/fonts.
Use input tooltips conservatively. Add them only when a control represents a non-obvious statistical or mathematical concept, uses a confusing scale, changes the result conceptually, or needs context that does not fit naturally in its label.
- Treat two to four tooltips per app as an upper limit, not a target.
- Do not add help to obvious controls such as the number of observations unless there is an important non-obvious consequence.
- Keep each tooltip focused on one idea and preferably below 25 words.
- Do not repeat the general explanation already available in
readme.md. - Preserve existing label wrappers such as
tags$small()and the current sidebar spacing. - Use namespace-qualified calls instead of adding
library(bsicons). - Do not add
titleto the icon: it creates a native browser tooltip that competes withbslib::tooltip(). - Add a small spacing utility such as
class = "ms-1"so the icon remains visually separate from the label. - Support hover, keyboard focus, and click/touch with
options = list(trigger = "hover focus click"). - Keep tooltip theming minimal; a softer dark background preserves Bootstrap's default white text.
- Test the icon at normal and narrow widths and verify the interaction directly in the running app.
Customize the shared tooltip appearance only when an app needs it:
apptheme <- theme_vdl(tooltip_bg = "#495057")Use the shared label helper:
label = input_label_vdl(
"Parameter name",
"Short explanation."
)Before implementing tooltips across an app, list the selected inputs and why each one needs help. It is valid for an app to need no tooltips.
For apps built around a fillable sidebar and cards, use these four parameters as the default layout convention:
card <- purrr::partial(
bslib::card,
full_screen = TRUE,
wrapper = purrr::partial(bslib::card_body, padding = 0)
)
ui <- page_fillable(
padding = 0,
layout_sidebar(
fillable = TRUE,
padding = "0.75rem",
sidebar = sidebar(...),
layout_columns(
gap = "0.75rem",
...
)
)
)Each parameter controls a different level of spacing:
page_fillable(padding = 0)removes spacing around the full application page.layout_sidebar(padding = "0.75rem")adds spacing around the main card area.layout_columns(gap = "0.75rem")controls the space between cards.card_body(padding = 0)lets plots and HTML widgets use the full card body.
Pass plots and widgets directly to the configured card() helper so its wrapper
is applied. An explicit card_body() without padding = 0 restores Bootstrap's
default card padding and bypasses this convention.
Use col_widths and row_heights explicitly when the composition matters; a
2-by-2 comparison should normally use equal column widths and
row_heights = c(1, 1). Prefer these bslib layout arguments over custom CSS
for page padding, card gaps, or card-body spacing. Keep app-specific exceptions
inside the app instead of expanding the shared theme into a layout framework.
Do not run R/build_site.R locally. The script is part of the GitHub Actions
workflow and rebuilds generated files such as apps.yml and docs/. Push the
source changes and let GitHub Actions execute the build in its controlled
environment.
The build:
- reads app metadata;
- skips drafts;
- exports every Shinylive app to
docs/live/; - stops if a declared Shinylive app cannot be exported;
- uses
AppURLfor server apps; - prepares gallery screenshots and
apps.yml; - renders the Quarto site to
docs/.
GitHub Actions runs the same build for pull requests and pushes to master.
Pull requests build the complete site as a compatibility check but do not publish it. Pushes to master build docs/ and deploy that directory as a GitHub Pages artifact.
GitHub Pages should use GitHub Actions as its publishing source. No generated docs/ branch or commit is required.
Apps with Runtime: server are published separately by Posit Connect Cloud from their own app folders.
Many apps include Markdown with htmltools::includeMarkdown(). Keep reusable explanatory text in readme.md and visible signature text in credits.md.
For MathJax inside included Markdown, use double backslashes:
\\(k\\)
\\((r_i, g_i, b_i, x_i, y_i)\\)Run an app from a fresh copy of the repository with:
source("https://raw.githubusercontent.com/jbkunst/visual-data-lab/master/R/run_app.R")
run_app("kmeans")