Contributing to BFHtheme
April 24, 2026 · View on GitHub
Thank you for your interest in contributing to BFHtheme! This guide will help you get started with development, testing, and submitting contributions.
Table of Contents
- Getting Started
- Package Architecture
- Utility Helper Conventions
- Caching Strategy
- Development Workflow
- Code Style Guidelines
- Testing
- Documentation
- Commit Guidelines
- Pull Request Process
- Review Checklist
Getting Started
Prerequisites
- R (>= 4.0.0)
- RStudio (recommended)
- Git
Setup Development Environment
- Clone the repository:
git clone https://github.com/johanreventlow/BFHtheme.git
cd BFHtheme
- Install development dependencies:
# Install devtools if not already installed
install.packages("devtools")
# Install all package dependencies
devtools::install_dev_deps()
# Install suggested packages for full functionality
install.packages(c("testthat", "covr", "lintr", "styler", "roxygen2"))
- Load the package for development:
devtools::load_all()
- Verify installation:
# Run all tests
devtools::test()
# Check package
devtools::check()
Package Architecture
Understanding the module structure of BFHtheme helps you navigate the codebase and know where to add new features.
Module Overview
BFHtheme is organized into focused, single-responsibility modules:
R/
├── themes.R # ggplot2 theme functions
├── colors.R # Color palettes and color utilities
├── scales.R # ggplot2 scale functions
├── fonts.R # Font detection with session caching
├── helpers.R # Plot utilities (save, combine, labs)
├── branding.R # Logo and branding overlay functions
├── logo_helpers.R # Logo path resolution
├── defaults.R # Global defaults management
├── utils_operators.R # Internal operators (%||%)
├── utils_validation.R # Input validation helpers
└── BFHtheme-package.R # Package-level documentation
Module Responsibilities
Core Theming
-
themes.R- ggplot2 theme() objects- Exports:
theme_bfh() - Responsibilities: Define theme elements, typography, spacing, grids
- Dependencies:
fonts.Rfor font detection,marqueefor text rendering - Note: Theme variants removed in v0.2.0 - users customize via
theme()calls
- Exports:
-
colors.R- Color definitions and palette management- Exports:
bfh_colors,bfh_palettes,bfh_cols(),bfh_pal() - Responsibilities: Store official colors, create palette generators
- Pattern: Uses session-level caching for palette functions
- Exports:
-
scales.R- ggplot2 scale_* functions- Exports:
scale_color_bfh(),scale_fill_bfh(),scale_*_date_bfh(), etc. - Responsibilities: Bridge between palettes and ggplot2's scale system
- Dependencies:
colors.Rfor palette data
- Exports:
Supporting Infrastructure
-
fonts.R- Font detection and caching- Exports:
get_bfh_font(),check_bfh_fonts(),clear_bfh_font_cache() - Responsibilities: Auto-detect available fonts (Mari → Roboto → Arial → sans)
- Pattern: Session-level caching for 10-15x speedup
- Internal:
.bfh_font_cacheenvironment
- Exports:
-
defaults.R- Global plot defaults- Exports:
set_bfh_defaults(),reset_bfh_defaults() - Responsibilities: Set package theme/colors as global ggplot2 defaults
- Pattern: Uses ggplot2's
theme_set()andupdate_geom_defaults()
- Exports:
User Utilities
-
helpers.R- Plot workflow utilities- Exports:
bfh_save(),bfh_combine(),bfh_labs() - Responsibilities: Simplify common tasks (saving with presets, combining plots, uppercase labels)
- Dependencies: Uses
patchworkfor combining (suggested dependency)
- Exports:
-
branding.R- Visual branding overlays- Exports:
add_bfh_logo(),add_bfh_footer(),bfh_title_block() - Responsibilities: Add logos, footers, and branded elements to plots
- Pattern: Uses
gridgraphics to overlay elements post-rendering - Security: Validates file paths to prevent path traversal
- Exports:
-
logo_helpers.R- Logo path resolution- Exports:
get_bfh_logo(),get_region_h_logo() - Responsibilities: Resolve logo paths for different sizes/variants (color, grey, mark)
- Pattern: Uses
system.file()to locate package-bundled logos
- Exports:
Internal Utilities
-
utils_operators.R- Common operators- Exports: None (internal only)
- Provides:
%||%(NULL coalescing operator) - Usage:
value <- input %||% default
-
utils_validation.R- Input validation helpers- Exports: None (internal only)
- Provides:
validate_palette(),validate_size(),validate_logical(), etc. - Pattern: Fail-fast validation at function entry points
-
BFHtheme-package.R- Package documentation- Exports: Package-level help (
?BFHtheme) - Responsibilities: Import statements, package description, global docs
- Exports: Package-level help (
Data Flow & Interactions
User Code
↓
┌───────────────────────────────────────────────┐
│ themes.R │
│ ├→ fonts.R (get font) │
│ └→ returns theme() object │
└───────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────┐
│ scales.R │
│ ├→ colors.R (get palette) │
│ └→ returns scale_* object │
└───────────────────────────────────────────────┘
↓
ggplot2 rendering
↓
┌───────────────────────────────────────────────┐
│ branding.R │
│ ├→ logo_helpers.R (resolve path) │
│ └→ grid overlay on plot │
└───────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────┐
│ helpers.R │
│ └→ save with presets or combine plots │
└───────────────────────────────────────────────┘
Where to Add New Code
New theme variant?
→ Add to themes.R
- Follow existing pattern (
theme_bfh_*) - Use
get_bfh_font()for font detection - Return a complete
theme()object
New color palette?
→ Add to colors.R
- Add colors to
bfh_colorslist - Add palette definition to
bfh_paletteslist - Update palette documentation
New scale function?
→ Add to scales.R
- Follow naming:
scale_<aesthetic>_bfh() - Use
bfh_pal()for palette generation - Support both discrete and continuous if applicable
New branding element?
→ Add to branding.R
- Validate file paths with
normalizePath() - Use
gridgraphics for overlays - Test with different plot types
New plot helper?
→ Add to helpers.R
- Keep functions composable and simple
- Document presets/defaults clearly
- Use
%||%for default parameter handling
Internal utility?
→ Add to utils_*.R files
utils_operators.Rfor operatorsutils_validation.Rfor validation functions- Prefix internal functions with
.
Architectural Principles
- Single Responsibility: Each module has one clear purpose
- Composability: Functions work together via
+operator or piping - Graceful Degradation: Fallbacks for missing fonts/resources
- Session-level Caching: Expensive operations cached per R session
- Input Validation: All exported functions validate inputs early
- NULL Safety: Use
%||%for safe default handling
Example: Adding a New Feature
Let's say you want to add a new theme variant theme_bfh_compact():
-
Open
R/themes.R -
Write the function:
#' BFH Compact Theme #' #' @param base_size Base font size #' @param base_family Base font family #' @export theme_bfh_compact <- function(base_size = 10, base_family = NULL) { # Validate inputs if (!is.numeric(base_size) || base_size <= 0) { stop("base_size must be positive", call. = FALSE) } # Get font with fallback base_family <- base_family %||% get_bfh_font(check_installed = TRUE, silent = TRUE) # Build theme theme_bfh(base_size = base_size, base_family = base_family) + theme( plot.margin = margin(5, 5, 5, 5), # ... compact spacing ) } -
Add tests in
tests/testthat/test-themes.R:test_that("theme_bfh_compact creates valid theme", { theme <- theme_bfh_compact() expect_s3_class(theme, "theme") }) -
Document and check:
devtools::document() devtools::test() devtools::check()
Utility Helper Conventions
BFHtheme provides standardized utility helpers to ensure consistent input validation, error handling, and NULL safety across the codebase. All contributors must use these helpers to maintain code quality and consistency.
NULL Coalescing Operator (%||%)
The %||% operator (defined in utils_operators.R) provides safe NULL handling for default values.
Definition:
`%||%` <- function(x, y) {
if (is.null(x)) y else x
}
When to use:
- Setting default values for optional parameters
- Providing fallbacks when values might be NULL
- Avoiding verbose
if (is.null(x))checks
Pattern:
# ✅ Good - Use %||% for NULL defaults
your_function <- function(base_family = NULL, text = NULL, width = NULL) {
# Simple, readable NULL coalescing
base_family <- base_family %||% get_bfh_font()
text <- text %||% "Default text"
width <- width %||% dims$width
# ... rest of function
}
# ❌ Bad - Verbose NULL checking
your_function <- function(base_family = NULL) {
if (is.null(base_family)) {
base_family <- get_bfh_font()
}
# ... rest of function
}
Real-world examples from BFHtheme:
# From themes.R - Font fallback
base_family <- base_family %||% get_bfh_font(check_installed = TRUE, silent = TRUE)
# From helpers.R - Dimension defaults
width <- width %||% dims$width
height <- height %||% dims$height
# From branding.R - Text fallback
text <- text %||% "Bispebjerg og Frederiksberg Hospital"
# From fonts.R - Font path fallbacks
bold = font_paths$bold %||% font_paths$regular,
italic = font_paths$italic %||% font_paths$regular
Important: Only use %||% for NULL checks. For other default logic, use explicit if statements.
Validation Helpers
All validation helpers are defined in utils_validation.R and provide consistent error messages and fail-fast behavior.
1. validate_palette_argument(palette)
Purpose: Validate palette name is a single, non-empty string
Usage:
scale_color_bfh <- function(palette = "main", ...) {
# Validate at function entry
palette <- validate_palette_argument(palette)
# Now safe to use palette
colors <- bfh_pal(palette)
}
Validates:
- ✅ Is character type
- ✅ Has length 1 (not a vector)
- ✅ Is not NA
- ✅ Is not empty string (
"")
Error example:
validate_palette_argument("")
# Error: palette must be a single character string
validate_palette_argument(c("main", "secondary"))
# Error: palette must be a single character string
2. validate_logical_argument(value, name)
Purpose: Validate boolean flags are proper logical values
Usage:
scale_color_bfh <- function(palette = "main", reverse = FALSE, discrete = TRUE, ...) {
palette <- validate_palette_argument(palette)
reverse <- validate_logical_argument(reverse, "reverse")
if (discrete) {
discrete <- validate_logical_argument(discrete, "discrete")
}
}
Validates:
- ✅ Is logical type
- ✅ Has length 1 (not a vector)
- ✅ Is not NA
Error example:
validate_logical_argument(NA, "reverse")
# Error: reverse must be a single logical value (TRUE or FALSE)
validate_logical_argument(c(TRUE, FALSE), "discrete")
# Error: discrete must be a single logical value (TRUE or FALSE)
3. validate_numeric_range(value, name, min, max, allow_null = FALSE)
Purpose: Validate numeric values are within allowed ranges
Usage:
add_bfh_logo <- function(plot, logo_path, size = 0.1, alpha = 1, padding = 0.02) {
# ... other validation
# Validate numeric parameters with ranges
size <- validate_numeric_range(size, "size", 0.001, 1)
alpha <- validate_numeric_range(alpha, "alpha", 0, 1)
padding <- validate_numeric_range(padding, "padding", 0, 1)
}
Validates:
- ✅ Is numeric type
- ✅ Has length 1 (not a vector)
- ✅ Is not NA
- ✅ Falls within [min, max] range (inclusive)
- ✅ Optionally allows NULL if
allow_null = TRUE
Error examples:
validate_numeric_range(1.5, "size", 0, 1)
# Error: size must be between 0 and 1 (inclusive)
validate_numeric_range(NULL, "alpha", 0, 1)
# Error: alpha cannot be NULL
validate_numeric_range(NULL, "padding", 0, 1, allow_null = TRUE)
# Returns NULL (valid when allow_null = TRUE)
4. validate_choice(value, name, choices, allow_null = FALSE)
Purpose: Validate value is from a set of allowed options
Usage:
add_bfh_logo <- function(plot, logo_path, position = "bottomright", ...) {
# Validate position is one of 4 corners
position <- validate_choice(
position,
"position",
c("topleft", "topright", "bottomleft", "bottomright")
)
# Now safe to use in switch() or if/else
coords <- switch(position, ...)
}
Validates:
- ✅ Is character type
- ✅ Has length 1 (not a vector)
- ✅ Is not NA
- ✅ Matches one of the allowed choices
- ✅ Optionally allows NULL if
allow_null = TRUE
Error example:
validate_choice("center", "position", c("topleft", "topright", "bottomleft", "bottomright"))
# Error: position must be one of: "topleft", "topright", "bottomleft", "bottomright"
Error Handling Patterns
Use call. = FALSE for User-Facing Errors
Always use call. = FALSE in stop() for errors triggered by user input. This hides internal function calls from error messages.
# ✅ Good - Clean error message
if (!file.exists(path)) {
stop("Logo file not found: ", basename(path), call. = FALSE)
}
# Error: Logo file not found: logo.png
# ❌ Bad - Exposes internal call stack
if (!file.exists(path)) {
stop("Logo file not found: ", basename(path))
}
# Error in add_bfh_logo(...): Logo file not found: logo.png
Write Clear, Actionable Error Messages
Error messages should:
- State the problem clearly
- Include the parameter name when relevant
- Suggest valid alternatives when possible
- Avoid technical jargon unless necessary
# ✅ Good - Clear and actionable
if (!(position %in% valid_positions)) {
stop(
sprintf("position must be one of: %s", paste(dQuote(valid_positions), collapse = ", ")),
call. = FALSE
)
}
# Error: position must be one of: "topleft", "topright", "bottomleft", "bottomright"
# ❌ Bad - Vague and unhelpful
if (!(position %in% valid_positions)) {
stop("Invalid position")
}
# Error: Invalid position
Validation Order
Validate parameters in this order:
- Type checks (is it a character, numeric, logical?)
- Structural checks (is length correct?)
- Content checks (is value in allowed range/set?)
- External checks (does file exist? is package available?)
your_function <- function(palette = "main", size = 0.1, position = "topright") {
# 1. Type & structure (via validation helpers)
palette <- validate_palette_argument(palette)
size <- validate_numeric_range(size, "size", 0, 1)
position <- validate_choice(position, "position", c("topleft", "topright", "bottomleft", "bottomright"))
# 2. Content validation
if (!(palette %in% names(bfh_palettes))) {
stop("Unknown palette: ", palette, call. = FALSE)
}
# 3. External dependencies
if (!requireNamespace("ggplot2", quietly = TRUE)) {
stop("Package 'ggplot2' required but not installed", call. = FALSE)
}
# ... now safe to proceed with function logic
}
Complete Example: Function with Proper Validation
Here's a complete example showing all validation patterns:
#' Add Custom Branding Element
#'
#' @param plot ggplot object
#' @param element_type Type of element ("logo", "watermark", "footer")
#' @param position Position on plot
#' @param size Size as proportion of plot (0-1)
#' @param text Text to display (optional)
#' @param alpha Transparency (0-1)
#' @export
add_branding_element <- function(plot,
element_type,
position = "bottomright",
size = 0.1,
text = NULL,
alpha = 1) {
# === Input Validation (fail-fast at function entry) ===
# 1. Required parameters
if (missing(plot) || !inherits(plot, "ggplot")) {
stop("plot must be a ggplot object", call. = FALSE)
}
if (missing(element_type)) {
stop("element_type is required", call. = FALSE)
}
# 2. Use validation helpers for standard types
element_type <- validate_choice(
element_type,
"element_type",
c("logo", "watermark", "footer")
)
position <- validate_choice(
position,
"position",
c("topleft", "topright", "bottomleft", "bottomright")
)
size <- validate_numeric_range(size, "size", 0.001, 1)
alpha <- validate_numeric_range(alpha, "alpha", 0, 1)
# 3. Optional parameter with NULL coalescing
text <- text %||% "Default Branding Text"
# 4. Conditional validation (only if text provided)
if (!is.character(text) || length(text) != 1) {
stop("text must be a single character string", call. = FALSE)
}
# === Function Logic (inputs now validated) ===
# Safe to proceed - all inputs validated
result <- switch(
element_type,
logo = add_logo_internal(plot, position, size, alpha),
watermark = add_watermark_internal(plot, text, alpha),
footer = add_footer_internal(plot, text, position)
)
return(result)
}
Checklist: Adding Validation to Functions
When writing or reviewing a function, ensure:
- All parameters validated at function entry (fail-fast)
- Use validation helpers for standard types (palette, logical, numeric, choice)
- Use
%||%for NULL defaults instead of verboseifchecks - Use
call. = FALSEin all user-facingstop()calls - Error messages are clear and include parameter names
- Validation order: type → structure → content → external
- Required parameters checked before optional ones
- Custom validation only when helpers don't fit
Caching Strategy
BFHtheme uses session-level caching to optimize performance by avoiding expensive operations like font detection and palette interpolation. Understanding the caching strategy is essential when:
- Adding new cached operations
- Modifying cached functions
- Debugging caching issues
- Writing tests that depend on cache state
Why Caching Exists
Performance Benefits
Font Detection (10-15x speedup):
- Without caching: Each
theme_bfh()call queries the system for available fonts - With caching: Font detected once per R session, reused for all subsequent calls
- Impact: Creating 100 plots goes from ~30 seconds to ~2 seconds
Palette Interpolation:
- Without caching: Each
scale_color_bfh()creates a newcolorRampPalette()function - With caching: Palette function created once per palette+reverse combination
- Impact: Faster plot rendering, especially with many categorical values
User Experience
Caching makes BFHtheme feel instant and responsive rather than sluggish, especially when:
- Creating multiple plots in a session
- Using RMarkdown/Quarto with many figures
- Interactive exploration with Shiny apps
- Building reports with consistent theming
Font Caching
Location: R/fonts.R
Cache environment: .bfh_font_cache
Primary function: get_bfh_font()
How Font Caching Works
# Package-level cache environment (created when package loads)
.bfh_font_cache <- new.env(parent = emptyenv())
get_bfh_font <- function(check_installed = TRUE, silent = FALSE, force_refresh = FALSE) {
fonts <- c("Mari", "Mari Office", "Roboto", "Arial", "sans")
if (!check_installed) {
return(fonts[1]) # No caching needed
}
# Cache key (simple string)
cache_key <- "selected_font"
# Check cache first
if (!force_refresh && exists(cache_key, envir = .bfh_font_cache, inherits = FALSE)) {
cached_font <- get(cache_key, envir = .bfh_font_cache, inherits = FALSE)
if (!silent) message("Using cached font: ", cached_font)
return(cached_font)
}
# Expensive operation: Detect available fonts
selected_font <- detect_best_font(fonts) # Simplified
# Store in cache for future calls
assign(cache_key, selected_font, envir = .bfh_font_cache)
return(selected_font)
}
Cache Key Structure
Current implementation:
- Simple string:
"selected_font" - No parameters in key: Assumes font availability doesn't change during session
Future consideration (if needed):
# If you need to cache different results based on parameters:
cache_key <- paste0("font_", paste(fonts, collapse = "_"), "_", check_installed)
When to Clear Font Cache
Call clear_bfh_font_cache() after:
- ✅ Installing new fonts (Mari, Roboto, etc.)
- ✅ Uninstalling fonts
- ✅ Modifying system font configuration
- ✅ Testing font detection logic
# Example: After installing Roboto
install.packages("showtext")
library(showtext)
font_add_google("Roboto", "Roboto")
# Clear cache so next call detects Roboto
clear_bfh_font_cache()
# Now Roboto will be detected
get_bfh_font()
Bypassing Cache (Force Refresh)
Use force_refresh = TRUE for one-time cache bypass:
# Bypass cache without clearing it
font <- get_bfh_font(force_refresh = TRUE)
Palette Caching
Location: R/colors.R
Cache environment: .bfh_pal_cache
Primary function: bfh_pal()
How Palette Caching Works
# Package-level cache environment
.bfh_pal_cache <- new.env(parent = emptyenv())
bfh_pal <- function(palette = "main", reverse = FALSE, ...) {
# Validation
palette <- validate_palette_argument(palette)
reverse <- validate_logical_argument(reverse, "reverse")
# Cache key combines palette name and reverse flag
cache_key <- paste0(palette, "_", reverse)
# Check cache first
if (exists(cache_key, envir = .bfh_pal_cache, inherits = FALSE)) {
return(get(cache_key, envir = .bfh_pal_cache, inherits = FALSE))
}
# Create palette function (expensive)
pal <- bfh_palettes[[palette]]
if (reverse) pal <- rev(pal)
pal_fn <- grDevices::colorRampPalette(pal, ...)
# Store in cache
assign(cache_key, pal_fn, envir = .bfh_pal_cache)
return(pal_fn)
}
Cache Key Structure
Current implementation:
cache_key <- paste0(palette, "_", reverse)
# Examples:
# "main_FALSE" → bfh_pal("main")
# "main_TRUE" → bfh_pal("main", reverse = TRUE)
# "blues_FALSE" → bfh_pal("blues")
# "hospital_blues_TRUE" → bfh_pal("hospital_blues", reverse = TRUE)
Why include reverse in key:
- Different
reversevalues produce different palette functions - Must cache separately to return correct results
⚠️ Known limitation:
- Additional arguments via
...are NOT included in cache key - If you call
bfh_pal("main", alpha = TRUE)and thenbfh_pal("main"), the second call returns the cached version with alpha (potentially unexpected)
Solution if ... args become important:
# Future-proof cache key if needed
cache_key <- paste0(
palette, "_",
reverse, "_",
digest::digest(list(...)) # Hash of additional arguments
)
When to Clear Palette Cache
Call clear_bfh_pal_cache() after:
- ✅ Modifying
bfh_paletteslist (during development) - ✅ Changing palette definitions
- ✅ Testing palette generation logic
- ❌ Not needed during normal package use
# Example: After modifying palettes during development
bfh_palettes$new_palette <- c("#007dbb", "#009ce8", "#cce5f1")
# Clear cache so next call uses new palette
clear_bfh_pal_cache()
# Now new_palette is available
pal <- bfh_pal("new_palette")
Extending the Caching System
When adding new cached operations, follow these patterns:
1. Create Package-Level Cache Environment
# At top of R/your_file.R
.your_cache <- new.env(parent = emptyenv())
2. Design Cache Key
Principles:
- Include all parameters that affect the result
- Use simple string concatenation for readability
- Use consistent separators (underscore
_is standard) - Consider hash functions for complex objects
Examples:
# Simple: Single parameter
cache_key <- palette_name
# Moderate: Multiple parameters
cache_key <- paste0(theme, "_", size, "_", variant)
# Complex: Hash of list
cache_key <- digest::digest(list(theme, options, data))
3. Implement Cache Check Pattern
your_function <- function(param1, param2, force_refresh = FALSE) {
# Build cache key
cache_key <- paste0(param1, "_", param2)
# Check cache (unless force_refresh)
if (!force_refresh && exists(cache_key, envir = .your_cache, inherits = FALSE)) {
return(get(cache_key, envir = .your_cache, inherits = FALSE))
}
# Expensive operation
result <- expensive_computation(param1, param2)
# Store in cache
assign(cache_key, result, envir = .your_cache)
return(result)
}
4. Provide Cache Clear Function
#' Clear Your Feature Cache
#'
#' @return Invisibly returns TRUE
#' @export
clear_your_cache <- function() {
rm(list = ls(envir = .your_cache), envir = .your_cache)
message("Your cache cleared")
invisible(TRUE)
}
5. Document Caching Behavior
In roxygen documentation:
#' @details
#' Results are cached in the package environment; use [clear_your_cache()]
#' or set `force_refresh = TRUE` after modifying underlying data.
Cache Lifecycle
Cache creation:
- Environments created when package loads (
.onLoad()) - Empty at package load time
Cache population:
- Happens lazily on first function call
- Each unique cache key populated independently
Cache persistence:
- Lasts for entire R session
- Cleared when R session ends
- Not saved between sessions
Cache invalidation:
- Manual: Call
clear_*_cache()functions - Force refresh: Use
force_refresh = TRUEparameter - Restart R session: Clears all caches
Testing with Caches
Clear Caches in Test Setup
test_that("function works with clean cache", {
# Clear cache before test
clear_bfh_font_cache()
clear_bfh_pal_cache()
# Now test with fresh state
result <- get_bfh_font()
expect_type(result, "character")
})
Test Cache Behavior
test_that("font cache speeds up subsequent calls", {
clear_bfh_font_cache()
# First call populates cache
time1 <- system.time(font1 <- get_bfh_font(silent = TRUE))
# Second call uses cache (should be faster)
time2 <- system.time(font2 <- get_bfh_font(silent = TRUE))
expect_identical(font1, font2)
expect_lt(time2["elapsed"], time1["elapsed"])
})
Test Force Refresh
test_that("force_refresh bypasses cache", {
clear_bfh_font_cache()
# Populate cache
font1 <- get_bfh_font(silent = TRUE)
# Force refresh should re-detect
font2 <- get_bfh_font(force_refresh = TRUE, silent = TRUE)
expect_identical(font1, font2)
})
Performance Considerations
When to Add Caching
Good candidates:
- ✅ File system operations (font detection, logo path resolution)
- ✅ Complex computations (palette interpolation, color conversions)
- ✅ External system queries (font availability, package checks)
- ✅ Operations called repeatedly with same arguments
Poor candidates:
- ❌ Fast operations (< 1ms)
- ❌ Operations with unique arguments each time
- ❌ Operations with side effects
- ❌ Operations requiring fresh data
Cache Size Management
Current caches are small:
- Font cache: 1 entry (`"selected_font"$)
- \text{Palette} \text{cache}: ~20-30 \text{entries} (\text{palette} \times \text{reverse} \text{combinations})
- \text{Memory} \text{impact}: \text{Negligible} (< 1 \text{KB} \text{total})
\text{If} \text{cache} \text{grows} \text{large}: $``r
Add size limit to cache function
if (length(ls(envir = .your_cache)) > 100) {
Clear oldest entries or implement LRU
warning("Cache size exceeded limit, clearing cache") clear_your_cache() }
---
### Checklist: Adding New Cached Function
When implementing a new cached operation:
- [ ] Create package-level cache environment (`.your_cache`)
- [ ] Design cache key that includes **all** relevant parameters
- [ ] Implement cache check with `exists()` and `get()`
- [ ] Add `force_refresh` parameter for cache bypass
- [ ] Store result with `assign()`
- [ ] Create `clear_your_cache()` function
- [ ] Export clear function with `@export`
- [ ] Document caching behavior in roxygen `@details`
- [ ] Write tests for cache hit/miss scenarios
- [ ] Add cache clear to test setup/teardown
- [ ] Update this documentation with new cache details
---
### Available Cache Functions
**Font caching:**
```r
clear_bfh_font_cache() # Defined in R/fonts.R
Palette caching:
clear_bfh_pal_cache() # Defined in R/colors.R
Clear all caches:
# Helper function (add if useful)
clear_all_bfh_caches <- function() {
clear_bfh_font_cache()
clear_bfh_pal_cache()
invisible(TRUE)
}
Development Workflow
BFHtheme follows Test-Driven Development (TDD) principles:
1. Create a Feature Branch
git checkout -b feature/your-feature-name
# or
git checkout -b fix/bug-description
Branch naming conventions:
feature/- New featuresfix/- Bug fixesrefactor/- Code refactoringdocs/- Documentation changestest/- Test additions or modifications
2. Write Tests First
Before implementing any feature or fix:
# Create or modify test file in tests/testthat/
# Example: tests/testthat/test-your-feature.R
test_that("your feature works correctly", {
result <- your_function(input)
expect_equal(result, expected_output)
})
3. Implement the Feature
Write minimal code to make tests pass:
# In R/your-file.R
your_function <- function(input) {
# Implementation
}
4. Run Tests Continuously
# Run all tests
devtools::test()
# Run specific test file
testthat::test_file("tests/testthat/test-your-feature.R")
# Check entire package
devtools::check()
5. Document Your Code
Update roxygen comments and regenerate documentation:
devtools::document()
6. Code Quality Checks
# Format code
styler::style_pkg()
# Lint code
lintr::lint_package()
# Check code coverage
covr::package_coverage()
Code Style Guidelines
Naming Conventions
- Functions:
snake_case - Arguments:
snake_case - Objects/Variables:
snake_case - Internal functions: Prefix with
.(e.g.,.resolve_base_family)
# ✅ Good
theme_bfh <- function(base_size = 12, base_family = NULL) { }
# ❌ Bad
themeBFH <- function(baseSize = 12, baseFamily = NULL) { }
Comments
- Danish comments for internal documentation
- English for exported function documentation (roxygen2)
# Danske kommentarer for intern logik
.resolve_base_family <- function(base_family) {
base_family %||% get_bfh_font(check_installed = TRUE, silent = TRUE)
}
#' BFH Theme for ggplot2
#'
#' English documentation for exported functions
#' @export
theme_bfh <- function() { }
Defensive Programming
Always validate inputs for exported functions:
theme_bfh <- function(base_size = 12, base_family = NULL, ...) {
# Input validation
if (!is.numeric(base_size) || base_size <= 0) {
stop("base_size must be a positive number", call. = FALSE)
}
# NULL coalescing with %||% operator
base_family <- base_family %||% get_bfh_font(check_installed = TRUE, silent = TRUE)
# ... implementation
}
NULL Handling
Use the %||% operator (defined in utils_operators.R):
# ✅ Good - use %||% for defaults
base_family <- base_family %||% get_bfh_font()
text <- text %||% "Default text"
# ❌ Bad - missing fallback
base_family <- base_family
ggplot2 Patterns
# ✅ Good - Return ggplot objects for composition
create_plot <- function(data) {
ggplot(data, aes(x = x, y = y)) +
geom_line() +
theme_bfh()
}
# ❌ Bad - Don't print inside functions
create_plot <- function(data) {
plot <- ggplot(data, aes(x = x, y = y)) + geom_line()
print(plot) # DON'T DO THIS
}
Testing
Test Structure
Tests are organized in tests/testthat/ by module:
test-themes.R- Theme functionstest-colors.R- Color palettestest-scales.R- Scale functionstest-fonts.R- Font detectiontest-helpers.R- Plot helperstest-branding.R- Logo and brandingtest-defaults.R- Global defaults
Writing Tests
test_that("function handles valid input correctly", {
# Arrange
input <- valid_data
# Act
result <- your_function(input)
# Assert
expect_type(result, "character")
expect_length(result, 5)
})
test_that("function validates input parameters", {
# Act & Assert
expect_error(
your_function(invalid_input),
"Expected error message"
)
})
Running Tests
# All tests
devtools::test()
# Specific test file
testthat::test_file("tests/testthat/test-colors.R")
# With coverage report
covr::package_coverage()
Coverage Goals
- ≥90% overall coverage (current: 89.74%)
- 100% on exported functions
- Edge cases: NULL inputs, empty data, invalid types
- Security tests: Path traversal, input validation
Visual Regression Tests
BFHtheme uses vdiffr for visual regression testing of rendered plots.
Test snapshots live in tests/testthat/_snaps/visual-regression/.
Running visual regression tests:
# Run the full suite — snapshot diffs appear as test failures
devtools::test()
When a snapshot diff appears:
A diff means the rendered output changed. Before accepting, inspect the diff to confirm the change is intentional (e.g. a theme tweak) and not a regression:
# Open an interactive diff viewer for all failing snapshots
vdiffr::manage_cases()
# Or review a single case by name
vdiffr::manage_cases(filter = "theme_bfh_basic")
The viewer shows the expected image, the new image, and a diff overlay.
Accepting a change:
If the diff is intentional, accept it in manage_cases(). This rewrites the
reference SVG on disk. Stage and commit the updated snapshots together with
the code change that caused them:
git add tests/testthat/_snaps/
git commit -m "test(visual): accept updated snapshots for <describe change>"
Rejecting a change:
If the diff reveals an unintended regression, close manage_cases() without
accepting, fix the code, and re-run the tests.
Adding a new snapshot test:
test_that("theme_bfh produces expected output", {
skip_if_not_installed("vdiffr")
skip_if_not_installed("ggplot2")
p <- ggplot2::ggplot(mtcars, ggplot2::aes(wt, mpg)) +
ggplot2::geom_point() +
theme_bfh()
vdiffr::expect_doppelganger("theme_bfh_basic", p)
})
On first run the snapshot is created automatically. Subsequent runs compare against the stored reference.
CI behaviour:
Snapshot tests run in CI. New snapshots (no reference yet) are skipped; diffs
fail the build. Keep snapshots up to date by running vdiffr::manage_cases()
locally and committing the result before pushing.
Test Best Practices
- Test one thing per test
- Use descriptive test names
- Include both positive and negative test cases
- Test edge cases and error conditions
- Use
skip_if_not_installed()for optional dependencies
test_that("add_bfh_logo requires ggplot2", {
skip_if_not_installed("ggplot2")
p <- ggplot(mtcars, aes(wt, mpg)) + geom_point()
result <- add_bfh_logo(p, get_bfh_logo())
expect_s3_class(result, "ggplot")
})
Documentation
Roxygen2 Format
All exported functions must have complete roxygen documentation:
#' Short Title (One Line)
#'
#' @description
#' Detailed description of what the function does, when to use it,
#' and any important details about behavior.
#'
#' @param param_name Description of parameter, including type and valid values
#' @param another_param Another parameter description
#'
#' @return Description of what is returned, including type
#'
#' @export
#'
#' @examples
#' # Example with comments
#' result <- my_function(x = 1:10)
#'
#' # More complex example
#' result2 <- my_function(x = data, param = "value")
#'
#' \dontrun{
#' # Example requiring external data
#' my_function(data = external_data)
#' }
my_function <- function(param_name, another_param = NULL) {
# Implementation
}
Required Roxygen Tags
@description- Detailed function description@param- For each parameter@return- What the function returns@export- For exported functions@examples- Working examples@importFrom- For imported functions from other packages
Update Documentation
After modifying roxygen comments:
# Regenerate documentation and NAMESPACE
devtools::document()
# Verify documentation builds without errors
devtools::check_man()
Vignettes
For major features, consider adding a vignette:
# Create new vignette
usethis::use_vignette("your-feature-name")
Commit Guidelines
Commit Message Format
type(scope): short action-oriented description
Optional longer description providing context and rationale.
- Bullet points for multiple changes
- Breaking changes marked explicitly
Commit Types
feat- New featurefix- Bug fixrefactor- Code refactoring (no behavior change)test- Adding or modifying testsdocs- Documentation changeschore- Maintenance tasksperf- Performance improvements
Examples
# Good commit messages
git commit -m "feat(themes): add custom theme styling option"
git commit -m "fix(colors): resolve bfh_cols() zero-argument bug"
git commit -m "test(fonts): add coverage for extrafont fallback path"
git commit -m "docs(readme): update installation instructions"
# Bad commit messages
git commit -m "update code"
git commit -m "fix bug"
git commit -m "changes"
Important Commit Rules
DO NOT include:
- ❌ "🤖 Generated with [Claude Code]"
- ❌ "Co-Authored-By: Claude noreply@anthropic.com"
- ❌ Other AI attribution footers
These should only be used when explicitly requested by maintainers.
Pull Request Process
Before Submitting
Complete this checklist:
- All tests pass (
devtools::test()) - Package check passes (
devtools::check()) - Code is formatted (
styler::style_pkg()) - Code is linted (
lintr::lint_package()) - Documentation is updated (
devtools::document()) - Examples are verified and working
- NAMESPACE is regenerated (via
devtools::document()) - Coverage is maintained or improved
Creating a Pull Request
- Branch from
develop:
git checkout develop
git pull origin develop
git checkout -b feature/your-feature-name
- Push your branch:
git push origin feature/your-feature-name
-
Create PR on GitHub:
- Go to https://github.com/johanreventlow/BFHtheme
- Click "New Pull Request"
- Select your branch
- Use
developas the base branch for feature and fix work - Fill in the PR template
-
Promotion to
main:- Do not merge feature branches directly to
main. - When
developCI is green, GitHub Actions opens adevelop→mainPR automatically. - Review and merge that promotion PR manually.
- Do not merge feature branches directly to
-
PR Description Template:
## Summary
Brief description of changes
## Motivation
Why this change is needed
## Changes
- List of specific changes
- Include breaking changes if any
## Testing
How the changes were tested
## Checklist
- [ ] Tests pass
- [ ] Documentation updated
- [ ] Code formatted and linted
PR Review Process
- Maintainers will review your PR
- Address any feedback or requested changes
- Tests must pass before merging
- At least one maintainer approval required
Review Checklist
When reviewing code (or self-reviewing before submission):
Functionality
- Code does what it's supposed to do
- Edge cases are handled
- Error messages are informative
- No breaking changes to existing API (unless discussed)
Code Quality
- Follows snake_case naming convention
- Input validation for exported functions
- NULL safety using
%||%operator - No hardcoded values (use constants/parameters)
- DRY principle followed (no unnecessary duplication)
Testing
- Tests are comprehensive
- Tests cover edge cases
- Tests include error conditions
- Coverage maintained or improved (≥90%)
Documentation
- Roxygen documentation complete
- Examples are working and useful
- README updated if needed
- Vignettes updated if applicable
Performance
- No obvious performance issues
- Efficient algorithms used
- Caching used where appropriate
Security
- Input validation prevents path traversal
- No hardcoded credentials or sensitive data
- File operations use safe paths
Additional Resources
Package Development
BFHtheme Specific
- CLAUDE.md - Detailed development instructions
- README.md - Package overview and usage
- FONTS.md - Font system documentation
- TROUBLESHOOTING.md - Common issues
Getting Help
- Issues: Report bugs or request features via GitHub Issues
- Discussions: Ask questions in GitHub Discussions
- Email: Contact the maintainers directly
Code of Conduct
Our Standards
- Be respectful and inclusive
- Focus on what's best for the project and community
- Accept constructive criticism gracefully
- Show empathy towards other contributors
Our Responsibilities
Maintainers are responsible for clarifying standards of acceptable behavior and will take appropriate action in response to any unacceptable behavior.
License
By contributing to BFHtheme, you agree that your contributions will be licensed under the MIT License.
Thank you for contributing to BFHtheme! Your efforts help make healthcare data visualization more accessible and professional.