# janusplot 0.1.1

### v0.1.1 NUMERICAL-SHIFT NOTICE (engine default change)

**v0.1.1 changes the default GAM fitting backend from `mgcv::gam`
to `mgcv::bam`** (Feature 4 below). `bam` uses fREML estimation
instead of REML, which differs by ~1-3% in effective degrees of
freedom on identical data. **Existing v0.1.0 users will see
slightly different EDFs, asymmetry indices, and per-cell colour
fills when they upgrade.** This is the single non-byte-identical
change in v0.1.1 — every other v0.1.1 feature is strictly additive.

**Recovery.** Set `engine = "gam"` on `janusplot()` or
`janusplot_data()` to reproduce v0.1.0 numerical output verbatim.
The package's vdiffr visual-regression suite pins
`engine = "gam"` for exactly this purpose — every old snapshot
remains valid under the backward-compat escape.

### v0.1.1 Feature 4 — Default fitting engine = `bam` (with `gam` escape)

* **New `engine = c("bam", "gam")` argument**, default `"bam"`.
  At janusplot's scale (k = 15-25 vars, 600+ fits per call)
  `mgcv::bam`'s block-Lanczos solve + fREML estimation delivers
  ~3-10x wall-time speedup vs `mgcv::gam` without any new
  dependency (`mgcv` already exports both).
* **fREML vs REML.** `bam` defaults to fREML (fast REML); `gam`
  defaults to REML. The two methods optimise the same penalty
  target via different paths — EDFs differ by ~1-3% on identical
  data. v0.1.1 surfaces this as the one numerical break with
  v0.1.0, documented prominently above.
* **`method` argument default is now `NULL`**, resolved
  per-engine: `"fREML"` for bam, `"REML"` for gam. Users who
  passed `method` explicitly in v0.1.0 see no behaviour change.
* **`discrete = FALSE` (bam-only)** — opt-in to mgcv's
  covariate-discretisation optimisation. Further ~2-5x speedup
  at sub-pixel prediction shift cost.
* **`nthreads = 1L` (bam-only)** — intra-fit threading. Default
  1 to avoid oversubscription when combined with `parallel =
  TRUE` (which fans out across pair-fits already).
* **`engine` + `method` provenance** — both columns now appear
  in `janusplot(..., with_data = TRUE)$data` and on every
  `janusplot_data()$pairs[[i]]` entry. Useful for paper figures
  whose methodology section needs to document which backend
  produced the EDFs they report.
* **`bam` inherits from `gam`** — `class(bam_fit)` is
  `c("bam", "gam", "glm", "lm")`, so `mgcv::k.check()`,
  `predict.gam()`, derivative LP-matrix arithmetic, and every
  shape-metric extractor work without modification. Engine is
  plumbing, not redesign.
* **vdiffr suite pinned to `engine = "gam"`** so every legacy
  visual snapshot continues to validate under the v0.1.0
  backward-compat escape. Acts as a regression gate on the
  `engine = "gam"` recovery path.

### v0.1.1 Feature 3 — Axes rendering modes + figure-to-file output

* **`axes`** rendering knob with four modes:
  `"original"` (default), `"standardised"`, `"centred"`, `"rank"`.
  Applied per variable from raw data; same transform propagates
  consistently to (a) raw scatter, (b) spline prediction grid,
  (c) CI ribbon, and (d) the matrix-border variable label
  (`"mpg (z)"`, `"mpg (centred)"`, `"rank(mpg)"`).
* **Rendering-only invariant.** The underlying `mgcv` fits are
  byte-identical across modes — verified in
  `tests/testthat/test-axes.R` via direct column comparison of
  `with_data` output across all four modes on a non-linear DGP.
* **At-k coverage.** The new test suite renders every
  `(k, mode)` combination across `k ∈ {2, 5, 10, 15, 20, 26}`
  and `mode ∈ {original, standardised, centred, rank}` —
  24 panels — and asserts none emit warnings. Border-label
  suffixing is independently verified at `k ∈ {3, 12, 20}` by
  walking patchwork's plot list and matching against the
  expected suffix pattern.
