Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ $run_dev.*
^README\.Rmd$
^CODE_OF_CONDUCT\.md$
^scratchpad\.md$
^app\.R$
^\.Renviron\.example$
^\.github$
^.*\.log$
^AGENTS\.md$
^rsconnect$
13 changes: 13 additions & 0 deletions .Rprofile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Set CRAN repository to Posit snapshot matching the R release date
# This ensures reproducible package installation across different environments
local({
version_info <- R.Version()
release_date <- sprintf(
"%s-%02i-%02i",
version_info$year,
as.integer(version_info$month),
as.integer(version_info$day)
)
repos <- sprintf("https://packagemanager.posit.co/cran/%s", release_date)
options(repos = repos)
})
2 changes: 1 addition & 1 deletion .github/actions/capture-cran-snapshot/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ runs:
as.integer(version_info$month),
as.integer(version_info$day)
)
repos <- sprintf("https://packagemanager.rstudio.com/cran/%s", release_date)
repos <- sprintf("https://packagemanager.posit.co/cran/%s", release_date)
cat(
sprintf("CRAN_SNAPSHOT=%s\n", repos),
file = Sys.getenv("GITHUB_ENV"),
Expand Down
70 changes: 69 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,45 @@
- Always use the native pipe operator `|>` instead of the magrittr pipe `%>%` in all R code.
- Use NZ (New Zealand) English spelling for all function names and documentation (e.g., "colour" not "color").
- Name files in `R/` with hyphenated prefixes to signal logical layers (e.g., `domain-value_objects.R`) because subdirectories are not permitted.
- Assume all the required packages are declared in `DESCRIPTION` and installed in the environment; Never not use runtime checks like `if (requireNamespace("pkgname"))` in package scripts.

---

## Shiny Application Design and Development

- When designing a Shiny web app part, use the context7 tool first to plan the work.
- Search context7 for "golem" to understand how to structure the files so the web app behaves like a package.
- Use context7 for the "shiny" package when you need reference on basic Shiny components.
- Consult context7 for "shinydashboard" when working with dashboard-specific layout parts.
- Consult context7 for "shinydashboardPlus" when handling controlbar or other dashboard-plus specific components.
- Refer to context7 for "mastering-shiny" for guidance on Shiny mechanics, best practices, and reactivity patterns.

---

## Architecture and Design Principles

- We use a layered architecture approach, separating concerns into distinct layers (e.g., domain, functions, application) to enhance maintainability and testability.
- Each layer should only depend on layers below it, promoting loose coupling and high cohesion.
- Functions that belong to different layers, live in different files. We name our files in `R/` starting with the layer name, and then hyphenated for the module.

The layers are as such:
- app | mod
- functions
- domain

For functions that receive and/or return `data.frame` we strive to use value objects to our instead of generic data.frames

## Linter
- Markdown: avoid tab characters and keep blank lines before/after fenced code blocks.
- Markdown: start files with a top-level heading.
- Markdown: surround lists with blank lines.
- Markdown: ensure fenced code blocks are balanced (no stray closing ```); remove unmatched fences.
- R: replace non-ASCII characters in R source code with ASCII equivalents (e.g., em dash `—` → hyphen `-`). Non-ASCII characters in data/config (YAML, comments) are acceptable.
- R: avoid raw non-ASCII symbols in R/ source (e.g., µ, superscripts). Use plotmath in code (e.g. `expression(mu)` or `annotate(..., label = "mu == 10", parse = TRUE)`) and use `\eqn{\mu}` (with an ASCII fallback) in roxygen/Rd. Non-ASCII in data/config (YAML, comments) is acceptable.
- R: avoid `@importFrom` for functions that conflict with base R (e.g., `config::get()`, `base::get()`); use namespace notation instead.
- R: in plotmath unit expressions use `*` instead of `~` for tight prefix spacing (e.g., `mu*g/cm^2`).
- R: in roxygen2 `@param` and roxygen comments, escape curly braces as `\{` and `\}` (e.g., `\{shiny\}` not `{shiny}`) to prevent Rd formatting errors.
- R: use `.data$column_name` instead of bare column names in `dplyr` verbs to avoid R CMD check global variable binding warnings (e.g., `dplyr::filter(.data$id == "value")`).

---

Expand Down Expand Up @@ -41,4 +73,40 @@

- **Use ggplot2 for all plots**: All visualizations should be created using the ggplot2 package to ensure consistency and leverage its powerful layering system.

- **Use the `scales` package for axis formatting**: For formatting axis labels and breaks, always use functions from the `scales` package (e.g., `scales::label_number()`, `scales::breaks_extended()`) to ensure consistent and professional appearance.
- **Use the `scales` package for axis formatting**: For formatting axis labels and breaks, always use functions from the `scales` package (e.g., `scales::label_number()`, `scales::breaks_extended()`) to ensure consistent and professional appearance.

---

## Shiny Module & Configuration Best Practices

- **Externalize configuration to YAML**: Store slider parameters, UI configuration, and other settings in `inst/config/` as YAML files instead of hardcoding in R. Use `config::get()` to load at package initialization.

- **Use `purrr::map_dfr()` for robust config conversion**: When converting lists from config files to tibbles, prefer `purrr::map_dfr(cfg, tibble::as_tibble)` over `do.call(rbind, lapply(...))` for better type preservation and error handling.

- **Avoid row iteration with `seq_len(nrow())`**: When iterating over data frame/tibble rows, use `purrr::pmap()` instead of `lapply(seq_len(nrow(...)), ...)` for better performance and readability.

- **Validate config early**: Add error handling when loading external configuration to fail fast if config is empty, malformed, or missing required fields.

---

## Data Wrangling

- Prefer `dplyr` (tidyverse) verbs for data manipulation for clearer intent, consistent semantics with tibbles, and better compatibility with grouped operations.

- Prefer `dplyr` alternatives to base functions. For example:

- use `dplyr::group_split()` rather than `base::split()`
- use `dplyr::bind_rows()` rather than `base::rbind()`
- use `dplyr::filter(.data$col == value)` rather than `base::subset()` or manual indexing

- Avoid concatenating multiple columns into a single string to encode structured information, and avoid ad-hoc parsing of strings to reconstruct columns. Keep distinct data elements in separate columns (or use list-columns) so types and semantics are preserved.

- When splitting, binding, or combining data, prefer the `dplyr`/tidyverse approach to preserve column types and attributes and to avoid unexpected behaviour with tibbles.

---

## Defensive checks guideline

- Never add defensive checks (for example, input validation, `stop()` on missing columns, or NULL guards) inside function bodies unless explicitly instructed to do so.
- Do not add `warning()` calls inside functions unless explicitly instructed to do so.
- Assume validated value objects provide required columns and types; do not re-check inputs inside functions that use value-objects.
23 changes: 21 additions & 2 deletions .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@
# Need help debugging build failures? Start at
# https://github.com/r-lib/actions#where-to-find-help
on:
push:
pull_request:
push:
branches:
- master
workflow_dispatch:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to check'
required: false
default: ''

name: R-CMD-check

Expand All @@ -12,6 +20,8 @@ permissions: read-all
jobs:
R-CMD-check:
runs-on: ubuntu-latest
container:
image: rocker/shiny-verse:4.5.1

env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -37,11 +47,20 @@ jobs:
install-r: false
cran: "${{ env.CRAN_SNAPSHOT }}"

- uses: r-lib/actions/setup-r-dependencies@v2
- name: Cache check tooling
uses: r-lib/actions/setup-r-dependencies@v2
with:
packages: |
any::sessioninfo
extra-packages: any::roxygen2,any::rcmdcheck,any::desc
cache: true
cache-version: ubuntu-r-4-5-1

- name: Cache package dependencies
uses: r-lib/actions/setup-r-dependencies@v2
with:
cache: true
cache-version: ubuntu-r-4-5-1

- name: Build documentation with roxygen2
run: roxygen2::roxygenise()
Expand Down
83 changes: 66 additions & 17 deletions .github/workflows/deploy-shinyapps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ permissions: read-all
jobs:
deploy:
runs-on: ubuntu-latest
container:
image: rocker/shiny-verse:4.5.1
environment: Production

env:
Expand All @@ -22,11 +24,12 @@ jobs:
steps:
- name: Verify required secrets
env:
SHINYAPPS_APPNAME: ${{ secrets.SHINYAPPS_APPNAME || vars.SHINYAPPS_APPNAME }}
SHINYAPPS_NAME: ${{ secrets.SHINYAPPS_NAME || vars.SHINYAPPS_NAME }}
SHINYAPPS_TOKEN: ${{ secrets.SHINYAPPS_TOKEN || vars.SHINYAPPS_TOKEN }}
SHINYAPPS_SECRET: ${{ secrets.SHINYAPPS_SECRET || vars.SHINYAPPS_SECRET }}
run: |
[ -n "$SHINYAPPS_NAME" ] && [ -n "$SHINYAPPS_TOKEN" ] && [ -n "$SHINYAPPS_SECRET" ] || { echo "Error: Missing required secrets (SHINYAPPS_NAME, SHINYAPPS_TOKEN, SHINYAPPS_SECRET)"; exit 1; }
[ -n "$SHINYAPPS_APPNAME" ] && [ -n "$SHINYAPPS_NAME" ] && [ -n "$SHINYAPPS_TOKEN" ] && [ -n "$SHINYAPPS_SECRET" ] || { echo "Error: Missing required secrets (SHINYAPPS_APPNAME, SHINYAPPS_NAME, SHINYAPPS_TOKEN, SHINYAPPS_SECRET)"; exit 1; }

- name: Checkout repository
uses: actions/checkout@v4
Expand All @@ -38,28 +41,74 @@ jobs:
with:
r-version: '4.5.1'

- name: Capture CRAN snapshot from R release date
uses: ./.github/actions/capture-cran-snapshot

- name: Configure CRAN snapshot
uses: r-lib/actions/setup-r@v2
- name: Cache R packages
id: cache-r-packages
uses: actions/cache@v4
with:
r-version: '4.5.1'
install-r: false
cran: "${{ env.CRAN_SNAPSHOT }}"
path: ~/R/deploy-library
key: ${{ runner.os }}-r-4.5.1-deploy-${{ hashFiles('DESCRIPTION') }}
restore-keys: |
${{ runner.os }}-r-4.5.1-deploy-
${{ runner.os }}-r-4.5.1-

- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::rsconnect,any::desc
cache: true
cache-version: ubuntu-r-4-5-1
- name: Configure cached R library
run: |
deploy_lib="${HOME}/R/deploy-library"
mkdir -p "$deploy_lib"
echo "R_LIBS_USER=$deploy_lib" >> "$GITHUB_ENV"

- name: Install deployment packages
env:
PKG_CACHE_HIT: ${{ steps.cache-r-packages.outputs.cache-hit }}
run: |
deploy_lib <- Sys.getenv("R_LIBS_USER", unset = "")
if (deploy_lib == "") {
deploy_lib <- file.path(Sys.getenv("HOME"), "R", "deploy-library")
}
dir.create(deploy_lib, recursive = TRUE, showWarnings = FALSE)
.libPaths(c(deploy_lib, .libPaths()))
cache_hit <- identical(Sys.getenv("PKG_CACHE_HIT"), "true")
repos <- getOption("repos")
if (!cache_hit) {
try(remove.packages(desc::desc_get("Package")), silent = TRUE)
if (!"remotes" %in% rownames(installed.packages())) install.packages("remotes", repos = repos)
pkgs <- c("sessioninfo", "desc", "devtools", "golem", "pkgload", "rsconnect", "usethis")
remotes::install_cran(pkgs, repos = repos, upgrade = "never", force = FALSE)
remotes::install_deps(dependencies = TRUE)
deps <- rsconnect::appDependencies(appDir = ".", appFiles = "DESCRIPTION", appMode = "shiny")
bad <- deps[is.na(deps$Source) | deps$Source %in% c("unknown", "source"), , drop = FALSE]
if (nrow(bad)) remotes::install_cran(bad$Package, repos = repos, upgrade = "never", force = TRUE)
} else {
message("Cache hit detected for deployment library; skipping reinstalls.")
}
deps <- rsconnect::appDependencies(appDir = ".", appFiles = "DESCRIPTION", appMode = "shiny")
print(deps)
shell: Rscript {0}

- name: Deploy to shinyapps.io
- name: Deploy to shinyapps.io (as package)
env:
R_CONFIG_ACTIVE: production
CRAN_SNAPSHOT: ${{ env.CRAN_SNAPSHOT }}
SHINYAPPS_APPNAME: ${{ secrets.SHINYAPPS_APPNAME || vars.SHINYAPPS_APPNAME }}
SHINYAPPS_NAME: ${{ secrets.SHINYAPPS_NAME || vars.SHINYAPPS_NAME }}
SHINYAPPS_TOKEN: ${{ secrets.SHINYAPPS_TOKEN || vars.SHINYAPPS_TOKEN }}
SHINYAPPS_SECRET: ${{ secrets.SHINYAPPS_SECRET || vars.SHINYAPPS_SECRET }}
run: |
Rscript inst/tasks/deploy_to_shinyio.R
deploy_lib <- Sys.getenv("R_LIBS_USER", unset = "")
if (deploy_lib != "") {
dir.create(deploy_lib, recursive = TRUE, showWarnings = FALSE)
.libPaths(c(deploy_lib, .libPaths()))
}
# Prepare the application for deployment
unlink(c(".Rprofile", ".git", ".github"), recursive = TRUE, force = TRUE)
# Add shinyapps.io file
golem::add_shinyappsio_file(open = FALSE)
# Remove package namespace from app.R to prevent rsconnect from treating it as a non-reproducible source dependency
pkg_name <- desc::desc_get("Package")
app_content <- paste(readLines("app.R", encoding = "UTF-8"), collapse = "\n")
pattern <- paste0(pkg_name, "::run_app()")
app_content_clean <- gsub(pattern, "run_app()", app_content, fixed = TRUE)
writeLines(app_content_clean, con = "app.R", useBytes = TRUE)
# Deploy the application
rsconnect::setAccountInfo(name = Sys.getenv('SHINYAPPS_NAME'), token = Sys.getenv('SHINYAPPS_TOKEN'), secret = Sys.getenv('SHINYAPPS_SECRET'))
rsconnect::deployApp(appName = Sys.getenv('SHINYAPPS_APPNAME'), forceUpdate = TRUE, logLevel = 'normal', launch.browser = FALSE, appMode = "shiny")
shell: Rscript {0}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ rsconnect/
man/
scratchpad.md
inst/doc
/.quarto/
/.quarto/
*.html
21 changes: 12 additions & 9 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
Package: whitelabel
Title: An Amazing Shiny App
Version: 0.0.0.9000
Version: 0.1.0.9000
Authors@R:
person(given = "bilbo",
family = "baggins",
role = c("aut", "cre"),
email = "BBaggins@tonkintaylor.co.nz")
person("bilbo", "baggins", , "BBaggins@tonkintaylor.co.nz", role = c("aut", "cre"))
Description: What the package does (one paragraph).
License: file LICENSE
Imports:
Imports:
config,
fresh,
golem,
shiny
Encoding: UTF-8
LazyData: true
RoxygenNote: 7.3.3
Suggests:
knitr,
pkgload,
rmarkdown,
testthat (>= 3.0.0)
VignetteBuilder:
knitr
Config/testthat/edition: 3
Encoding: UTF-8
LazyData: true
RoxygenNote: 7.3.3
14 changes: 14 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
# Generated by roxygen2: do not edit by hand

export(app_server)
export(app_ui)
export(run_app)
import(shiny)
importFrom(fresh,adminlte_color)
importFrom(fresh,adminlte_global)
importFrom(fresh,create_theme)
importFrom(golem,activate_js)
importFrom(golem,add_resource_path)
importFrom(golem,bundle_resources)
importFrom(golem,favicon)
importFrom(golem,with_golem_options)
importFrom(shiny,NS)
importFrom(shiny,fluidPage)
importFrom(shiny,moduleServer)
importFrom(shiny,plotOutput)
importFrom(shiny,renderPlot)
importFrom(shiny,shinyApp)
importFrom(shiny,sidebarLayout)
importFrom(shiny,sliderInput)
importFrom(shiny,tagList)
26 changes: 26 additions & 0 deletions R/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Data Wrangling

- Prefer using packages from the "tidyverse" collection for data manipulation tasks. This includes using "dplyr" for data frame operations, "tibble" for enhanced data frames, and "readr" for reading data files.
- Prefer using "purrr" for functional programming tasks, such as mapping functions over lists or vectors.

## Namespace Management

- **Prefer `@importFrom` over `@import`**: Always use `@importFrom package_name specific_functions` rather than `@import package_name` for all packages. This keeps the namespace clean and makes dependencies explicit by only importing the functions actually used in the codebase.
- **Centralize imports in `r1099894-package.R`**: Declare all `@importFrom` statements in `R/r1099894-package.R` instead of in individual function roxygen comments. This provides a single source of truth for all package dependencies.
- **Exception**: Only use `@import` in rare cases where a package is used extensively throughout the codebase (e.g., `rlang` for error handling utilities across many functions).

## Shiny App

- We are using the "golem" framework for building the Shiny application. Use the context7 mcp tool to get more context on how to use golem functions and structure your app.
- **Colour customization**: Update colour constants in `R/app-themes-constants.R` and use `create_app_theme()` in `R/app-themes.R` to apply them to the dashboard.
- **Theme customization via `app-themes.R`**: All dashboard theme customization (colors, layouts, backgrounds) should be done through the `create_app_theme()` function in `R/app-themes.R`. Before adding custom CSS or other styling methods, check `app-themes.R` first—it likely already provides a way to control the element you want to change. The function uses the `fresh` package to manage:
- Box header colors (via `adminlte_color()`)
- Dashboard backgrounds and box backgrounds (via `adminlte_global()`)
- Sidebar styling (via `adminlte_sidebar()`)
- Other AdminLTE theme variables
- See `R/app-themes.R` for current theme configuration and available customization options.
- **Using `fresh` themes in Shiny**: Call `use_theme(create_app_theme())` directly inside `dashboardBody()`, not in `tagList()`. The theme function must be invoked where it's applied (inside the body element), not stored as a variable in the tagList wrapper.

- **Icons for UI elements**: Always use `icon()` (from Shiny/Font Awesome) instead of emoji symbols or special Unicode characters to avoid non-ASCII characters in R code. Examples: Use `icon("info-circle")` instead of "ℹ️"

- **Controlbar widget width**: When adding widgets to the controlbar, always set `width = "100%"` for widgets that provide a width argument so they align with the panel.
Empty file added R/app-constants.R
Empty file.
6 changes: 3 additions & 3 deletions R/app-server.R
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@

#' The application server-side
#'
#' @param input,output,session Internal parameters for {shiny}.
#' @param input,output,session Internal parameters for \{shiny\}.
#' DO NOT REMOVE.
#' @import shiny
#' @noRd
#' @export
app_server <- function(input, output, session) {
# Your application server logic
mod_histogram_server("histogram_1")
Expand Down
Loading