---
title: "Contributing to hvtiPlotR"
author: "John Ehrlinger <ehrlinj@ccf.org>"
date: today
format:
  html:
    toc: true
    toc-depth: 3
    html-math-method: mathjax
editor:
  markdown:
    wrap: 80
vignette: >
  %\VignetteIndexEntry{Contributing to hvtiPlotR}
  %\VignetteEngine{quarto::html}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE, eval = FALSE, message = FALSE,
                      warning = FALSE)
```

If you are a biostatistician porting a SAS template to R, start at
[Track A](#track-a). If you are here for testing, CI, or package
infrastructure, start at [Track B](#track-b).

---

# Development environment setup {#setup}

## Prerequisites

- R ≥ 4.1
- RStudio (recommended) or any R-capable editor
- Git

## Installing development dependencies

Clone the repository, then install the development dependencies declared in
`DESCRIPTION`. The `install_deps()` call picks up both `Imports` (needed at
runtime) and `Suggests` (needed to run tests and build vignettes), so your
local environment matches what CI sees.

```r
# Clone (first time only)
# git clone https://github.com/ehrlinger/hvtiPlotR.git

# Install devtools if needed
install.packages("devtools")

# Install all Imports + Suggests from DESCRIPTION
devtools::install_deps(dependencies = TRUE)
```

## The development workflow loop

These four calls cover the full cycle for any code change: reload, redocument,
test, then check. You will run `load_all()` and `test()` constantly; `document()`
whenever you edit roxygen comments; `check()` before opening a PR.

```r
# Load the package into the current session without installing
devtools::load_all()

# Regenerate NAMESPACE and .Rd files from roxygen comments
devtools::document()

# Run all tests
devtools::test()

# Full R CMD CHECK (must pass before merging)
devtools::check()

