---
title: "Adding a New Module"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Adding a New Module}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
    collapse = TRUE,
    comment = "#>",
    eval = FALSE
)
```

This checklist walks through how to add a new plotting module to **VizModules** so it matches the package's organization, documentation, and testing standards.

**VizModules** modules are designed to be a joy to use, which requires some discipline to implement in a consistent way. This checklist may seem daunting at first glance, but most of the items already have helpers to implement them. 

And a fair few are to keep you from shooting yourself in the foot and avoiding most of the common module pitfalls.

If you are working with an AI coding agent, the package ships an
[Agent Skill](https://agentskills.io) covering exactly this checklist -
**`vizmodules-new-module`** - along with the required roxygen sections, the uniform input
helpers, the traps that have cost real time here, and file templates to copy. Install it
(and its two siblings) into your project with `VizModules::use_vizmodules_skills(".")`, or
pass `client = "copilot"` / `client = "claude"` to write to `.github/skills/` or
`.claude/skills/` instead of the default `.agents/skills/`. The skills live in
`inst/skills/` in this repository, so **keep them in step with this vignette** when you
change a convention it documents.

## Quick Checklist

- [ ] Pick the plot function you are wrapping and name your module accordingly:
  - For **dittoViz** functions: use `dittoViz_<PlotName>` (e.g., `dittoViz_scatterPlot` for `dittoViz::scatterPlot`)
  - For **plotthis** functions: use `plotthis_<PlotName>` (e.g., `plotthis_AreaPlot` for `plotthis::AreaPlot`)
  - For **custom/standalone** functions: use the plot name directly (e.g., `linePlot`, `piePlot`)
- [ ] If adding a brand-new plotting function (e.g., `piePlot`), define and document that plot function first, then wrap it with the module.
- [ ] Create the three core files: `R/<moduleName>_module_ui.R`, `R/<moduleName>_module_server.R`, and `R/<moduleName>_module_app.R`.
- [ ] Document UI, server, and app functions with roxygen (`@export`, params, examples).
- [ ] In the UI function docstring, add **three required sections**:
  - `@section Plot parameters not implemented or with altered functionality:` - List inputs not exposed and why
  - `@section Plot parameters and defaults:` - Document all exposed parameters with UI labels and defaults
  - `@section Plot parameters implementing new functionality:` - Document any new or plotly-specific controls (axes, ticks, reference lines, etc)
- [ ] Add an example app that uses the module twice to prove multi-instance behavior.
- [ ] Wire up reactive `defaults` support so parent apps can drive parameters from app state (see [Supporting Reactive Defaults]).
- [ ] Freeze any input you update from the server so the plot does not render twice (see [Updating Your Own Inputs From the Server]).
- [ ] Debounce any free-text input the plot reads, so a `textInput()` does not rebuild the plot once per keystroke (see [Debouncing Free-Text Inputs]).
- [ ] Wire up manual layout-edit persistence so user-dragged titles, legends, annotations, and colorbars survive re-renders (see [Persisting Manual Layout Edits]).
- [ ] Cover the base plotting function with `testthat`; cover the module/app with `testServer` where feasible.
- [ ] Note any `ggplotly` conversion quirks that change or drop functionality.
- [ ] **Never use `eval(str2expression())` on raw user input.** Use `safe_eval_filter()`, `validate_expression()`, or `safe_resolve_adj_fxn()` instead (see [Sanitizing User-Provided Expressions] below).
- [ ] If you write any CSS, anchor every selector on a class this package owns — a stylesheet goes into the host app's document unscoped (see [Styling and CSS Containment]).

## Naming & Organization

- [ ] File names follow the module naming pattern:
  - **dittoViz wrappers**: `dittoViz_<PlotName>_module_ui.R` (e.g., `dittoViz_scatterPlot_module_ui.R`)
  - **plotthis wrappers**: `plotthis_<PlotName>_module_ui.R` (e.g., `plotthis_AreaPlot_module_ui.R`)
  - **Custom modules**: `<plotName>_module_ui.R` (e.g., `linePlot_module_ui.R`)
- [ ] Function names follow the pattern: `<moduleName>InputsUI()`, `<moduleName>OutputUI()`, `<moduleName>Server()`, `<moduleName>App()`.
  - Examples: `plotthis_AreaPlotInputsUI()`, `dittoViz_scatterPlotServer()`, `linePlotApp()`
- [ ] Internal helpers stay in existing helper files when broadly useful; otherwise keep them inside the module file.

## Documentation Standards

- [ ] Roxygen headers include title, description, parameters, return value, authors, and `@export`.
- [ ] Examples show minimal, runnable usage with a small dataset.
- [ ] **The UI function must include three documentation sections:**

### 1. `@section Plot parameters not implemented or with altered functionality:`

List all parameters from the base plot function that are **not** exposed via UI inputs, with explanations:

```r
#' @section Plot parameters not implemented or with altered functionality:
#' The following [plotthis::AreaPlot()] parameters are not available via UI inputs:
#' \itemize{
#'   \item \code{xlab} - X-axis label (plotly allows interactive editing)
#'   \item \code{ylab} - Y-axis label (plotly allows interactive editing)
#'   \item \code{title} - Plot title (plotly allows interactive editing)
#'   \item \code{subtitle} - Plot subtitle (not supported in plotly)
#'   \item \code{legend.position} - Legend positioning (plotly allows interactive repositioning)
#'   \item \code{split_by} - Split variable (returns a patchwork object, not supported in plotly)
#'   \item \code{palette} - Managed internally via the palette selection UI
#' }
```

### 2. `@section Plot parameters and defaults:`

Document all parameters that **are** exposed, listing their UI label and default value:

```r
#' @section Plot parameters and defaults:
#' The following [plotthis::AreaPlot()] parameters can be accessed via UI inputs and/or the \code{defaults} argument:
#' \itemize{
#'   \item \code{x} - X-axis variable (UI: "X values", default: 2nd categorical variable)
#'   \item \code{y} - Y-axis variable (UI: "Y values", default: 2nd numeric variable)
#'   \item \code{group_by} - Grouping variable (UI: "Group by", default: 3rd categorical variable or "")
#'   \item \code{facet_by} - Faceting variable (UI: "Facet by", default: "")
#'   \item \code{theme} - ggplot2 theme (UI: "Theme", default: "theme_this")
#'   \item \code{alpha} - Area fill transparency (UI: "Alpha", default: 1)
#' }
```

### 3. `@section Plot parameters implementing new functionality:`

Document all module-specific parameters (plotly controls, reference lines, etc.):

```r
#' The following parameters implementing new functionality or controlling plotly-specific features are also available:
#' \itemize{
#'   \item \code{axis.font.size} - Axis title font size (UI: "Axis font size", default: 18)
#'   \item \code{axis.showline} - Show axis border lines (UI: "Show axis lines", default: TRUE)
#'   \item \code{axis.tickfont.size} - Size of tick labels (UI: "Tick label size", default: 12)
#'   \item \code{hline.intercepts} - Y-coordinates for horizontal reference lines (UI: "Y-intercepts", default: "")
#'   \item \code{hline.colors} - Colors for horizontal lines, comma-separated (UI: "Colors", default: "#000000")
#'   \item \code{hline.linetypes} - Line types for horizontal lines, comma-separated (UI: "Line types", default: "dashed")
#'   \item \code{vline.intercepts} - X-coordinates for vertical reference lines (UI: "X-intercepts", default: "")
#'   \item \code{abline.slopes} - Slopes for diagonal reference lines (UI: "Slopes", default: "")
#' }
```

**Note:** Reference line parameters (`hline.*`, `vline.*`, `abline.*`) accept comma-separated values to control each line individually.

## Functionality & Non-Exposed Inputs

- [ ] For each plot function argument, decide: expose, set a fixed default, or drop.
- [ ] If dropped or fixed, document it inside the UI function (description + reason).
- [ ] If `ggplotly` alters or drops a feature (e.g., certain geoms, annotations), note that limitation in the UI docs so users know what to expect.

## Example App Requirement

- [ ] Provide an app in `<plot>_module_app.R` as a thin wrapper around [createModuleApp()]:

```{r example-app}
myPlotApp <- function(data_list = NULL, defaults = NULL, hide.inputs = NULL, hide.tabs = NULL) {
    # With no data, open on the module's showcase example (its entry in
    # .module_showcase()), layering any caller defaults over its own.
    if (is.null(data_list)) {
        example <- .module_example("myplot")
        data_list <- example$data_list
        defaults <- utils::modifyList(example$defaults, defaults %||% list())
    }
    createModuleApp(
        inputs_ui_fn = myPlotInputsUI,
        output_ui_fn = myPlotOutputUI,
        server_fn    = myPlotServer,
        data_list    = data_list,
        defaults     = defaults,
        hide.inputs  = hide.inputs,
        hide.tabs    = hide.tabs,
        title        = "Modular myPlots"
    )
}
```

- [ ] `createModuleApp()` already handles validation, data import, data filtering, and dataset switching — no need to duplicate that logic.
- [ ] Open the `*App()` on the module's showcase example when no `data_list` is given, as the other `*App()` functions do: `example <- .module_example("<id>")`, then use `example$data_list` and layer the caller's `defaults` over `example$defaults` (see "Gallery App and Figure Builder" below).

## Testing Requirements

- [ ] `testthat`: cover the base plotting function's core behavior and arguments (data handling, grouping, palette handling, etc.).
- [ ] `testServer`: cover the module/app where feasible.
- [ ] Place tests under `tests/testthat/` with clear file names (`test-<plot>.R`, `test-<plot>-app.R`).
- [ ] Ensure tests run headless and deterministically (seed randomness where needed).
- [ ] For new plotting functions, add dedicated `testthat` coverage of the plotting helper itself (input validation, defaults, edge cases) in addition to the module tests.

## Implementing a New Plotting Function (e.g., `piePlot`)

- [ ] Add the plotting function under `R/` with full roxygen docs, inputs/returns, and examples.
- [ ] Keep arguments consistent with existing plot functions (data first, `...` last, palette/palcolor patterns).
- [ ] Document any assumptions about input shape (e.g., pre-summarized table for pies).
- [ ] Add `testthat` coverage for the plotting function (happy paths + invalid inputs).
- [ ] Only after the plotting function is stable, build the module UI/server/app wrappers around it.
- [ ] If you pass an array to a trace by value (such as `error_y = list(array = ...)`) on a plot coloured by a discrete column, sort the data by that column first. plotly re-sorts its own copy by the colour column before splitting it into traces, but not your array, so otherwise each series is drawn with other series' values. `linePlot()` does this with `.group_rows_by_trace()`; test it on the built figure, with colour columns stored as character, factor and ordered factor.
- [ ] Inside `summarise()`/`mutate()`, a data column named like one of your arguments (`y`, say) masks it. Resolve what the verb needs into local variables before calling it, and test with a data frame whose columns are literally named `x` and `y`.

## Supporting Reactive Defaults

An entry in `defaults` may be a `reactive()` rather than a fixed value, so a
parent app can make an input follow its state without the double render that
`update*Input()` causes (see
`vignette("defaults-and-hiding", package = "VizModules")`). Two lines wire this
up, and they are required in every new module.

- [ ] Make the first statement of your `moduleServer()` body build the parameter
  store:

    ```r
    params <- setup_reactive_defaults(defaults, input, session)
    ```

- [ ] Pass that store to `setup_auto_update_logic()` inside your generate
  reactive:

    ```r
    generate_myPlot <- reactive({
        isolate_fn <- setup_auto_update_logic(input, params)
        ...
        main = isolate_fn(input$main)
    })
    ```

Easy. `setup_reactive_defaults()` returns `NULL` when no
entry is reactive, in which case `isolate_fn` is the plain `identity()`/`isolate()`
it has always been. When a store is present, `isolate_fn` recognises direct
`input$<key>` reads and resolves them from the store instead, so your existing
read sites need no edits, as long as they stay in the
`isolate_fn(input$<key>)` form. A wrapped read such as
`isolate_fn(as.numeric(input$size))` cannot be recognised and will not support a
reactive default; do the conversion outside the call instead.

Your `*InputsUI()` and reset observer need no special handling: both go through
`get_default()`, which resolves reactive entries on its own. Reset therefore
restores the reactive's current value.

## Updating Your Own Inputs From the Server

Modules often derive a value on the server and push it back into one of their
own controls, e.g. an auto-calculated y-axis range, a regenerated list of stat
comparison pairs, a rebuilt colour picker. `update*Input()` is an asynchronous
round-trip to the browser, so the plot renders **twice**: once immediately with
the stale value, then again when the client echoes the new one. This is annoying as hell and can be avoided by freezing the input before you update it:

- [ ] Call `freezeReactiveValue()` on the input immediately before you update it:

    ```r
    observeEvent(input$stat.x, {
        pairs <- generate_pair_strings(data(), input$stat.x)
        if (length(pairs) > 0) {
            freezeReactiveValue(input, "stat.pairs")
            update_viz_select(session, "stat.pairs",
                choices = c("", pairs), selected = .default_stat_pairs(defaults, pairs)
            )
        }
    })
    ```

Freezing pauses everything that reads that input until the real value arrives,
so the intermediate render never happens. Three rules:

- **Freeze only when you are definitely going to update.** A frozen input that
  never receives a value leaves the plot suspended. Keep the freeze inside the
  same `if` branch as the `update*Input()` call.
- **Do not add a catch-all `tryCatch()` to your `renderPlotly()`.** The pause is
  delivered as a silent condition; swallowing it turns a clean pause into a blank
  plot. Guard with `req()` and explicit `if` branches instead.

This does not apply to the reset observer, where a burst of updates is expected.

### Inputs Rebuilt by `renderUI()`

Freezing does **not** cover an input your module rebuilds with `renderUI()`, such
as a [multiColorPicker()] whose groups follow the data. A freeze pauses only the
readers that run *after* it in that flush, and at startup the plot output runs
first. The freeze lands too late to pause anything, and the value the freshly
built input reports then rebuilds the plot for a mapping it was already drawing.

In short, this results in the plot being re-rendered one or more times in a way 
that can be annoying, particularly for large data sets.

Give the plot a server-side value to read instead, so the client's echo is
compared against what is already in use rather than against `NULL`. For a colour
picker, `setup_group_colors()` does this for you — it resolves the mapping as
soon as the group set is known and holds it in a `reactiveVal()`, which only
invalidates on a real change:

- [ ] Create the store beside your group-levels reactive:

    ```r
    palette_store <- setup_group_colors(
        input, "palette.colours", palette_groups,
        default_palette_values, defaults, params
    )
    ```

- [ ] Seed it inside the picker's `renderUI()`, with the same colours the input
  itself is built from, so the mapping is right even when the render was
  deferred (a picker on a hidden tab is suspended until that tab is opened):

    ```r
    initial_colors <- isolate(resolve_palette(
        groups, input$palette.colours, default_palette_values,
        .default_group_colors(defaults, "palette.colours")
    ))
    palette_store(initial_colors)
    ```

- [ ] Read the store in the plot reactive, in place of the raw input:

    ```r
    palette_values <- isolate_fn(palette_store())   # not isolate_fn(input$palette.colours)
    ```

The same shape works for any `renderUI()`-rebuilt input: resolve the value on
the server, hold it in a `reactiveVal()`, and have the plot read that.

### Axis Limits

Limits behave the same way, and for the same reason: the module derives them on
the server, pushes them into the `y.min`/`y.max` controls, and the echo of that
push rebuilds the plot. `setup_axis_range()` is the store for them.

- [ ] Create the store, then seed it beside every `update*Input()` that sets the
  limits — the y-data observer and the Reset button:

    ```r
    y_range_store <- setup_axis_range(input, session, params = params)

    observeEvent(input$y.data, {
        y_range <- .calculate_range(df = data(), data_col_y = input$y.data,
                                    axis_scale_factor = .y_axis_scale_factor)
        if (!is.null(y_range)) {
            y_range_store(list(min = y_range$min, max = y_range$max))
            updateNumericInput(session, "y.min", value = y_range$min)
            updateNumericInput(session, "y.max", value = y_range$max)
        }
    })
    ```

- [ ] Read `isolate_fn(y_range_store())` in the plot reactive, in place of
  `isolate_fn(input$y.min)` and `isolate_fn(input$y.max)`.

If your module draws significance brackets, pass `headroom` as well. The
brackets are stacked above the data, so the limits have to clear them or they
are drawn clipped; `stat_bracket_y_max()` works out how high they will reach,
and the store raises the maximum to meet it and updates the control to match.
It only ever raises, so a larger limit the user chose is left alone:

```r
y_range_store <- setup_axis_range(
    input, session, params = params,
    headroom = function() {
        if (!isTRUE(input$stats.enabled)) {
            return(NULL)
        }
        .stat_bracket_headroom(
            df = data(), x = input$x.data, y = input$y.data,
            group.by = .blank_to_null(input$group.by),
            facet.by = .blank_to_null(input$facet.by),
            per.facet = isTRUE(input$stat.per.facet),
            input = input
        )
    }
)
```

Pass the resolved limits on to `apply_stat_annotations()` too, as `y.min` and
`y.max`. It has the last word on the drawn range, and knowing what you asked for
is what lets it leave a large maximum alone rather than shrinking the axis onto
the brackets.

## Debouncing Free-Text Inputs

The re-render problems above come from the *server* pushing values at the
client. Free text has the opposite problem: `textInput()` and `textAreaInput()`
report to the server on **every keystroke**, so a plot that reads one directly
is rebuilt once per character.

That matters most when the input is an expression, because the intermediate
states are not just wasted work, they are mostly *invalid* work. Typing
`condition == "Disease"` walks through `c`, `co`, `con`, ... and every one of
those is a complete render cycle spent on an expression that cannot parse.

Wrap the read in `shiny::debounce()` so a burst of typing collapses into one
render once the user pauses:

```{r debounce}
# Once, in the module server body -- not inside a reactive.
filter_text <- debounce(reactive(input$row_filter), 700)