* **Tier-3 no-op.** At compact tier 3 (`n_var >= 25`) the cells
  render only colour fill + shape-class glyph (no curve, no
  scatter), so `axes` is a documented no-op there. The border
  labels still pick up the mode suffix so the user can tell
  which transform they're looking at.
* **`save_as = NULL`** — new file-output knob. When set to a
  path with extension, writes the assembled matrix to disk via
  [ggplot2::ggsave()]; the device is inferred from the
  extension. Supported: `.png`, `.pdf`, `.svg`, `.jpg` /
  `.jpeg`, `.tif` / `.tiff`, `.eps`, `.ps`, `.bmp`. The
  function still returns the ggplot — the file is a side-effect.
* **`save_width` / `save_height` / `save_dpi`** — overrides
  for the auto-resolved square (`pmax(6, 0.65 * k_n)` inches,
  300 dpi for raster). Useful when targeting a fixed-size
  R Journal figure.

### v0.1.1 Feature 2 — Scale-aware compact rendering

* **Progressive content tiers.** New `compact` argument with values
  `"auto"` (default), `"always"`, and `"never"`. Under `"auto"`,
  each matrix renders at a tier resolved from the pixel-budget
  ladder:
  * Tier 0 (`n_var < 12`) — v0.1.0 behaviour: spline + CI + scatter
    + every annotation.
  * Tier 1 (`12 <= n_var < 18`) — drop raw scatter; keep spline,
    CI, colour fill, and at most one annotation (`k_warn` if it
    was requested).
  * Tier 2 (`18 <= n_var < 25`) — spline + colour fill only.
  * Tier 3 (`n_var >= 25`) — colour mini-tile + shape-class glyph
    at the cell centre; no spline.
* **Configurable ladder.** `compact_threshold = 12L` sets where
  Tier 1 begins; the full ladder shifts with it
  (`t2 = t1 + 6`, `t3 = t1 + 13`). Power users can override the
  ladder directly via `compact_levels = list(t1 = ..., t2 = ...,
  t3 = ...)`.
* **Backward-compat escape.** `compact = "never"` forces Tier 0 on
  any matrix, reproducing v0.1.0 output regardless of `n_var`.
  `compact = "always"` forces at least Tier 1 even at small
  `n_var` (useful for very dense fixed-size renders).
* **Focus filter (companion).** New `focus_by` argument accepts
  `NA` (default — no filter), `"asymmetry"`, `"edf"`,
  `"non_linearity"` (defined as `edf - 1`), or `"k_flag"`. Cells
  whose chosen metric falls below `focus_threshold` (default
  `"q90"`, a quantile string, or a numeric cutoff) are rendered in
  `grey85` at alpha `focus_dim_alpha` (default `0.25`). The matrix
  shape is preserved; attention drains visually to high-metric
  cells. This is a **visual filter, not a statistical one** — the
  underlying fits and the `with_data` table are unchanged.
* **Rendering-only knob.** Compact tier and focus mask are pure
  presentation layers. Fits, EDFs, asymmetry indices, shape
  classifications, k-check diagnostics, and the `with_data`
  summary table are byte-identical across `compact` settings — a
  regression-tested invariant.

### v0.1.1 Feature 1 — Per-cell k-checking (Strategy A always-on + Strategy B opt-in)

