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} +:::