# Build vignettes only
devtools::build_vignettes()
```

`devtools::load_all()` re-sources all `R/*.R` files and makes every exported
function available immediately, much faster than `install.packages()` during
development.

---

# Track A: Porting a SAS template {#track-a}

This track walks through adding a brand-new plot by porting an existing SAS
template. We use a fictional template `tp.np.bmi.avrg_curv.binary.sas` as the
running example, and port it the way every new plot family in the package is
built (`hazard_plot()`, `survival_difference_plot()` and `nnt_plot()` are
legacy single-call functions kept for compatibility, not a pattern to copy): a
constructor, `hv_bmi_curve()`, that validates the data and returns an
`hv_data` object, and a `plot.hv_bmi_curve()` method that turns that object
into a bare ggplot. `R/spaghetti-plot.R` is the real file to keep open beside
you; the example below follows it step for step.

## Step 1: Understand the SAS template output

Before writing any R code, identify:

1. **What datasets does the template produce?**
   Most `tp.np.*` templates export a `predict` dataset (the fitted curve)
   and a `means` dataset (binned patient-level summaries). Export these to
   CSV from SAS to use as your development data.

2. **What are the SAS column names?**
   Document the mapping in the constructor's roxygen `@description` block.
   For example:

   | SAS column | R column | Meaning |
   |---|---|---|
   | `iv_bmi` | `time` | x-axis variable (BMI) |
   | `mean_curv` | `estimate` | predicted probability |
   | `cll_p68` | `lower` | 68 % CI lower bound |
   | `clu_p68` | `upper` | 68 % CI upper bound |

3. **Which existing R function is closest?**
   Check `_pkgdown.yml` and the plot-functions vignette. You may only need
   to extend an existing function rather than create a new one. For this
   template, `hv_nonparametric()` already covers most of the ground, so in
   real life you would extend it. We build a new pair here only to show
   the pattern.

## Step 2: Create the R source file

Create `R/bmi-curve-plot.R`. Filenames are kebab-case and end in `-plot.R`
where the concept is a plot (`R/spaghetti-plot.R`,
`R/nonparametric-curve-plot.R`). One concept lives in one file: the
sample-data generator, the `hv_<concept>()` constructor, and its `print` and
`plot` methods together.

The work splits in two, and the split is the point. The constructor checks
the data and records which columns play which role; it draws nothing. The
plot method reads that record back and draws; it never re-validates column
names. A caller can print the object, inspect `$data`, or plot it several
ways without repeating the checks.

### The constructor

```r
# File: R/bmi-curve-plot.R

#' Prepare average BMI curve data for plotting
#'
#' Validates the fitted curve output from a nonparametric analysis of a
#' binary outcome against BMI (or any continuous covariate) and returns an
#' `hv_bmi_curve` object. Call [plot.hv_bmi_curve()] on the result to draw
#' it. Ports `tp.np.bmi.avrg_curv.binary.sas`.
#'
#' SAS column mapping:
#' - `time` is `iv_bmi` (BMI on the x-axis)
#' - `estimate` is `mean_curv` (predicted probability)
#' - `lower` is `cll_p68` (68 % CI lower)
#' - `upper` is `clu_p68` (68 % CI upper)
#'
#' @param data         Data frame of fitted curve output, one row per x value.
#' @param x_col        Name of the x-axis column. Default `"time"`.
#' @param estimate_col Name of the predicted value column. Default `"estimate"`.
#' @param lower_col    Name of the lower CI column, or `NULL` for no ribbon.
#'   Default `NULL`.
#' @param upper_col    Name of the upper CI column, or `NULL`. Default `NULL`.
#'
#' @return An object of class `c("hv_bmi_curve", "hv_data")`; call `plot()`
#'   on it to draw the figure. The list contains:
#'   - `$data`: the validated input data frame.
#'   - `$meta`: named list of the column names above, plus `n_points` and
#'     `n_missing`.
#'   - `$tables`: empty list.
#'
#' @seealso [plot.hv_bmi_curve()] to draw the figure,
#'   [sample_bmi_curve_data()] for example data,
#'   [theme_hv_manuscript()] for the publication theme.
#'
#' @references SAS template: `tp.np.bmi.avrg_curv.binary.sas`.
#'
#' @family BMI curve
#'
#' @examples
#' dat <- sample_bmi_curve_data(n = 500)
#' bc  <- hv_bmi_curve(dat, lower_col = "lower", upper_col = "upper")
#' bc  # prints the column mapping
#'
#' @export
hv_bmi_curve <- function(data,
                         x_col        = "time",
                         estimate_col = "estimate",
                         lower_col    = NULL,
                         upper_col    = NULL) {
  .check_df(data)
  .check_cols(data, c(x_col, estimate_col, lower_col, upper_col))
  incomplete <- .count_incomplete(data, c(x_col, estimate_col))

  new_hv_data(
    data = as.data.frame(data),
    meta = list(
      x_col        = x_col,
      estimate_col = estimate_col,
      lower_col    = lower_col,
      upper_col    = upper_col,
      n_points     = nrow(data),
      n_missing    = incomplete$n_missing
    ),
    tables   = list(),
    subclass = "hv_bmi_curve"
  )
}
```

`new_hv_data()` in `R/hvti-data.R` is the only way to build the return
value. It guarantees the three slots every `hv_data` object carries
(`$data`, `$meta`, `$tables`) and sets the class to
`c("hv_bmi_curve", "hv_data")`, so the base methods in that file act as the
fallback for anything your subclass does not define. `.check_df()`,
`.check_cols()` and `.count_incomplete()` live in `R/validators.R`; use them
rather than writing your own `stop()` calls, so the error messages match the
rest of the package.

### The print and plot methods

```r
#' Print an hv_bmi_curve object
#'
#' @param x   An `hv_bmi_curve` object from [hv_bmi_curve()].
#' @param ... Ignored.
#' @return `x`, invisibly.
#' @export
print.hv_bmi_curve <- function(x, ...) {
  m <- x$meta
  cat("<hv_bmi_curve>\n")
  cat(sprintf("  N points    : %d\n", m$n_points))
  cat(sprintf("  x / estimate: %s / %s\n", m$x_col, m$estimate_col))
  if (!is.null(m$lower_col))
    cat(sprintf("  CI columns  : %s / %s\n", m$lower_col, m$upper_col))
  invisible(x)
}

#' Plot an hv_bmi_curve object
#'
#' Draws the average curve, with a confidence ribbon when the constructor
#' was given CI columns.
#'
#' @param x          An `hv_bmi_curve` object.
#' @param line_width Width of the curve line. Default `1.0`.
#' @param alpha      Transparency of the ribbon in \eqn{[0,1]}. Default `0.2`.
#' @param ...        Ignored; present for S3 consistency.
#'
#' @return A bare [ggplot2::ggplot()] object; compose with `+` to add
#'   scales, labels, and [theme_hv_manuscript()].
#'
#' @seealso [hv_bmi_curve()] to build the data object.
#'
#' @family BMI curve
#'
#' @examples
#' dat <- sample_bmi_curve_data(n = 500)
#' bc  <- hv_bmi_curve(dat, lower_col = "lower", upper_col = "upper")
#' plot(bc) +
#'   ggplot2::scale_y_continuous(labels = scales::percent) +
#'   ggplot2::labs(x = "BMI (kg/m2)", y = "Prevalence of AF") +
#'   theme_hv_manuscript()
#'
#' @importFrom ggplot2 ggplot aes geom_line geom_ribbon
#' @importFrom rlang .data
#' @export
plot.hv_bmi_curve <- function(x, line_width = 1.0, alpha = 0.2, ...) {
  .check_alpha(alpha)
  m <- x$meta

  p <- ggplot2::ggplot(
    x$data,
    ggplot2::aes(x = .data[[m$x_col]], y = .data[[m$estimate_col]])
  )

  if (!is.null(m$lower_col) && !is.null(m$upper_col)) {
    p <- p + ggplot2::geom_ribbon(
      ggplot2::aes(ymin = .data[[m$lower_col]], ymax = .data[[m$upper_col]]),
      alpha = alpha
    )
  }

  p + ggplot2::geom_line(linewidth = line_width)
}
```

The conventions these two functions follow are the ones in the conventions
table in `CONTRIBUTING.md`:

- **Column names are strings**, passed as `x_col = "time"`, and stored in
  `$meta` for the plot method to read back. Never use `enquo()` or `{{ }}`;
  they make column names opaque to the caller.
- **`.data[[col]]`** does the tidy evaluation. It needs
  `@importFrom rlang .data`.
- **No colors or themes** are applied inside either function. The caller
  adds `scale_color_*()`, `labs()` and `theme_hv_manuscript()` afterwards,
  as the examples show.
- **The plot method returns `p`**, not `print(p)` or `p + theme(...)`.

S3 registration comes from the plain `@export` tag on `print.hv_bmi_curve()`
and `plot.hv_bmi_curve()`. roxygen2 recognizes the `generic.class` name and
writes `S3method(plot,hv_bmi_curve)` into `NAMESPACE`, next to
`S3method(plot,hv_spaghetti)`. You do not need `@method`.

## Step 3: Add a sample-data generator

Add `sample_bmi_curve_data()` to the top of the same file, as
`sample_spaghetti_data()` sits at the top of `R/spaghetti-plot.R`. The
generator should:

- Accept `n` (patient count), the x range, and `seed`.
- Return a data frame whose column names **match the constructor defaults**
  (`time`, `estimate`, `lower`, `upper`), not the SAS column names.
- Produce realistic-looking data at a plausible scale.

```r
#' Sample BMI Curve Data
#'
#' Simulates the fitted curve output from a nonparametric BMI analysis,
#' matching the column layout expected by [hv_bmi_curve()].
#'
#' @param n        Number of simulated patients (controls CI width).
#'   Default `500`.
#' @param bmi_min  Lower end of the BMI range. Default `18`.
#' @param bmi_max  Upper end of the BMI range. Default `50`.
#' @param n_points Number of points on the prediction grid. Default `200`.
#' @param seed     Random seed. Default `42`.
#'
#' @return A data frame with columns `time` (BMI grid), `estimate`,
#'   `lower`, `upper`.
#'
#' @seealso [hv_bmi_curve()]
#'
#' @examples
#' dat <- sample_bmi_curve_data(n = 300)
#' head(dat)
#'
#' @importFrom stats plogis qnorm
#' @export
sample_bmi_curve_data <- function(n        = 500,
                                  bmi_min  = 18,
                                  bmi_max  = 50,
                                  n_points = 200,
                                  seed     = 42L) {
  set.seed(seed)
  z   <- stats::qnorm(0.84)          # 68 % CI is about 1 SD
  bmi <- seq(bmi_min, bmi_max, length.out = n_points)
  est <- stats::plogis(-2 + 0.05 * (bmi - 30))
  se  <- sqrt(est * (1 - est) / (n * 0.02))

  data.frame(
    time     = bmi,
    estimate = est,
    lower    = pmax(est - z * se, 0),
    upper    = pmin(est + z * se, 1)
  )
}
```

## Step 4: Write roxygen documentation

Roxygen markdown is enabled in this package (`Roxygen: list(markdown = TRUE)`
in `DESCRIPTION`), so backticks and `[fn()]` links render as written. Each
exported function needs:

| Tag | Constructor | `plot` method | Notes |
|---|---|---|---|
| Description | Yes | Yes | Paragraph after the title, or `@description`; names the SAS template and column mapping |
| `@param` | Yes | Yes | One per argument; include the default |
| `@return` | Yes | Yes | Constructor: the class and its three slots. Method: a bare ggplot |
| `@seealso` | Yes | Yes | Link the pair to each other and to the `sample_*()` generator |
| `@references` | Yes | No | The exact SAS template filename(s) |
| `@family` | Yes | Yes | Same family name on both, so their help pages cross-link |
| `@examples` | Yes | Yes | Must run; use `\donttest{}` for slow examples |
| `@importFrom` | As needed | As needed | Declare every function used from other packages |
| `@export` | Yes | Yes | On the method it also registers the S3 method |

Then regenerate `NAMESPACE` and `man/`:

```r
devtools::document()
```

Check that `NAMESPACE` now carries `export(hv_bmi_curve)`,
`export(sample_bmi_curve_data)`, `S3method(plot,hv_bmi_curve)` and
`S3method(print,hv_bmi_curve)`. Commit `NAMESPACE` and `man/` with the
source change.

## Step 5: Register in `_pkgdown.yml`

The `reference:` index is explicit, and pkgdown errors on an exported topic
missing from it. List the constructor, both methods and the generator, in
the same order the existing sections use:

```yaml
- title: "Nonparametric Covariate Curves"
  desc: >
    Average curves plotted against a continuous covariate (BMI, age, etc.)
    rather than against time. Ports `tp.np.bmi.avrg_curv.binary.sas`.
  contents:
  - hv_bmi_curve
  - plot.hv_bmi_curve
  - print.hv_bmi_curve
  - sample_bmi_curve_data
```

If an existing section fits, add the four lines there instead of starting a
new one.

## Step 6: Add a worked example to the plot-functions vignette

Open `vignettes/plot-functions.qmd` and add a new top-level section before
the "Draft Footnotes" section. Show both steps, so a reader sees the object
before the figure:

````markdown
# BMI Curve Plot

`hv_bmi_curve()` prepares a fitted nonparametric average curve of a binary
outcome against BMI; `plot()` draws it ...

```{r bmi_curve_basic}
dat <- sample_bmi_curve_data(n = 500)
bc  <- hv_bmi_curve(dat, lower_col = "lower", upper_col = "upper")
bc
plot(bc) + ggplot2::labs(x = "BMI (kg/m2)", y = "Prevalence of AF") +
  theme_hv_manuscript()
```
````

## Step 7: Add a row to the SAS migration guide

In `vignettes/sas-migration-guide.qmd`, add a row to the template lookup
table. The third column names the constructor:

```markdown
| `tp.np.bmi.avrg_curv.binary.sas` | np | `hv_bmi_curve()` | [BMI curve](#np-bmi) |
```

Then add the corresponding section with a runnable example further down in
the `# Nonparametric temporal trends` family.

## Step 8: Write tests {#step-tests}

Create `tests/testthat/test_bmi_curve_plot.R`. Test files use an underscore,
`test_*.R`, and the name follows the source file.

A plot test has to prove the plot carries data. A ggplot whose every layer
holds zero rows still has class `"ggplot"` and still renders an empty
panel, so `expect_s3_class(p, "ggplot")` on its own is a smoke test, not
coverage. `tests/testthat/helper-plot-data.R` provides
`expect_plot_has_data()`, which runs `ggplot2::ggplot_build()` and fails
when a data layer is empty. testthat loads `helper-*.R` files before the
tests, so the helper is available without a `source()` call. Its arguments
tighten the check:

- `min_rows`: every data layer must hold at least this many rows.
- `geoms`: each geom class named here must appear in the plot, for example
  `"GeomRibbon"`.
- `min_groups`: at least one data layer must split into this many groups,
  which catches a stratified plot that collapsed to one line.

Reference lines (`GeomHline`, `GeomVline`, `GeomAbline`) do not count as
data layers, though each must still draw at least one row.

```r
library(testthat)
library(hvtiPlotR)

dat <- sample_bmi_curve_data(n = 100, n_points = 50, seed = 1L)

test_that("sample_bmi_curve_data returns the constructor's default columns", {
  expect_s3_class(dat, "data.frame")
  expect_named(dat, c("time", "estimate", "lower", "upper"))
  expect_equal(nrow(dat), 50)
})

test_that("hv_bmi_curve returns an hv_data object with the column mapping", {
  bc <- hv_bmi_curve(dat, lower_col = "lower", upper_col = "upper")
  expect_s3_class(bc, c("hv_bmi_curve", "hv_data"))
  expect_equal(bc$meta$estimate_col, "estimate")
  expect_identical(bc$tables, list())
})

test_that("hv_bmi_curve errors on a missing column", {
  expect_error(hv_bmi_curve(dat, x_col = "no_such_col"), "column")
})

test_that("plot.hv_bmi_curve draws the curve from every data row", {
  p <- plot(hv_bmi_curve(dat))
  expect_plot_has_data(p, min_rows = nrow(dat), geoms = "GeomLine")
})

test_that("plot.hv_bmi_curve adds a ribbon carrying data when CI columns are given", {
  p <- plot(hv_bmi_curve(dat, lower_col = "lower", upper_col = "upper"))
  expect_plot_has_data(p, min_rows = nrow(dat),
                       geoms = c("GeomRibbon", "GeomLine"))
})

test_that("print.hv_bmi_curve output is stable", {
  expect_snapshot(print(hv_bmi_curve(dat, lower_col = "lower", upper_col = "upper")))
})
```

At minimum, a new plot needs these tests:

| Test | What to check |
|---|---|
| Sample data shape | Class, column names, and number of rows |
| Constructor object | `expect_s3_class(obj, "hv_data")` and the `$meta` entries the plot method reads |
| Error on bad input | `expect_error(hv_<concept>(dat, x_col = "no_such_col"))` |
| Plot carries data | `expect_plot_has_data(plot(obj), min_rows = ...)`; a bare class check does not count |
| Optional layers | `expect_plot_has_data(..., geoms = "GeomRibbon")` for each layer an argument switches on |

The package uses testthat edition 3. The first `devtools::test()` run
writes the `expect_snapshot()` baseline to
`tests/testthat/_snaps/bmi_curve_plot.md`; commit that file with the test.
When a later change alters the output on purpose, review and accept the new
snapshot rather than deleting the file:

```r
devtools::test(filter = "bmi_curve")
testthat::snapshot_review()   # inspect the diff
testthat::snapshot_accept()   # accept the intended change
```

## Step 9: Update NEWS.md

Add a bullet under the `# hvtiPlotR (unreleased)` heading at the top of
`NEWS.md`, adding that heading if it is not already there:

```markdown
* Added `hv_bmi_curve()`, its `plot()` and `print()` methods, and
  `sample_bmi_curve_data()`: a nonparametric average curve of a binary
  outcome against a continuous covariate (BMI).
  Ports `tp.np.bmi.avrg_curv.binary.sas`.
```

## Step 10: Final checklist before opening a PR

Run these commands in order before pushing your branch. All must complete
cleanly: every test passing, and zero errors, zero warnings and zero notes
from `check()`. Then open a pull request against `main` on GitHub.

```r
devtools::document()   # regenerate NAMESPACE + .Rd without errors
devtools::test()       # all tests pass
lintr::lint_package()  # zero lints; CI fails on any
devtools::check()      # 0 errors, 0 warnings, 0 notes
```

---

# Track B: Package infrastructure {#track-b}

## Package structure overview

The tree below shows where each piece of the package lives. The two directories
you will touch most are `R/` (one file per plot family) and
`tests/testthat/` (one test file per source file). Everything under `man/` and
`NAMESPACE` is auto-generated; do not edit those by hand.

```
hvtiPlotR/
├── R/                    # Source: one file per plot family
├── man/                  # Auto-generated .Rd files — do not edit manually
├── tests/
│   └── testthat/         # One test_*.R per source file
├── vignettes/            # Quarto (.qmd) vignettes
├── inst/                 # Bundled files (extdata/, incl. hv_ppt_template.pptx)
├── DESCRIPTION           # Package metadata and dependency declarations
├── NAMESPACE             # Auto-generated — do not edit manually
├── _pkgdown.yml          # Website reference and articles layout
└── NEWS.md               # User-visible changelog
```

`NAMESPACE` and `man/*.Rd` are both auto-generated by `devtools::document()`
from the roxygen comments in `R/*.R`. **Never edit these files by hand.**

## Adding a dependency

- **Runtime dependency** (used inside a plot function): add to `Imports` in
  `DESCRIPTION` and declare with `@importFrom pkg fn` in the roxygen block.
- **Vignette/test only**: add to `Suggests`. Wrap any code that uses it in
  `if (requireNamespace("pkg", quietly = TRUE)) { ... }` or use
  `#| eval: false` in the vignette chunk.

```r
# Check before adding — is it already available in base R or an existing Import?
# Keep Imports lean; every new dependency adds installation weight.
```

## Code style and conventions {#conventions}

### File and function naming

| Item | Convention | Example |
|---|---|---|
| Source files | `kebab-case.R` | `bmi-curve-plot.R` |
| Constructors | `hv_<concept>()` | `hv_bmi_curve()` |
| Plot methods | `plot.hv_<concept>()` | `plot.hv_bmi_curve()` |
| Sample generators | `sample_` prefix | `sample_bmi_curve_data()` |
| Internal helpers | `.` prefix | `.bmi_compute_ci()` |

Internal helpers (prefixed with `.`) should have `@keywords internal` and
no `@export`, so they will not appear in `NAMESPACE` or generate `.Rd`
files.

### The bare-ggplot pattern

Every `plot.hv_<concept>()` method follows five rules:

1. Draw model-output data held in the `hv_data` object (not raw patient data).
2. Take every column reference from a string argument (`x_col =`, etc.) stored in `$meta`.
3. Use `.data[[col]]` for tidy evaluation inside `aes()`.
4. Return an unstyled ggplot: no `scale_*()`, no `labs()`, no theme.
5. Not call `print()` or `invisible()`.

Return the bare ggplot and callers can layer scales, labels, and a theme on
top with the `+` composition grammar covered in
`vignettes/plot-decorators.qmd`.

### Tidy evaluation

We pass column names as strings (e.g., `x_col = "time"`) and reference them
inside `aes()` with `.data[[col]]`. This keeps the caller's interface explicit
and avoids the non-standard evaluation pitfalls that come with `enquo()` or
bare symbols. The `.data` pronoun must be declared in the roxygen block with
`@importFrom rlang .data`.

```r
# Correct — column name is a string; .data masks the data frame
ggplot2::aes(x = .data[[x_col]], y = .data[[estimate_col]])

# Wrong — bare symbol; fails when x_col is a variable
ggplot2::aes(x = x_col, y = estimate_col)

# Wrong — enquo; column name is opaque to the caller
ggplot2::aes(x = !!rlang::enquo(x_col))
```

Always declare `.data` in the roxygen block:
```r
#' @importFrom rlang .data
```

## Testing {#testing}

### Test file layout

Each source file `R/my-plot.R` should have a corresponding
`tests/testthat/test_my_plot.R`. The minimum set of tests for a new plot is the table in
[Step 8 of Track A](#step-tests); the one that matters most is a data-carrying assertion.
`expect_plot_has_data()` in `tests/testthat/helper-plot-data.R` builds the plot with
`ggplot_build()` and fails when a layer holds no rows, which `expect_s3_class(p, "ggplot")`
cannot catch.

### Snapshot tests

The `tests/testthat/_snaps/` directory stores snapshot outputs for
`expect_snapshot()` tests. To update snapshots after an intentional change:

```r
testthat::snapshot_review()   # review diffs interactively
testthat::snapshot_accept()   # accept all pending diffs
```

### Running checks

Use `devtools::test()` for fast, interactive feedback during development.
Switch to `devtools::check()` (or the more verbose `rcmdcheck::rcmdcheck()`)
when you want the full `R CMD CHECK` sweep, including example execution and
vignette builds.

```r
devtools::test()               # run all tests
devtools::test(filter = "bmi") # run tests matching "bmi"
devtools::check()              # full R CMD CHECK
rcmdcheck::rcmdcheck(          # more detailed output
  args = "--no-manual",
  error_on = "warning"
)
```

### What R CMD CHECK validates

- All examples in `@examples` blocks run without error.
- All exported functions have documentation.
- `NAMESPACE` matches the actual exports.
- No undefined global variables (lintr / `R CMD CHECK` NOTE).
- Vignette chunks marked `eval: true` run successfully.

## Vignette conventions {#vignette-conventions}

Vignettes live in `vignettes/` as `.qmd` files; Quarto builds them
(`VignetteBuilder: quarto`). A few things to keep in mind:

- All code chunks that read files, write files, or access a network must
  have `#| eval: false`.
- Use `here::here()` for file paths in eval-false chunks (not hard-coded
  absolute paths).
- Use `system.file("extdata", "hv_ppt_template.pptx", package = "hvtiPlotR")` for the
  bundled PPT template, never a hard-coded path.
- After adding a new vignette, register it in `_pkgdown.yml` under
  `articles`.

## Releasing a new version {#releasing}

1. Update the version in `DESCRIPTION` (follow
   [semantic versioning](https://semver.org/): `MAJOR.MINOR.PATCH`).
2. Rename the `# hvtiPlotR (unreleased)` heading in `NEWS.md` to `# hvtiPlotR X.Y.Z`.
3. Run `devtools::check()`; you need zero errors, zero warnings, and zero notes
   before tagging.
4. Tag the release commit: `git tag -a vX.Y.Z -m "Release X.Y.Z"`.
5. Push the tag: `git push origin vX.Y.Z`.
6. The pkgdown GitHub Action picks up the tag and rebuilds the documentation
   site.

---

# Session info

```{r session-info, eval=TRUE}
sessionInfo()
```