filtered <- reactive({
    keep <- safe_eval_filter(filter_text(), data())
    if (is.null(keep)) data() else data()[keep, , drop = FALSE]
})
```

`debounce()` emits its initial value immediately and then holds the previous
value while typing, so startup is unaffected and there is no blank first render
to design around.

- [ ] Debounce any `textInput()`/`textAreaInput()` a plot reads, especially one
  holding an expression. 500-800ms is a good range; the more expensive the plot,
  the longer the delay earns its keep.
- [ ] Leave select, numeric, checkbox, and slider inputs alone. They report
  discrete choices rather than keystrokes, so there is nothing to collapse.
- [ ] Create the debounced reactive **once** in the server body. Creating it
  inside another reactive rebuilds the timer on every invalidation, which
  defeats it.
- [ ] Debouncing is not a substitute for the Auto Update tack. The tack lets the
  user opt out of live updates entirely; debouncing makes live updates bearable
  for the user who leaves it on.

`ComplexHeatmap_HeatmapServer()`'s Row/Column Filter inputs are the worked
example — a heatmap is drawn to a device and measured before it can be shown, so
a per-keystroke rebuild is especially painful there.

## Persisting Manual Layout Edits

Every VizModules plot is interactively editable: users can drag the legend,
reposition or re-text annotations, drag the (draggable) axis titles to their heart's content. 

Because each module rebuilds its figure from scratch on every input change, 
those hand-made tweaks would be lost on the next re-render unless they are captured and re-applied.

Two exported helpers handle this for you. 
Use them in **every** new module so the behaviour is available from the start.

### Server

- [ ] Near the top of your `moduleServer()` body, create a unique plotly event
  source for this module instance and the edit store:

    ```r
    # Unique source so each module instance captures only its own events.
    plot_source <- session$ns("myplot")
    edit_store <- setup_manual_edits(input, session, plot_source)
    ```

- [ ] In your `renderPlotly()`, pass the freshly built figure through
  `finalize_manual_edits()` immediately before returning it:

    ```r
    output$myPlot <- renderPlotly({
        req(input$x, input$y)
        fig <- generate_myPlot()  # your function that builds the plotly figure
        finalize_manual_edits(fig, plot_source, edit_store, session)
    })
    ```

Pretty simple. 

`setup_manual_edits()` registers the observers that capture `plotly_relayout` events (legend, annotation, and axis-title drags)
plus the JavaScript-forwarded colorbar drag; `finalize_manual_edits()` tags the
figure with the event source, restores any captured edits, records the figure for
stable annotation keying, and re-attaches the colorbar listener.

Edits are matched to annotations by a content-derived key, 
so they survive even when annotations are
added, removed, or reordered between rebuilds 
(e.g. when statistical brackets or
reference labels appear).

### Key helpers (all in `R/plot_helpers.R`)

| Function | Purpose |
|---|---|
| `setup_manual_edits()` | Create the edit store and register the relayout/colorbar capture observers (call **once**, near the top of the server) |
| `finalize_manual_edits()` | Tag the event source, restore captured edits, record the figure, and attach the colorbar listener (call in `renderPlotly()`, just before returning) |

Both functions are exported, so the same two-step pattern works from a custom
module in your own package. The supporting internals (`.capture_manual_edits()`,
`.reapply_manual_edits()`, `.add_colorbar_listener()`) are not exported and shouldn't 
need to be used directly (though you can always access them via `VizModules:::` if really necessary.

See any module server (e.g. `dittoViz_scatterPlotServer`) for a complete example.

## Integrating Statistical Testing (Stats Tab)

Modules for categorical-vs-numeric plots (box, violin, etc.) can include an optional **Stats** tab that provides pairwise statistical testing with plotly bracket annotations. If your new module supports grouped comparisons along a categorical x-axis, follow this pattern:

### UI

- [ ] Add a `"Stats"` tab to the module's `tabsetPanel` containing `.uniform_stats_inputs_ui(ns, defaults)`.

### Server

- [ ] Create a `reactiveVal` to store the last computed stats table: `last_stats_df <- reactiveVal(NULL)`.
- [ ] When `input$stats.enabled` is TRUE in the plot rendering block, call `compute_pairwise_stats()` to run tests, then `create_stat_annotations()` to build bracket shapes/annotations, then `apply_stat_annotations()` to append them to the plotly figure.
- [ ] Store the result: `last_stats_df(stats_df)`.
- [ ] Add an `observeEvent` to update `stat.pairs` choices when the x or grouping column changes, using `generate_pair_strings()`, selecting `.default_stat_pairs(defaults, pairs)` so a `stat.pairs` default is honoured.
- [ ] Call `.reset_stats_inputs(session, defaults, pairs)` in the reset observer, with the pairs currently on offer.

### Key helpers (all in `R/stat_helper.R`)

| Function | Purpose |
|---|---|
| `compute_pairwise_stats()` | Run pairwise or omnibus tests with p-value adjustment |
| `create_stat_annotations()` | Convert stats to plotly shapes/annotations with bracket packing |
| `apply_stat_annotations()` | Append shapes/annotations to the plotly figure and adjust y-axes |
| `generate_pair_strings()` | Build `"A vs B"` strings for the comparison selector |
| `parse_pair_strings()` | Convert selected pair strings back to list of length-2 vectors |

See the `plotthis_BoxPlotServer`, `dittoViz_yPlotServer`, or `dittoViz_freqPlotServer` implementations for complete integration examples.

## Gallery App and Figure Builder

- [ ] Register the module in `.module_showcase()` (`R/module_showcase.R`). That one entry gives it a tab in the gallery (`moduleGalleryApp()`), a place in the Figure Builder, and the example its `*App()` opens on. It holds the module's three functions, a `label` and a shorter `tab_label`, the `.example_datasets()` entry it is shown on, and the `defaults` it opens with.
- [ ] Make those `defaults` show the module off: switch on its distinctive features (statistics, highlights, fit lines, annotations, splits), not just the column mapping. If no bundled dataset gives a feature anything to show, extend one in `data-raw/generate_example_data.R` rather than leaving the feature off.
- [ ] `tests/testthat/test-showcase.R` checks every entry's defaults name real columns and are accepted by the controls they seed; add an assertion there for any data effect your demo depends on (e.g. that a showcased comparison is significant).
- [ ] Verify namespacing: each module instance should have a unique `id` and independent state.
- [ ] Keep dependencies minimal (prefer built-in or generated datasets).
- [ ] If the module's output is not a plotly graph, give it a `vector_svg` and a `raster_png` function of `(width, height, res)` so its panels appear in the Figure Builder's SVG export and its source download carries images; `draw_to_svg()` and `draw_to_png()` build them from any grid or base drawing. Put them on the summary list the server's reactive returns (the source download reads them there), and attach `vector_svg` to the reactive as an attribute as well (the Figure Builder's canvas export reads it there). See `ComplexHeatmap_HeatmapServer()`.

## Review Before Submitting

- [ ] Run `devtools::document()` to update NAMESPACE and Rd files.
- [ ] Run `devtools::check()` and ensure tests pass locally.
- [ ] Confirm UI text/tooltips mention any missing or altered plot features.
- [ ] Verify both module instances in the example app work independently (namespacing correct).
- [ ] If you added CSS, render the module beside a stock `selectInput()`, `sidebarPanel()` and `DT::datatable()` and confirm none of them changed (see [Styling and CSS Containment]).

## Style Guide

Following a consistent style makes the package easier to read, maintain, and extend. Apply these conventions to every new module.

### Input Labels

- **Capitalize** the first word of every input label: `"Group By"`, not `"group by"`.
- **Be concise** — prefer short, scannable labels over long descriptions. Move detail into a `tipify` tooltip instead.
- **Avoid redundant words.** `"Color"` is better than `"Select a Color"`.
- **Match plotthis/dittoViz parameter names loosely**, so users can cross-reference the upstream docs. E.g., label the `group_by` input `"Group By"`.

### Select Inputs

Use `viz_select_input()` rather than `shiny::selectInput()` or `shiny::selectizeInput()`, and `update_viz_select()` in place of their `update*()` counterparts. It takes the same `inputId`/`label`/`choices`/`selected`/`multiple` arguments, but renders a virtualised dropdown, so an input backed by a column with tens of thousands of distinct values stays usable. A search box appears automatically once there are more than ten choices.

An empty-string choice still means "no selection"; it is displayed as `(none)` so users can see and pick it.

```r
viz_select_input(ns("group.by"), "Group By",
    choices = cat.choices,
    selected = get_default(defaults, "group.by", "", function(x) x %in% cat.choices)
)
```

### Tooltips with `tipify`

Wrap any non-obvious input in `shinyBS::tipify()` to show a tooltip on hover. This keeps labels concise while still informing the user.

Apply `tipify` when:

- The input's purpose is not immediately clear from its label alone.
- The input accepts a specific format that users might not guess (e.g., comma-separated values, index positions for categorical axes).
- The input has a non-trivial effect on the plot (e.g., stat correction methods, bracket inset).

Standard pattern — always use `placement = "top"` and `options = list(container = "body")` so tooltips render correctly inside sidebar panels:

```r
tipify(
    textInput(ns("hline.intercepts"), "Y-intercepts",
        placeholder = "e.g. 2, -2",
        value = get_default(defaults, "hline.intercepts", "")
    ),
    paste(
        "For categorical or factor axes, enter the index (position) of the",
        "category rather than its name."
    ),
    placement = "top", options = list(container = "body")
)
```

Inputs that are self-explanatory from their label (e.g., `"Plot Title"`, `"X-axis Variable"`) do not need a tooltip.

### Reuse Uniform Input Helpers

In time, these helpers will be further formalized and exported, 
but they can be used with the `VizModules:::` prefix in the meantime.

Before writing custom inputs, check whether a uniform helper already covers your needs:

| Helper | Provides |
|---|---|
| `uniform_lines_inputs_ui()` | Horizontal, vertical, and diagonal reference line controls |
| `uniform_axes_inputs_ui()` | Font, axis border, gridline, tick, and facet styling |
| `.uniform_stats_inputs_ui()` | Pairwise statistical testing and bracket annotation controls |
| `uniform_plotly_inputs_ui()` | Download buttons, margins, subplot spacing, and draw-shape styling |
| `uniform_legend_inputs_ui()` | Legend visibility, font family and colour, and title and entry label font sizes |
| `uniform_annotation_inputs_ui()` | Highlighting and labelling of individual data points |

Each UI helper has a matching `reset_*_inputs()` function to call from the module's
`observeEvent(input$reset, ...)` block.

Modules that draw individual points can adopt `uniform_annotation_inputs_ui()` to let users
highlight and label points by the values of a chosen column. The server side needs the chosen
column carried in the plot's hover text (that is where the values are read back from), then
`.apply_highlight_styling()` to restyle matching markers and
`.create_highlight_annotations()`/`.create_selected_annotations()` to build the labels.
Set `require.markers = TRUE` when other scatter traces are drawn from the same data
(box or violin outlines, say) so only the point markers are matched. Append the resulting
annotations to `fig$x$layout$annotations` rather than replacing them, or facet strip labels
and statistical brackets will be lost.

Pass `ns` and a `defaults` list to each helper. Use the `include.*` arguments to opt in to optional groups (e.g., `include.fit.lines = TRUE` for scatter plots, `include.rotate = TRUE` for bar plots).

Using the uniform helpers ensures that shared inputs behave identically across every module and that future changes to those helpers propagate automatically.

### Imports: `@importFrom` Over `::`

- **Always use `@importFrom pkg fun`** in the roxygen header of any file that calls an external function, then call the function directly (`fun()`) in the body.
- **Avoid `pkg::fun()` calls** in module code.

The only exception is a one-off call in an `@examples` block or vignette where the full qualified name aids readability.

### Additional Conventions

- Use **4-space indentation** and keep lines to **120 characters** max (enforced by `.lintr`).
- Avoid `sapply()` — use `vapply()` or `lapply()` with explicit types instead.
- Do not edit `NAMESPACE` manually; always regenerate with `devtools::document()`.

## Styling and CSS Containment

**Every stylesheet a module loads is injected into the host app's document.** There is no
shadow DOM and no automatic scoping: a rule you write for your widget applies to the whole
page. Because each plot module pulls in a colour picker, attaching *any* module to a page
brings the package's stylesheets with it — so a careless selector silently restyles an app
that merely embedded one plot.

This has bitten the package three times (#355), and every case was the same mistake:
styling a class the package does not own.

### Only ever style classes this package created

Anchor every selector on a package-owned prefix — `.multi-color-picker`, `.mc-`, `.mdi-`,
`.vizmodules-`, `.viz-`, `.pb-`. Never write a bare rule against a class that belongs to
Bootstrap, Shiny, selectize, DT or any other library:

```css
/* WRONG — restyles every dropdown, well and tab strip in the host app */
.selectize-dropdown .option { padding: 0 !important; }
.well .btn { padding: 4px 10px; }
.nav-tabs { flex-wrap: wrap; }

