diff --git a/.gitignore b/.gitignore
index 8eb30ad5..fd43895c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,4 @@ _quarto/*
!_quarto/_freeze/
*.docx
*.knit.md
+tests/testthat/_snaps/
diff --git a/DESCRIPTION b/DESCRIPTION
index 53cbb655..863df5c3 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -15,9 +15,11 @@ Language: en-US
Depends:
R (>= 4.1.0)
Imports:
+ cli,
stats
Suggests:
altdoc,
+ foodwebr,
knitr,
rmarkdown,
spelling,
diff --git a/NAMESPACE b/NAMESPACE
index 79e14c4f..93b6af16 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -1,3 +1,4 @@
# Generated by roxygen2: do not edit by hand
+export(calculate_summary)
export(example_function)
diff --git a/NEWS.md b/NEWS.md
index db48a7fa..17a14a40 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,9 @@
# rpt (development version)
+* Added package structure visualization article using foodwebr to demonstrate function dependencies and call graphs
+* Created layered helper function architecture for realistic foodwebr demonstrations
+* Added `calculate_summary()` function for computing summary statistics
+
* Switched from pkgdown to altdoc for documentation generation. Now using Quarto Website for documentation with native math equation support via MathJax.
* Removed pkgdown-specific configurations and workflows.
* Retained RevealJS multi-format support for Quarto vignettes and articles.
diff --git a/R/calculate_statistic.R b/R/calculate_statistic.R
new file mode 100644
index 00000000..d882d062
--- /dev/null
+++ b/R/calculate_statistic.R
@@ -0,0 +1,13 @@
+#' Calculate Statistic
+#'
+#' Internal helper function to calculate a statistical measure on cleaned data.
+#'
+#' @param x A numeric vector
+#'
+#' @return The median value
+#' @keywords internal
+#' @noRd
+calculate_statistic <- function(x) {
+ x_clean <- clean_data(x)
+ stats::median(x_clean)
+}
diff --git a/R/calculate_summary.R b/R/calculate_summary.R
new file mode 100644
index 00000000..ab9357be
--- /dev/null
+++ b/R/calculate_summary.R
@@ -0,0 +1,21 @@
+#' Calculate Summary Statistics
+#'
+#' Calculate multiple summary statistics for a numeric vector.
+#'
+#' @param x A numeric vector
+#'
+#' @return A named list with mean, median, and standard deviation
+#' @export
+#'
+#' @examples
+#' calculate_summary(c(1, 2, 3, 4, 5))
+#' calculate_summary(c(1, NA, 3, 4, 5))
+calculate_summary <- function(x) {
+ x_clean <- clean_data(x)
+
+ list(
+ mean = format_result(mean(x_clean)),
+ median = example_function(x),
+ sd = format_result(stats::sd(x_clean))
+ )
+}
diff --git a/R/clean_data.R b/R/clean_data.R
new file mode 100644
index 00000000..7bd4d3fc
--- /dev/null
+++ b/R/clean_data.R
@@ -0,0 +1,14 @@
+#' Clean Data
+#'
+#' Internal helper function to clean data by removing NA values and preparing
+#' for analysis.
+#'
+#' @param x A numeric vector
+#'
+#' @return A cleaned numeric vector with NA values removed
+#' @keywords internal
+#' @noRd
+clean_data <- function(x) {
+ validate_input(x)
+ x[!is.na(x)]
+}
diff --git a/R/example_function.R b/R/example_function.R
index 962aade8..5d95050f 100644
--- a/R/example_function.R
+++ b/R/example_function.R
@@ -1,14 +1,17 @@
#' Example Function
#'
#' This is an example function that demonstrates basic functionality.
+#' It validates, cleans, calculates statistics, and formats the result.
#'
#' @param x A numeric vector
#'
-#' @return The median of the input vector
+#' @return The median of the input vector, rounded to 2 decimal places
#' @export
#'
#' @examples
#' example_function(c(1, 2, 3, 4, 5))
+#' example_function(c(1, NA, 3, 4, 5))
example_function <- function(x) {
- stats::median(x)
+ result <- calculate_statistic(x)
+ format_result(result)
}
diff --git a/R/format_result.R b/R/format_result.R
new file mode 100644
index 00000000..95c246ec
--- /dev/null
+++ b/R/format_result.R
@@ -0,0 +1,12 @@
+#' Format Result
+#'
+#' Internal helper function to format the result for output.
+#'
+#' @param result A numeric value
+#'
+#' @return A formatted numeric value (rounded to 2 decimal places)
+#' @keywords internal
+#' @noRd
+format_result <- function(result) {
+ round(result, digits = 2)
+}
diff --git a/R/validate_input.R b/R/validate_input.R
new file mode 100644
index 00000000..d02e2e6f
--- /dev/null
+++ b/R/validate_input.R
@@ -0,0 +1,21 @@
+#' Validate Input Data
+#'
+#' Internal helper function to validate input data for statistical calculations.
+#'
+#' @param x A numeric vector
+#'
+#' @return TRUE if valid, throws error otherwise
+#' @keywords internal
+#' @noRd
+validate_input <- function(x) {
+ if (!is.numeric(x)) {
+ cli::cli_abort("Input must be numeric", call = NULL)
+ }
+ if (length(x) == 0) {
+ cli::cli_abort("Input must have at least one element", call = NULL)
+ }
+ if (all(is.na(x))) {
+ cli::cli_abort("Input cannot be all NA values", call = NULL)
+ }
+ TRUE
+}
diff --git a/altdoc/quarto_website.yml b/altdoc/quarto_website.yml
index 3a4362cf..1ef327ee 100644
--- a/altdoc/quarto_website.yml
+++ b/altdoc/quarto_website.yml
@@ -34,6 +34,8 @@ website:
file: vignettes/quarto_vignette.qmd
- text: "Advanced"
file: vignettes/articles/quarto_article.qmd
+ - text: "Package Structure"
+ file: vignettes/articles/package-structure.qmd
sidebar:
collapse-level: 1
contents:
@@ -48,6 +50,7 @@ website:
- section: Advanced
contents:
- vignettes/articles/quarto_article.qmd
+ - vignettes/articles/package-structure.qmd
- section: $ALTDOC_MAN_BLOCK
style: floating
- text: News
diff --git a/inst/WORDLIST b/inst/WORDLIST
index 9bab8ede..5465c792 100644
--- a/inst/WORDLIST
+++ b/inst/WORDLIST
@@ -2,9 +2,11 @@ CMD
Callouts
CodeFactor
Codecov
+DiagrammeR
DOCX
Lifecycle
MathJax
+README
RevealJS
Shortcode
Slidebreak
@@ -14,9 +16,14 @@ altdoc
callout
callouts
cdot
+codebases
emplate
foldable
+foodweb
+foodwebr
+formatter
frac
+graphviz
frontmatter
linter
lintr
@@ -26,3 +33,4 @@ serodynamics
shortcode
slidebreak
tabset
+tidygraph
diff --git a/man/calculate_summary.Rd b/man/calculate_summary.Rd
new file mode 100644
index 00000000..2dd13384
--- /dev/null
+++ b/man/calculate_summary.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/calculate_summary.R
+\name{calculate_summary}
+\alias{calculate_summary}
+\title{Calculate Summary Statistics}
+\usage{
+calculate_summary(x)
+}
+\arguments{
+\item{x}{A numeric vector}
+}
+\value{
+A named list with mean, median, and standard deviation
+}
+\description{
+Calculate multiple summary statistics for a numeric vector.
+}
+\examples{
+calculate_summary(c(1, 2, 3, 4, 5))
+calculate_summary(c(1, NA, 3, 4, 5))
+}
diff --git a/man/example_function.Rd b/man/example_function.Rd
index 497dea06..74d7e12e 100644
--- a/man/example_function.Rd
+++ b/man/example_function.Rd
@@ -10,11 +10,13 @@ example_function(x)
\item{x}{A numeric vector}
}
\value{
-The median of the input vector
+The median of the input vector, rounded to 2 decimal places
}
\description{
This is an example function that demonstrates basic functionality.
+It validates, cleans, calculates statistics, and formats the result.
}
\examples{
example_function(c(1, 2, 3, 4, 5))
+example_function(c(1, NA, 3, 4, 5))
}
diff --git a/tests/testthat/test-calculate_summary.R b/tests/testthat/test-calculate_summary.R
new file mode 100644
index 00000000..325ba056
--- /dev/null
+++ b/tests/testthat/test-calculate_summary.R
@@ -0,0 +1,27 @@
+test_that("calculate_summary works", {
+ result <- calculate_summary(c(1, 2, 3, 4, 5))
+ expect_type(result, "list")
+ expect_named(result, c("mean", "median", "sd"))
+ expect_equal(result$mean, 3)
+ expect_equal(result$median, 3)
+ expect_equal(result$sd, 1.58)
+})
+
+test_that("calculate_summary handles NA values", {
+ result <- calculate_summary(c(1, NA, 3, 4, 5))
+ expect_type(result, "list")
+ expect_equal(result$median, 3.5)
+ expect_equal(result$mean, 3.25)
+})
+
+test_that("calculate_summary handles errors", {
+ expect_error(calculate_summary(character()), "Input must be numeric")
+ expect_error(
+ calculate_summary(numeric()),
+ "Input must have at least one element"
+ )
+ expect_error(
+ calculate_summary(c(NA_real_, NA_real_, NA_real_)),
+ "Input cannot be all NA values"
+ )
+})
diff --git a/tests/testthat/test-example_function.R b/tests/testthat/test-example_function.R
index f86a3be1..d7d98a3b 100644
--- a/tests/testthat/test-example_function.R
+++ b/tests/testthat/test-example_function.R
@@ -1,4 +1,18 @@
test_that("example_function works", {
expect_equal(example_function(c(1, 2, 3)), 2)
expect_equal(example_function(c(1, 2, 3, 4, 5)), 3)
+ expect_equal(example_function(c(1, NA, 3, 4, 5)), 3.5)
+ expect_equal(example_function(c(1.111, 2.222, 3.333)), 2.22)
+})
+
+test_that("example_function handles errors", {
+ expect_error(example_function(character()), "Input must be numeric")
+ expect_error(
+ example_function(numeric()),
+ "Input must have at least one element"
+ )
+ expect_error(
+ example_function(c(NA_real_, NA_real_, NA_real_)),
+ "Input cannot be all NA values"
+ )
})
diff --git a/vignettes/articles/package-structure.qmd b/vignettes/articles/package-structure.qmd
new file mode 100644
index 00000000..1e102155
--- /dev/null
+++ b/vignettes/articles/package-structure.qmd
@@ -0,0 +1,342 @@
+---
+title: "Package Function Structure"
+author: "UC Davis Seroepidemiology Research Group (UCD-SERG)"
+date: "`r Sys.Date()`"
+format:
+ revealjs:
+ output-file: package-structure-revealjs.html
+ html: default
+---
+
+```{r}
+#| include: false
+knitr::opts_chunk$set(
+ collapse = TRUE,
+ comment = "#>",
+ fig.width = 7,
+ fig.height = 5
+)
+# Check if we're rendering to a format that supports HTML widgets
+is_html_output <- knitr::is_html_output()
+```
+
+```{r setup}
+#| message: false
+library(rpt)
+```
+
+## Overview
+
+This article demonstrates how to visualize the structure and dependencies of package functions using the [`foodwebr`](https://lewinfox.com/foodwebr/) package. Understanding function dependencies helps developers:
+
+- Navigate and understand codebases more quickly
+- Identify potential refactoring opportunities
+- Document architecture and design decisions
+- Onboard new contributors more effectively
+
+## What is foodwebr?
+
+The `foodwebr` package creates dependency graphs showing which functions call which other functions. These visualizations are particularly useful for:
+
+- Exploring unfamiliar codebases
+- Understanding function relationships
+- Identifying central or isolated functions
+- Planning refactoring efforts
+
+## Installing foodwebr
+
+If you don't have `foodwebr` installed, you can install it from CRAN:
+
+```{r}
+#| eval: false
+install.packages("foodwebr")
+```
+
+Or from GitHub for the latest development version:
+
+```{r}
+#| eval: false
+devtools::install_github("lewinfox/foodwebr")
+```
+
+## Visualizing Package Structure
+
+### Basic Usage
+
+The `foodweb()` function analyzes function dependencies. When examining a package, you typically want to look at specific functions or the entire package namespace:
+
+```{r}
+#| message: false
+#| eval: !expr requireNamespace("foodwebr", quietly = TRUE)
+library(foodwebr)
+
+# Create a foodweb for the example_function
+fw <- foodweb(FUN = example_function)
+fw
+```
+
+### Plotting the Dependency Graph
+
+The `plot()` method creates an interactive visualization using DiagrammeR:
+
+```{r}
+#| label: fig-basic-foodweb
+#| fig-cap: "Basic function dependency graph for example_function"
+#| eval: !expr requireNamespace("foodwebr", quietly = TRUE) && is_html_output
+plot(fw)
+```
+
+::: {.callout-note}
+## Understanding the Graph
+
+- **Nodes** represent individual functions
+- **Edges** (arrows) show function calls
+- An arrow from A to B means "A calls B"
+- Isolated nodes indicate functions with no dependencies or dependents
+:::
+
+### Filtering Options
+
+By default, `foodwebr` filters to show only functions directly related to the specified function. You can control this behavior:
+
+```{r}
+#| eval: !expr requireNamespace("foodwebr", quietly = TRUE)
+# Show only connected functions (default)
+fw_filtered <- foodweb(FUN = example_function, filter = TRUE)
+
+# Show all functions in the environment
+fw_all <- foodweb(FUN = example_function, filter = FALSE)
+```
+
+### Examining the Entire Package
+
+To see all exported and internal functions in the package:
+
+```{r}
+#| eval: !expr requireNamespace("foodwebr", quietly = TRUE)
+# Get the package namespace environment
+pkg_env <- asNamespace("rpt")
+
+# Create foodweb for entire package
+fw_pkg <- foodweb(env = pkg_env)
+fw_pkg
+```
+
+```{r}
+#| label: fig-package-foodweb
+#| fig-cap: "Complete dependency graph for all package functions"
+#| eval: !expr requireNamespace("foodwebr", quietly = TRUE) && is_html_output
+plot(fw_pkg)
+```
+
+## Analyzing Dependencies
+
+### Understanding Function Roles
+
+From the dependency graph, you can identify different types of functions:
+
+- **Leaf functions**: Functions that don't call any other package functions (only base R or external packages)
+- **Root functions**: Functions that are called by many others but don't call package functions themselves
+- **Hub functions**: Functions that both call and are called by many functions
+- **Isolated functions**: Functions with no connections to others
+
+### Example Analysis
+
+For our package:
+
+```{r}
+#| eval: !expr requireNamespace("foodwebr", quietly = TRUE)
+# Convert to text representation for inspection
+cat(as.character(fw_pkg))
+```
+
+::: {.callout-tip}
+## Dependency Patterns
+
+In well-designed packages, you typically see:
+
+1. **Clear hierarchy**: Lower-level utility functions are called by higher-level API functions
+2. **Limited cross-dependencies**: Functions in the same "layer" don't call each other excessively
+3. **Focused functions**: Each function has a clear purpose with limited dependencies
+
+In the `rpt` package, we can see this pattern:
+
+- `validate_input()` is a low-level helper (no package dependencies)
+- `clean_data()` calls `validate_input()`
+- `calculate_statistic()` calls `clean_data()`
+- `format_result()` is a low-level formatter (no package dependencies)
+- `example_function()` is a high-level API that calls `calculate_statistic()` and `format_result()`
+- `calculate_summary()` is another high-level API that calls `clean_data()` and `example_function()`
+
+This creates a clear layered architecture with well-separated concerns.
+:::
+
+## Package Architecture
+
+### Current Structure
+
+Let's examine the actual structure of the `rpt` package:
+
+```{r}
+#| eval: !expr requireNamespace("foodwebr", quietly = TRUE)
+# List all functions in the package
+pkg_functions <- ls(asNamespace("rpt"))
+cat("Package functions:\n")
+cat(paste("-", sort(pkg_functions), collapse = "\n"))
+```
+
+The dependency relationships are:
+
+- **Exported functions** (user-facing API):
+ - `example_function()`: Calls `calculate_statistic()` → `format_result()`
+ - `calculate_summary()`: Calls `clean_data()` → `example_function()`
+
+- **Internal functions** (implementation details):
+ - `validate_input()`: No package dependencies (leaf function)
+ - `clean_data()`: Calls `validate_input()`
+ - `calculate_statistic()`: Calls `clean_data()`
+ - `format_result()`: No package dependencies (leaf function)
+
+This architecture demonstrates good separation of concerns with clear data flow from validation through cleaning to calculation and formatting.
+
+## Advanced Usage
+
+### Using with tidygraph
+
+The `tidygraph` package provides powerful tools for graph analysis. You can convert a `foodweb` object to work with these tools:
+
+```{r}
+#| eval: !expr requireNamespace("foodwebr", quietly = TRUE) && requireNamespace("tidygraph", quietly = TRUE)
+if (requireNamespace("tidygraph", quietly = TRUE)) {
+ tg <- tidygraph::as_tbl_graph(fw_pkg)
+ print(tg)
+}
+```
+
+### Customizing Visualizations
+
+You can pass additional arguments to customize the graph appearance:
+
+```{r}
+#| eval: false
+# These arguments are passed to DiagrammeR::grViz()
+plot(fw_pkg,
+ width = 800,
+ height = 600)
+```
+
+### Exporting as Text
+
+Get the graphviz DOT representation:
+
+```{r}
+#| eval: !expr requireNamespace("foodwebr", quietly = TRUE)
+# As text
+foodweb_text <- foodweb(env = asNamespace("rpt"), as.text = TRUE)
+cat(foodweb_text)
+```
+
+## Practical Applications
+
+### Code Review
+
+Use dependency graphs during code review to:
+
+- Verify that new functions follow existing architectural patterns
+- Identify unexpected dependencies
+- Ensure proper separation of concerns
+- Detect circular dependencies
+
+### Refactoring
+
+Before refactoring:
+
+1. Generate a dependency graph of the affected area
+2. Identify all functions that will be impacted
+3. Plan changes to minimize ripple effects
+4. After refactoring, regenerate the graph to verify improvements
+
+### Documentation
+
+Include dependency graphs in:
+
+- Developer documentation
+- Architecture decision records
+- Package vignettes (like this one!)
+- README files for complex packages
+
+::: {.callout-important}
+## Keeping Documentation Updated
+
+When adding new functions or modifying dependencies, regenerate the foodweb graphs to keep documentation current. Consider adding automated checks in your CI/CD pipeline.
+:::
+
+## Exploring Specific Functions
+
+You can focus on specific functions to understand their dependencies:
+
+### Example Function Dependencies
+
+```{r}
+#| label: fig-example-function
+#| fig-cap: "Dependency graph for example_function showing all related functions"
+#| eval: !expr requireNamespace("foodwebr", quietly = TRUE) && is_html_output
+fw_example <- foodweb(FUN = example_function)
+plot(fw_example)
+```
+
+### Summary Function Dependencies
+
+```{r}
+#| label: fig-summary-function
+#| fig-cap: "Dependency graph for calculate_summary showing its relationships"
+#| eval: !expr requireNamespace("foodwebr", quietly = TRUE) && is_html_output
+fw_summary <- foodweb(FUN = calculate_summary)
+plot(fw_summary)
+```
+
+These focused views help understand:
+
+- What functions a specific function depends on (its dependencies)
+- What functions depend on a specific function (its dependents)
+- The complete call chain for a given function
+
+## Best Practices
+
+When designing package architecture:
+
+1. **Minimize dependencies**: Each function should have a clear, focused purpose
+2. **Avoid circular dependencies**: A should not call B if B calls A
+3. **Layer your functions**: Separate low-level utilities from high-level APIs
+4. **Document dependencies**: Use `foodwebr` graphs in your documentation
+5. **Review regularly**: Check dependency graphs during code review
+
+## Learn More
+
+- **foodwebr documentation**:
+- **Package website**:
+- **GitHub repository**:
+- **DiagrammeR**:
+- **tidygraph**:
+
+## Summary
+
+This article demonstrated how to use `foodwebr` to visualize and understand package function dependencies. Key takeaways:
+
+- `foodwebr` creates interactive dependency graphs
+- Filtering options help focus on specific functions or show entire packages
+- Dependency graphs aid in code review, refactoring, and documentation
+- Integration with `tidygraph` enables advanced graph analysis
+- Regular visualization helps maintain clean architecture
+
+The `rpt` package demonstrates a clean layered architecture with:
+- Low-level utility functions (`validate_input()`, `format_result()`)
+- Mid-level processing functions (`clean_data()`, `calculate_statistic()`)
+- High-level API functions (`example_function()`, `calculate_summary()`)
+
+This structure makes the code easier to understand, test, and maintain.
+
+## References
+
+::: {#refs}
+:::