* **Diagnostic always on.** Every cell's fit now carries five new
  diagnostic fields derived from `mgcv::k.check()`: `k_prime`,
  `k_index`, `k_p`, `k_flag` (Wood's trifecta: `edf/k' > 0.9 &
  k-index < 1 & p < 0.05`), and `k_check_status` (`"ok"`,
  `"flagged"`, `"unreliable"`). Cells with `n_unique(x) < 10` are
  marked `"unreliable"` rather than flagged — `k.check()`'s
  simulation p-value is meaningless at very low unique-x.
* **Opt-in auto-refit.** New `auto_refit_k = FALSE` (default) and
  `k_max_iter = 2L` arguments on `janusplot()` and `janusplot_data()`.
  With `auto_refit_k = TRUE`, every flagged cell is refit with a
  doubling-k loop until either the flag clears, the per-cell
  unique-x cap is reached, or `k_max_iter` iterations have passed.
  Refit trajectory persisted on each cell: `k_initial`, `k_final`,
  `k_iterations`, `k_at_cap`.
* **Configurable thresholds.** New `k_check_thresholds =
  list(edf_ratio = 0.9, k_index = 1.0, p = 0.05)` argument.
  Defaults track `mgcv::gam.check()` and Wood (2017) §5.9.
* **Console summary.** A 3-line `cli::cli_inform()` summary fires at
  the end of `janusplot()` / `janusplot_data()` whenever at least
  one cell is flagged: total flagged, chance-expected count under
  α = 0.05, and a recovery hint pointing at either
  `auto_refit_k = TRUE` or `k_max_iter`. Quiet on clean datasets.
* **Cell annotation.** New `"k_warn"` entry in the `annotations`
  vocabulary on `janusplot()`. When included, flagged cells render
  a red `!` (ASCII) or `⚠` (Unicode) in the top-left corner.
  Off by default — opt in with `annotations = c("edf", "A", "k_warn")`.
* **Summary-table extension.** `janusplot(..., with_data = TRUE)$data`
  gains 9 new columns: `k_prime`, `k_index`, `k_p`, `k_flag`,
  `k_check_status`, `k_initial`, `k_final`, `k_iterations`,
  `k_at_cap`.
* **RNG isolation.** `mgcv::k.check()` runs its own simulation
  (default `n.rep = 400`) for the basis-deficiency p-value. The
  diagnostic's RNG draw is now isolated via an internal
  seed-preservation helper, so it does not shift downstream Monte
  Carlo consumers — `derivative_ci = "simultaneous"` bands and
  `future.seed = TRUE` reproducibility are unaffected.


# janusplot 0.1.0

## First CRAN release (2026-04-23)

Initial public release of `janusplot`. The package renders a pairwise,
asymmetric smoothed-association matrix of continuous variables, with each
cell showing the fitted spline from an `mgcv` generalised additive model.
Upper-triangle cells plot `gam(x_j ~ s(x_i))`; lower-triangle cells plot
`gam(x_i ~ s(x_j))`. The intentional asymmetry surfaces heteroscedasticity,
leverage, and directional non-linearity that a single scalar correlation
hides.

Public surface includes:

* `janusplot()` — the matrix-plot entry point, with per-cell fit / CI /
  raw-data controls, hclust-ordered variables, random-effects adjustment
  via `s(g, bs = "re")`, and opt-in parallel fits via `future.apply`.
* `janusplot_data()` — sibling returning the fitted-model metadata
  (EDF, F-test p, asymmetry index, shape classification) without
  rendering.
* `janusplot_shape_metrics()` + `janusplot_shape_cutoffs()` +
  `janusplot_shape_hierarchy()` — the 24-category shape taxonomy.
* `janusplot_shape_sensitivity()` + helpers — recovery-rate simulation
  over 12 ground-truth shapes × 7 sample sizes × 500 replicates, with
  the precomputed `shape_sensitivity_demo` dataset for quick inspection.

The remainder of this file documents pre-release development history,
retained for provenance.

---

# janusplot 0.0.0.9001 (development)

### AAGI-preset audit fixes (2026-04-22)

User-visible changes:

* **`data.table` dependency removed.** `janusplot(..., with_data = TRUE)$data`
  and any other tabular returns are now always a plain `data.frame` — no
  runtime or documented fallback to `data.table::as.data.table()`. The
  package charter bans `data.table` as a dependency (plotting function,
  overhead unearned). `data.table` is no longer listed in `Suggests:`.
* **Documentation example cleanup.** The `janusplot()` example no longer
  passes `show_asymmetry = TRUE` (which has been deprecated since
  `0.0.0.9000` and emitted a soft deprecation warning on every example
  render). The default `annotations = c("edf", "A")` already surfaces the
  asymmetry index, so the example is materially unchanged.

Internal / house-style fixes:

* **`match.arg()` → `rlang::arg_match()`** in the two remaining sites
  (`.shape_glyph()` in `R/shape-metrics.R` and `.display_title()` in
  `R/janusplot.R`). Brings every enumerated-argument gate to the same
  `rlang` discipline CONTRIBUTING already documents. Function signatures
  updated so the enum set lives on the formal default (as
  `rlang::arg_match()` requires).
* **`@family` tags added to 7 exports** — `janusplot_shape_cutoffs()`,
  `janusplot_shape_hierarchy()`, `janusplot_shape_metrics()`,
  `janusplot_shape_sensitivity()`, `janusplot_shape_sensitivity_shapes()`,
  `janusplot_shape_sensitivity_summary()`,
  `janusplot_shape_sensitivity_plot()`. The `_pkgdown.yml` reference
  index now organises these two families (`shape-metrics` and
  `shape-sensitivity`) alongside the existing `smooth-associations`
  family, replacing an alphabetical dump.
* **`inst/WORDLIST` extended** with the 24 Phase-F shape-category names
  (`linear_up`, `linear_down`, `convex_up`, `convex_down`, `concave_up`,
  `concave_down`, `s_shape`, `u_shape`, `inverted_u`, `skewed_peak`,
  `broad_peak`, `rippled_peak`, `rippled_monotone`, `rippled_wave`,
  `warped_wave`, `complex_wave`, `bimodal_ripple`, `bi_wave`,
  `bi_wave_ripple`, …). Keeps `devtools::spell_check()` clean.
* **`inst/CITATION` Article bibentry** now carries `email=` and
  `comment = c(ORCID = ...)` for consistency with the Manual bibentry,
  `CITATION.cff`, and `codemeta.json`.
* **Version pins harmonised** across `DESCRIPTION`, `CITATION.cff`, and
  `inst/CITATION` at `0.0.0.9001`.

Deferred to a later maintenance sweep (not blocking 0.0.0.9001):

* `renv.lock` snapshot.
* `covr::package_coverage() > 0.85` CI-side floor assertion.
* `adr/` relocation to `docs/adr/`.

### Legend height tracks matrix height (2026-04-22)

* **Colour-bar flex-fills vertical extent.** Removed the fixed
  `barheight = grid::unit(0.6, "npc")` override inside the
  `guide_colourbar()` for both diverging and sequential scales, and set
  `legend.key.height = grid::unit(1, "null")` in the legend plot's
  theme. The bar now tracks the matrix panel height robustly across
  figure sizes (corrplot-like behaviour), closing the visible
  proportional-scaling mismatch that appeared between narrow and wide
  renders. Added a thin `frame.colour = "grey50"` around the bar for
  visual weight.

### Lifecycle badge SVGs shipped (2026-04-22)

* **`man/figures/lifecycle-*.svg` added.** Running
  `usethis::use_lifecycle()` copied `lifecycle-experimental`,
  `lifecycle-stable`, `lifecycle-superseded`, and
  `lifecycle-deprecated` into `man/figures/`. The HTML help pages for
  every `lifecycle::badge()`-tagged function now render the badge
  instead of the broken-image placeholder (`?` in a box) that appeared
  when the SVG file was absent.

### Derivative views — single-display mode + Simpson 2018 MC CIs (2026-04-22)

* **New `display` parameter on `janusplot()`.** Scalar — one of
  `"fit"` (default, unchanged behaviour for legacy callers),
  `"d1"`, or `"d2"`. Controls which single quantity is rendered in
  every off-diagonal cell of the matrix. A matrix-level title
  (`"Direct fit"`, `"First derivative f'(x)"`, or
  `"Second derivative f''(x)"`) names the displayed quantity. To
  compare fit vs derivative, issue two or three `janusplot()` calls
  and place them side-by-side; each call keeps its own
  `with_data = TRUE` summary table, which now carries a `display`
  column tagging the rendered mode.
* **LP-matrix derivative estimation.** Derivatives computed
  analytically from `mgcv`'s linear-predictor matrix via
  `D %*% coef(fit)` with variance `D %*% Vp %*% t(D)`. The method
  of Wood (2017, §7.2.4), as popularised for GAMs by Simpson (2018,
  *Frontiers in Ecology and Evolution*). No new dependency
  (`gratia` is not required).
* **New `derivative_ci` parameter on `janusplot()` and
  `janusplot_data()`.** One of `"none"` (default), `"pointwise"`,
  or `"simultaneous"`. Default is `"none"` — no CI ribbon is drawn
  on derivative panels unless the caller opts in, because pointwise
  derivative ribbons over-read local features. `"pointwise"` draws
  `fit ± 1.96 * se` from the LP-matrix SE. `"simultaneous"` draws
  Monte Carlo critical-multiplier bands per Simpson (2018): draw
  $\tilde{\boldsymbol\beta}_b \sim N(\hat{\boldsymbol\beta}, V_p)$
  and use the $(1-\alpha)$ quantile of the normalised max-deviation
  statistic as the critical multiplier on the pointwise SE.
* **New `derivative_ci_nsim` parameter.** Integer number of Monte
  Carlo samples used when `derivative_ci = "simultaneous"`. Default
  `1000L` (Simpson 2018 uses 10000; 1000 is a throughput-quantile
  accuracy compromise affordable for medium matrices).
* **New `n_grid` parameter on `janusplot()` and `janusplot_data()`.**
  Prediction-grid resolution. `NULL` (default) resolves to 100 when
  `display = "fit"` and 200 otherwise. Callers may override. Larger
  grids shift the shape-metric values (`M`, `C`, turning /
  inflection counts) slightly because they are computed on this same
  grid — a deliberate side-effect, flagged in `?janusplot` and in
  the Limitations section of the vignette. Shapes and asymmetry are
  the primary matrix reading; M/C/counts are secondary diagnostics.
* **New `derivatives` parameter on `janusplot_data()`.** Integer
  vector of orders in `1:2` (multi-order is allowed — unlike
  `janusplot()`'s scalar `display`, the data companion returns
  arbitrary requested orders in a single call). Every per-pair list
  in the return carries `deriv_yx` and `deriv_xy` named lists keyed
  by order, each a data frame with columns
  `x`, `fit`, `se`, `lo`, `hi`, `ci_type`. When
  `derivative_ci = "simultaneous"` each derivative frame also
  carries a `"crit_multiplier"` attribute.
* **New vignette section "Derivative views: theoretical
  justification and applied use"** — three worked examples (fit /
  d1 / d2) plus a simultaneous-CI demo; covers the LP-matrix
  variance propagation, noise amplification and the rationale for
  the order-2 hard cap, and two applied framings (gain-scheduled
  control; derivative-of-dose-response as the causal estimand of
  Zhang & Chen 2025). BibTeX for the section lives in
  `references/janusplot-derivatives.bib` for re-use in the R
  Journal paper.
* **Validation.** `display` enforces the scalar choice via
  `rlang::arg_match()`; `n_grid < 10` is an error and `n_grid > 500`
  raises an informational note. Orders ≥ 3 are refused by design —
  higher-order derivatives of penalised regression splines amplify
  noise beyond usable signal at realistic sample sizes (Eilers,
  Marx & Durbán 2015).
* **Breaking change from the interim development release.** An
  earlier in-development pattern allowed `display` as a vector
  (`display = c("fit", "d1")`) that stacked panels inside every
  cell. That pattern is removed; the scalar API is the only one
  supported. Callers that reached the stacked-panel intermediate
  must switch to one `janusplot()` call per quantity.

### Label placement — border vs. diagonal (2026-04-22)

* **New `labels` parameter** with three modes: `"border"` (default —
  variable names along the top + left margins, mirroring `corrplot`'s
  `tl.pos = "lt"` convention), `"diagonal"` (previous in-matrix
  layout), and `"none"` (suppressed). Default flipped to `"border"`
  because border labels free the diagonal cells and scale better to
  `k > 4` variables.
* **New `label_srt`** — rotation of top labels when
  `labels = "border"`. Default `45°` matches the visual reference;
  `0` and `90` are accepted.
* **New `label_cex`** — positive multiplier on border-label font
  size. Default `1`.
* Diagonal cells are now rendered as blank bordered panels whenever
  `labels != "diagonal"`, giving the matrix a uniform grid reading.

### Breaking: shape-metric column names (2026-04-21)

* User-facing `M` and `C` columns renamed to `monotonicity_index`
  and `convexity_index` across every data surface: the flat data
  frame from `janusplot(..., with_data = TRUE)`, `janusplot_data()`
  per-pair lists (`monotonicity_index_yx` / `convexity_index_yx` /
  `_xy` variants), the return of `janusplot_shape_metrics()`, the
  raw output of `janusplot_shape_sensitivity()`, and the precomputed
  `shape_sensitivity_demo` dataset. Paper symbols `M` / `C` remain
  as mnemonics in the documentation. Internal classifier parameter
  names (`M`, `C`) are unchanged because they match the math
  convention.
* New "Shape metrics explained" section in the `janusplot`
  vignette.
* Callers referencing `result$M` / `result$C` must update to
  `result$monotonicity_index` / `result$convexity_index`.

### Phase G — sensitivity study as a package feature (2026-04-21)

* **`janusplot_shape_sensitivity()`** — new public function that runs a
  full-factorial shape-recognition sensitivity sweep (shapes ×
  sample sizes × noise levels × replicates) and returns a tidy raw
  data frame. Optional parallel dispatch via `future.apply`.
* **`janusplot_shape_sensitivity_shapes()`** — lists the 14 canonical
  ground-truth shapes available to the sweep.
* **`janusplot_shape_sensitivity_summary()`** — per-cell accuracy
  aggregation at the fine (24-category) or archetype (7-family)
  level.
* **`janusplot_shape_sensitivity_plot()`** — four built-in
  diagnostic plots: fine / archetype confusion matrices, per-shape
  accuracy grid, recovery curves.
* **`shape_sensitivity_demo`** dataset — precomputed 2160-fit sweep
  shipped with the package so the vignette and examples don't need
  to re-run the sweep. Regenerated deterministically via
  `data-raw/shape_sensitivity_demo.R`.
* **New vignette** `shape-recognition-sensitivity.Rmd` — presents the
  sweep design, the pre-registered hypotheses, and every diagnostic
  plot on the precomputed demo dataset.
* `LazyData: true` in DESCRIPTION so `shape_sensitivity_demo` is
  accessible without an explicit `data()` call under default
  user expectations.

### Phase F — post-first-render revisions (2026-04-21)

* **Default correlation = Pearson** (switched from Spearman on the back
  of first-render review). Spearman + Kendall remain as `colour_by`
  options.
* **Colour-bar title simplified to `corr`** for all three correlation
  encodings; actual method remains visible in `janusplot_data()`
  column names and the caption.
* **Taxonomy expanded to 24 categories** via `(T, I)` dispatch. New
  fine categories: `skewed_peak`, `broad_peak`, `rippled_peak`,
  `wave`, `warped_wave`, `rippled_wave`, `complex_wave`, `bimodal`,
  `bimodal_ripple`, `bi_wave`, `bi_wave_ripple`, `rippled_monotone`.
* **Shape-types legend redesigned** — now renders every category as a
  canonical 1-cm thumbnail spline in a 6-column grid below the matrix
  (font-independent, full-width). Each panel is labelled
  `label (code)`. The old right-margin Unicode-glyph list is retired.
* **Cell glyphs off by default.** `annotations` default is
  `c("edf", "A")`; opt into per-cell shape markers with
  `annotations = c(..., "shape")` (glyph) or `annotations = c(..., "code")`
  (2-letter ASCII — safer on any font / PDF pipeline). When both are
  passed, `"code"` wins.
* **Hierarchy columns added to every output.** `janusplot(..., with_data)$data`
  gains `shape_code`, `shape_archetype`, `shape_monotonic`,
  `shape_linear`. `janusplot_data()` pairs gain per-direction
  `shape_code_yx` / `shape_code_xy` and the matching
  `shape_archetype_*`, `shape_monotonic_*`, `shape_linear_*`.
* **New public function `janusplot_shape_hierarchy()`** — returns the
  24-row taxonomy with hierarchy columns (`category`, `code`,
  `archetype`, `monotonic`, `linear`, `label`, `gloss`). Intended for
  downstream group-bys and for cross-referencing the compact 2-letter
  codes when they appear in cells or the data table.
* **Classifier is noise-robust.** Sign-change detection now uses
  lobe-mass-weighted accounting, so tail-noise wiggles on a smoothed
  saturating curve no longer inflate the inflection count into
  `complex`.

**Academic framing.** The broader-tier vocabulary (linear /
non-linear, monotone / non-monotone, convex / concave) is standard
calculus; the archetype layer is anchored by Pya & Wood (2015)
*Stat & Comput* (shape-constrained additive models) and Calabrese
(2008) *Env Tox Chem* (dose-response taxonomy). The `(T, I)` dispatch
is a coarsened Morse-theoretic critical-point classification
(Milnor 1963).

### Breaking / behaviour changes (2026-04-21 cell encoding redesign)

* **Default cell colour now encodes Spearman rank correlation.** The
  per-cell fill previously encoded EDF (non-linearity). It now encodes
  classical monotonic correlation via the new `colour_by` argument
  with a diverging `RdBu` palette symmetric around zero. Choose
  `colour_by = "pearson"` / `"kendall"` for other correlation flavours
  or `colour_by = "edf"` to restore the legacy encoding.
* **`fill_by` is deprecated** — use `colour_by`. When supplied,
  `fill_by` fires a soft deprecation warning and forwards to
  `colour_by` for one minor version.
* **Corner annotations switched to `annotations`** — a character
  vector (subset of `c("edf", "A", "shape")`) now controls which
  corner labels render on each cell. `"A"` (asymmetry index) replaces
  the old `n = ...` annotation; `"edf"` keeps EDF visible as text;
  `"shape"` draws the new shape-category glyph bottom-right.
* **`show_asymmetry` is deprecated** — use `annotations`. When
  supplied, the legacy argument fires a soft deprecation warning and
  is merged into `annotations`.

### New features

* **Objective shape descriptor** — every cell now carries two
  continuous indices (monotonicity `M`, convexity `C`), two discrete
  counts (`n_turning_points`, `n_inflections`), and a discrete
  `shape_category` from a 12-category taxonomy (`linear_up`,
  `linear_down`, `convex_up`, `concave_up`, `convex_down`,
  `concave_down`, `u_shape`, `inverted_u`, `s_shape`, `complex`,
  `flat`, `indeterminate`). Cutoffs are tunable via
  `janusplot_shape_cutoffs()`.
* **`janusplot_shape_metrics()`** — public function to compute the
  shape descriptor for any fitted `mgcv::gam` with a single `s()`
  term.
* **`janusplot_shape_cutoffs()`** — public function returning the
  default threshold list; callers override individual thresholds via
  `...`.
* **Shape-types legend** — when `annotations` includes `"shape"`, a
  compact legend listing every category present in the matrix is
  attached alongside the colour bar. Toggle with `show_shape_legend`.
* **Unicode / ASCII glyph switch** — new `glyph_style` argument
  (`"unicode"` default, `"ascii"` fallback) for pipelines that lack
  Unicode curve glyphs.
* **Extended data table** — `janusplot(..., with_data = TRUE)$data`
  gains columns `cor_pearson`, `cor_spearman`, `cor_kendall`,
  `tie_ratio`, `M`, `C`, `n_turning_points`, `n_inflections`,
  `flat_range_ratio`, `shape_category`, `colour_value`. The legacy
  `fill_value` column is renamed `colour_value`.
* **Extended `janusplot_data()` pairs** — each per-pair element gains
  Pearson / Spearman / Kendall correlations, tie ratio, and
  per-direction shape descriptors (`M_yx`, `C_yx`, `M_xy`, `C_xy`,
  `n_turning_*`, `n_inflect_*`, `shape_yx`, `shape_xy`).

## janusplot 0.0.0.9000

### New features

* `janusplot()` — asymmetric smoothed-association matrix visualisation.
  For each pair of numeric variables `(X_i, X_j)` with `i != j`, the
  cell at matrix position `[i, j]` renders the fitted spline from
  `mgcv::gam(X_j ~ s(X_i) + <adjust>)`. Upper and lower triangles show
  the two directional fits. Diagonal cells carry variable labels.
* `janusplot_data()` — programmatic companion returning raw GAM fits
  and per-pair metrics (EDF, F-test p-value, deviance explained,
  asymmetry index) without constructing a ggplot. Set
  `keep_fits = TRUE` to retain the full `mgcv::gam` objects.
* **Asymmetry index** — the per-pair single-number summary
  `|EDF_yx − EDF_xy| / (EDF_yx + EDF_xy)`, bounded in `[0, 1]`,
  exposed both via `janusplot_data()$pairs[[i]]$asymmetry_index` and
  (optionally) as a cell corner annotation via `show_asymmetry = TRUE`.
* **Partial smooths via `adjust =`** — any one-sided formula RHS
  (e.g. `~ s(z) + s(g, bs = "re")`) propagates to every pairwise
  GAM, producing covariate-adjusted smooths and supporting random
  effects out of the box.
* **`order = "hclust"`** — reorder variables by hierarchical
  clustering of `1 - |cor|` (matching the `corrplot` convention) to
  group strongly correlated variables visually.
* **`na_action = "pairwise"`** (default) vs `"complete"` — per-pair
  complete observations with annotated `n_used` per cell, vs listwise
  deletion.
* **Parallelism via `future.apply`** — set `parallel = TRUE` to
  dispatch pair fits across a user-configured `future::plan()`.
* **Colour palette choice** — `palette =` accepts one of 16 options:
  - viridis family (colourblind-safe sequential): `viridis` (default),
    `magma`, `inferno`, `plasma`, `cividis`, `mako`, `rocket`;
  - viridis high-contrast: `turbo` (not colourblind-safe);
  - ColorBrewer diverging (colourblind-safe): `RdYlBu`, `RdBu`, `PuOr`;
  - ColorBrewer diverging (not colourblind-safe): `Spectral`;
  - ColorBrewer sequential (colourblind-safe): `YlOrRd`, `YlGnBu`,
    `Blues`, `Greens`.
* **Shared right-margin colourbar legend** when `fill_by != "none"`,
  placed in a dedicated fixed-width column (1.8 cm) so cells remain
  square.
* **Cell annotations scale with `k`** — `n` and `EDF` labels shrink
  sublinearly as the number of variables grows, keeping small-cell
  plots legible.
* **Glossary caption** — a dynamic caption below the matrix explains
  the on-plot abbreviations (`n`, `EDF`, `A`, fill encoding, and
  significance glyphs), showing only the keys actually displayed.
* **`with_data = TRUE`** — optional flat per-cell summary table
  returned alongside the ggplot, as a `data.table` when the package
  is installed or a `data.frame` otherwise.

### Testing and quality

* 70 unit + integration tests; 5 `vdiffr` visual-regression snapshots.
* Passes `R CMD check --as-cran` with 0 ERRORs, 0 WARNINGs; 2 NOTEs
  remain (both environmental: new-submission status; local HTML Tidy
  version).
* Three-scenario simulation study validating the approach — linear
  recovery, non-linear detection, and heteroscedastic asymmetry.
  Scenario 3 is the paper's headline result: under a DGP with
  Pearson `r = 0.93`, `janusplot` recovers an asymmetry index ≈ 0.56
  (IQR [0.52, 0.65]), exposing hidden directional structure a
  scalar correlation misses.

### Dependency diet

Imports kept minimal: `mgcv`, `ggplot2`,
`patchwork`, `grid`, `stats`, `cli`, `rlang`. Optional: `data.table`,
`future.apply`, `vdiffr`, `withr`, `palmerpenguins`, `MASS`,
`agridat`, `knitr`, `rmarkdown`.