/* RIGHT — reaches only markup this package rendered */
.mc-palette-dropdown .option { padding: 0 !important; }
.pb-app .well .btn { padding: 4px 10px; }
.vizmodules-input-tabs > .nav-tabs { flex-wrap: wrap; }
```

The trap is that these rules *look* scoped. `.well .btn` reads like "the buttons in my
well" — but `.well` is what `shiny::sidebarPanel()` renders, so it is every sidebar on the
page. If a selector's leftmost class is not one you invented, it is not scoped.

`tests/testthat/test-ui_utils.R` enforces this: it parses every selector in every bundled
and inline stylesheet and fails on anything not anchored to a package prefix. Add your
widget's prefix there if you introduce one.

### A dropdown or popover parented to `<body>` needs its own marker class

Anything that escapes its container to avoid clipping — selectize's
`dropdownParent: "body"`, a tooltip, a popper — can no longer be reached by a
`.my-widget .thing` selector, which is exactly why the leaking rules were written
unscoped in the first place. Give the escaped element a class of its own instead:

```js
$(select).selectize({
  dropdownParent: "body",
  // Selectize replaces its default dropdownClass wholesale, so its own class
  // has to be restated alongside ours.
  dropdownClass: "selectize-dropdown mc-palette-dropdown",
  ...
});
```

### Check that the rule matches what you think it does

`.selectize-dropdown .option` never matched the colour picker's own options at all — its
custom `render.option` emits `.mc-palette-option`, and selectize does not add `.option` to
that. The rule's *only* effect was on other people's dropdowns: a selector that misses
everything you own and hits everything you don't. Open the widget, inspect the element,
and confirm the class you are targeting is actually on it.

### Keep layout out of inline styles

An inline `style=` attribute can only be overridden with `!important`, which makes a host
app fight your widget rather than theme it. Put layout in a stylesheet and pass only the
values that vary per instance, as CSS custom properties:

```r
# organize_inputs() emits just the column count; the rest lives in vizModules.css
div(
    class = "vizmodules-input-grid",
    style = paste0("--viz-input-columns: ", columns, ";"),
    lapply(cells, function(x) div(class = "vizmodules-input-cell", x))
)
```

### Do not assume anything about the parent

The control grid used Bootstrap's negative-margin row idiom
(`margin-left: -15px` on the row, matching padding on each cell), which assumes a parent
with at least that much horizontal padding to absorb it. Dropped into a `bslib::sidebar()`
with less, the grid overhung on both sides and the sidebar grew a horizontal scrollbar.
Use `gap` instead — it needs nothing from the parent.

Give flex children `min-width: 0` as well. Without it a long select or label refuses to
shrink below its intrinsic width and pushes the container wider — the same overflow by a
different route.

### Shipping a stylesheet

Put it in `inst/src/`, serve it through an `htmlDependency()`, and attach it to the UI that
needs it rather than to the app as a whole:

```r
.viz_modules_dependency <- function() {
    htmlDependency(
        name = "viz-modules",
        version = as.character(utils::packageVersion("VizModules")),
        src = "src", package = "VizModules",
        stylesheet = "vizModules.css"
    )
}
```

Attaching it with `htmltools::attachDependencies(ui, dep, append = TRUE)` means the styles
travel with the markup — including through a runtime `insertUI()`, which the Figure Builder
relies on when it injects a panel's controls.

### Verifying containment

Automated checks catch an unscoped *selector*, but not whether your rule actually changed
anything. To see the real effect, load the widget beside a stock `selectInput()`,
`sidebarPanel()` and `DT::datatable()`, then disable just your stylesheet in the browser
and re-measure:

```js
document.querySelectorAll('link[rel=stylesheet]').forEach(function (l) {
  if (l.href.indexOf('yourFile.css') !== -1) { l.disabled = true; }
});
```

If a computed style on the host's own controls changes, your CSS is leaking. If nothing in
your widget changes, your rule was never matching in the first place.

## Sanitizing User-Provided Expressions

**Never use `eval(str2expression())` or `eval(parse())` on raw user input.**
If a Shiny app is deployed publicly, this allows arbitrary code execution on the
server (e.g., `system("rm -rf /")`). VizModules provides three exported helper
functions for safely handling user-typed expressions. Use them whenever your module
accepts free-text input that will be evaluated or passed to a plotting function.

### `safe_eval_filter(expr_text, data)`

Use when a module **evaluates** a user-typed filter expression directly to produce
a logical vector for row subsetting. The expression is parsed, its AST is walked
to ensure only allowed operations are present (comparisons, logical operators,
column references, and literals), and then it is evaluated in a restricted
environment containing only the data frame's columns.

```{r}
# In a module server — filtering rows by a textInput:
rows.use = safe_eval_filter(isolate_fn(input$rows.use), data())
```

Returns a logical vector (same length as `nrow(data)`), or `NULL` if the input is
empty, unparseable, or contains disallowed operations.

### `validate_expression(expr_text, col_names)`
Use when a module **passes** a user-typed expression string through to a
downstream plotting function that will evaluate it internally (e.g.,
`plotthis::BoxPlot(highlight = ...)`). The string is validated but not executed.

```{r}
# In a module server — passing a highlight expression to plotthis:
highlight <- validate_expression(isolate_fn(input$highlight), names(data()))
```

Returns the original string if safe, or `NULL`.

### `safe_resolve_adj_fxn(fn_name)`

Use when a module resolves a function name from a dropdown or text input into an
actual function reference (e.g., for `x.adj.fxn`, `y.adj.fxn`). Only function
names in the allowed list (`"log2"`, `"log"`, `"log10"`, `"neg_log10"`, `"log1p"`,
`"as.factor"`, `"abs"`, `"sqrt"`) are accepted.

```{r}
# In a module server — resolving an adjustment function:
x.adj.fxn = safe_resolve_adj_fxn(isolate_fn(input$x.adj.fxn))
```

Returns the function, or `NULL` if the name is empty or not in the allowed list.

### What counts as "allowed"?

`safe_eval_filter()` and `validate_expression()` share one whitelist of safe AST nodes:

- **Comparisons:** `<`, `>`, `<=`, `>=`, `==`, `!=`
- **Logical operators:** `&`, `&&`, `|`, `||`, `!`, `xor()`
- **Utilities:** `%in%`, `c()`, `is.na()`, `is.null()`
- **Arithmetic:** `-`, `+`, `*`, `/`, `:`, `%%`, `abs()`, `round()`
- **Strings and patterns:** `grepl()`, `startsWith()`, `endsWith()`, `substr()`,
  `nchar()`, `toupper()`, `tolower()`, `trimws()`
- **Grouping:** `()`
- **Column names** from the data
- **Literals:** numbers, strings, `TRUE`, `FALSE`, `NA`, `NULL`, `Inf`, `NaN`

Anything outside this list (including function calls like `system()`, `file.remove()`,
`library()`, etc.) is rejected and a warning is issued. So is a namespaced or extracted
call such as `base::log(x)`, which would otherwise slip past a name-based check.

The expression must also be a **single statement**: anything after a `;` or a newline is
rejected rather than silently ignored.

`safe_resolve_adj_fxn()` is the exception — it does not walk an AST at all. It matches the
whole input against its own short list of numeric transforms (`log2`, `log`, `log10`,
`neg_log10`, `log1p`, `as.factor`, `abs`, `sqrt`) and resolves it with `match.fun()`.

Model formulas (`.safe_build_model()`, behind the custom fit lines) reuse the same walker
with a formula-specific vocabulary: `~`, `+`, `-`, `*`, `/`, `^`, `(`, `:`, `I()`, and the
transforms `log()`, `log2()`, `log10()`, `sqrt()`, `exp()`, `poly()`.
