Changes in version 1.8.0 (2026-08-06) CRAN resubmission fixes - plotter(), plotterpro() and upload_tryCatch_pro() now return their HTML/JSON response as a string (invisibly) instead of only writing it via cat(). Under rApache, standard output is the actual HTTP response body, so the cat() calls remain and the deployment's behavior is unchanged; the return value simply gives a normal R caller (including R CMD check's console-output scanner) an object to work with instead of unsuppressable console text. - plotter() now saves and restores the global error option via an immediate on.exit() instead of leaving options(error = dump.frames) in effect for the rest of the R session after the function returns. Bug fixes - calculate_sds(..., type = "tv") no longer has a discontinuity at Prader bead boundaries: a raw ml reading is now mapped onto a continuous bead-index scale before interpolating, so both the low and high percentiles in the mid-P formula move together and the SDS varies smoothly with ml instead of jumping right at each bead threshold (e.g. 11.99ml vs 12.00ml). - calculate_sds(..., type = "tv") now correctly returns NA for an NA age or stage, instead of a finite but wrong value. The continuous bead-index interpolation above used approx()'s default na.rm = TRUE, which silently drops NA (x, y) pairs from the interpolation table before interpolating -- so an NA age (which makes every real bead percentile NA) left only the two artificial boundary values (P(TV>=0)=1, P(TV>=Inf)=0) in the table, and interpolated between just those two instead of propagating the NA. - interpolate_trajectory()'s returned stage component is now the same length as x/y/patient. It was previously built from the cumulative length of the output-so-far across all segments instead of the current segment's own length, making it longer than the other three components whenever a patient had more than one stage segment. Documentation - Documented that calculate_sds()'s return value is always the same length as age, and that stage must be the same length as age (not recycled) -- a length-1 stage, or any other length mismatch, is unsupported and can silently truncate, warn, produce NAs, or error depending on type. - Added vignettes/tanner.Rmd, covering a fuller plot_stadia() walkthrough across its type/plotline/colors/overlay arguments, how raw tv (ml) readings are handled differently by calculate_sds() (continuous interpolation) versus plot_stadia() (snap to the nearest Prader bead curve), and the mid-P value SDS methodology of van Buuren & Ooms (2009) -- including why it is a discrete/ordinal method distinct from LMS/BCCG continuous-measurement SDS (e.g. centile::y2z()). Originally written as a Quarto vignette (tanner.qmd, %\VignetteEngine{quarto::html}); converted to classic knitr::rmarkdown (VignetteBuilder: knitr) after CI revealed an unresolved upstream Windows-only bug in the Quarto vignette engine (see "Continuous integration" below) that would have blocked CRAN submission, since CRAN's own build machines check Windows. - Added a "Package structure" section to the README distinguishing the supported general-purpose library API (plot_stadia*, interpolate_trajectory, find_stage_segments, calculate_sds, tnologo) from the rApache webapp request handlers (plotter, plotterpro, upload_tryCatch_pro) kept only for the existing deployment. plotter(), plotterpro() and upload_tryCatch_pro() are now also grouped under a @family webapp-internals roxygen tag, so they cross-reference each other in ?tanner and on the pkgdown site. - Added a pkgdown site (_pkgdown.yml), with a reference index split into "Plotting & analysis", "Reference data", and "Webapp internals" sections, and URL/BugReports fields in DESCRIPTION. - Added full citations for van Buuren S (2014), "Growth charts of human development," Statistical Methods in Medical Research, 23(4), 346-368, in nl1997's source documentation. Testing - Added testthat unit tests for calculate_sds(), interpolate_trajectory(), find_stage_segments(), and smoke tests for the plot_stadia* drawing functions (which draw to a graphics device and return invisible NULL, so are tested by asserting they run without error). Extracted the tv ml-to-nearest-bead snapping logic out of plot_stadia_data() into an internal snap_tv_to_nearest_bead() helper so it can be unit tested directly. Internal refactoring - calculate_sds() and snap_tv_to_nearest_bead() each independently parsed the ml threshold from "T"-prefixed tv reference column names (e.g. "T12" -> 12); extracted the shared parsing step into an internal tv_ml_thresholds() helper. Pure refactor, no behavior change -- the bracket-and-interpolate vs. snap-to-nearest logic that follows at each call site is untouched. Continuous integration - Added .github/workflows/R-CMD-check.yaml (a standard R CMD check matrix across macOS/Windows/Ubuntu) and .github/workflows/pkgdown.yaml (builds and deploys the pkgdown site to GitHub Pages on push to master). windows-latest was briefly excluded from the check matrix after CI revealed that the quarto::html vignette engine hits an unresolved upstream Windows-only bug during R CMD build (the Quarto subprocess can't see the temporary library R CMD build just installed the package into; r-lib/rcmdcheck#236); re-added once the vignette was converted to classic knitr::rmarkdown (see "Documentation" above), which doesn't hit this bug since it renders in-process rather than via an external Quarto subprocess. - _pkgdown.yml now sets template: bootstrap: 5 (Quarto-rendered articles require it; the default Bootstrap 3 made the pkgdown site build fail before it ever reached the deploy step) and llm-docs: false (pkgdown's llms.txt generation, not needed here and fragile against the runner's pandoc version). Package loading - library(tanner) no longer creates a plotdumpdir directory (or writes to disk at all) as a side effect of loading the package. The directory is now created lazily, on the first actual write inside plotter()/plotterpro()/upload_tryCatch_pro(); plotdumpdir remains exported and mutable. - plotdumpdir now defaults to a "plotfiles" subdirectory of tempdir() instead of the hardcoded /tmp/plotfiles, and upload_tryCatch_pro()'s uploaded-file and .RData handoff files move there too (previously hardcoded to /tmp independently of plotdumpdir). This is a behavior change for the rApache deployment: tempdir() must resolve to a writable location that stays stable across the upload and plot requests for a given user session (true for a single long-running R process, which is how rApache normally hosts an R interpreter per worker). Unix-only /tmp was also a portability problem for R CMD check/CI on Windows and macOS runners, even though the webapp handlers themselves only ever ran on Linux. Breaking changes - Renamed three exported functions to consistent snake_case, matching plot_stadia()'s existing naming style: - calculateSDS() -> calculate_sds() - findXY() -> interpolate_trajectory() - findSegments() -> find_stage_segments() There are no backwards-compatible aliases for the old names; update any code calling them directly. - Renamed the two bundled reference datasets, following the naming convention used by the nlreferences and clopus packages: - pub.ref -> nl1997 - pub.ref.lines -> nl1997_lines Changes in version 1.6.0 Visual style matched to original TNO reference diagrams - Percentile-band colors, default plot colors, symbol/line/font sizes, and the single-patient webapp's PNG resolution now match the package's own historical reference diagrams (genital.pdf, boy.pdf, and the breast stage-line-diagram template), sourced by sampling exact pixel colors and by recovering the original plotting script (figures.r) that generated them. - Fixed plot_stadia() silently ignoring its own padid argument: the function always appended the patient id to the title regardless of what was passed, so padid = FALSE (already used by plotter()) never actually worked. - Replaced foreign::read.spss() with haven::read_sav() in upload_tryCatch_pro(); haven moved from Depends to Imports. Documentation - Converted README.Rmd to README.qmd (Quarto), for better editor support and to avoid a pandoc version mismatch on this system. Replaced the synthetic example data in the first README figure with a real patient's genital-stage trajectory, recovered from the original demo dataset (pubertal demo data.txt) used to produce genital.pdf/ boy.pdf, and noted the data source in the figure title. Changes in version 1.5.0 Package renamed - The package was renamed from pubertyplot to tanner, and the GitHub repository moved to growthcharts/tanner. Update library(pubertyplot) calls to library(tanner), and devtools::install_github(...) calls to point at growthcharts/tanner. Data corrections - Replaced the bundled testicular-volume reference data with a corrected R refit (pub.ref.1997r.txt). The previously shipped pub.ref.1997.txt was an older S-Plus fit that the original 2008 fitting script's own validation had already flagged as containing errors in the tv (testicular volume) curves — some tv percentiles were missing entirely at younger ages. The refit's genital-stage percentiles match Table 5 of van Buuren S (2014), "Growth charts of human development," Statistical Methods in Medical Research, 23(4), 346-368, https://doi.org/10.1177/0962280212473300, exactly. pub.ref and pub.ref.lines were rebuilt from the corrected data via data-raw/R/create_references.R. - pub.ref$tv / pub.ref.lines$tv now carry the full 12-point testicular volume scale (1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 25 ml), up from 7 columns previously. Testicular volume is now recorded directly in ml - calculateSDS() and plot_stadia() previously required tv to be recoded from a raw ml reading into a bead-index (1-8, then later 1-12) by hand before use. tv is now passed as the raw ml value directly — no recoding step required — and calculateSDS() correctly interpolates readings that fall between Prader orchidometer beads (e.g. 11 ml or 16 ml), rather than requiring one of the 12 standard bead volumes. plot_stadia() traces the trajectory along the nearest reference curve for the same off-bead readings. - Removed the manual tv recoding tables from plotter(), plotterpro() and the README examples, since they are no longer needed. - upload_tryCatch_pro()'s upload validation for tv now accepts any plausible ml reading (0-30) instead of rejecting anything outside the old fixed bead list. - The single-patient web app's testicular volume dropdown (inst/webapps/puberty/puberty.js) now offers the correct 12 Prader bead volumes (1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 25), replacing the previous incomplete/incorrect 8-value list. - Regenerated the bundled demo data (inst/webapps/pubertypro/demo.csv, demo.txt, demo.sav) with tv values spanning the corrected 12-point scale, including off-bead readings, since the original demo values were from a different orchidometer scale and no longer matched the corrected reference data. - Fixed the testis reference lines and labels in plot_stadia() / plot_stadia_lines(), which still only drew 8 of the 12 reference curves and labeled them with the old bead volumes. Documentation - Added a runnable @examples block to findXY(). - Rewrote the README with a plain-R SDS calculation walkthrough, a local mock web server (see below) for trying out the browser frontends without Apache/rApache, a bulk-plotting walkthrough for Puberty Pro, a citation for the underlying method (van Buuren & Ooms, 2009, Statistics in Medicine), GitHub install instructions, and an embedded example chart. - Documented all exported functions and data objects with roxygen2 (R/data.R, man pages for every exported function). - Bumped the minimum R version to 3.5.0, required by the version-3 serialization format used by the rebuilt .rda data files. Developer-facing changes - Added tools/run_mock_webapp.R, a local server (via httpuv) that serves the puberty and pubertypro browser frontends and answers their AJAX endpoints without needing a real Apache + rApache deployment. - Fixed the mock server writing generated plots to the wrong directory (it overrode a global plotdumpdir rather than the package-namespace variable that plotter()/plotterpro() actually read from). - Renamed dotted function names (e.g. plot.stadia) to underscore form (plot_stadia) to avoid the misleading appearance of S3 methods. - Replaced T/F with TRUE/FALSE in default arguments and call sites. - Applied the AIR formatter across the R source. - Replaced foreign::read.spss() with haven::read_sav() for reading uploaded .sav files in upload_tryCatch_pro(), moving haven from Depends to Imports.