---
title: "Building Custom Modules"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Building Custom Modules}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
    collapse = TRUE,
    comment = "#>",
    eval = FALSE
)
```

## Introduction
 
The modules in **VizModules** are designed to be composed and extended.
You can build higher-level modules that add custom logic such as data filtering, transformations,
or additional UI controls while reusing the full functionality of the base modules.

This vignette demonstrates how to create a custom module by building on top of the `scatterPlot` module.

If you are working with an AI coding agent, the package ships an
[Agent Skill](https://agentskills.io) covering exactly this material 
(**`vizmodules-custom-module`**), along with the namespace contract, reactive `defaults`,
avoiding double renders, manual-edit persistence, and model-line backends. 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/`. See the README for details.

## The Pattern

When building a custom module, you need to handle Shiny's namespacing correctly. Some key tips include:

1. **Your module's custom inputs** need to be namespaced with `NS(id)`.
2. **The base module's UI and server functions** should receive the bare `id`, not a namespaced version.
3. **Data processing** that requires access to your module's inputs must happen inside a `moduleServer()` block.
4. **The base module's server** should be called *outside* that `moduleServer()` block to avoid double-namespacing issues.

## A Minimal Example

Let's build a custom module that adds a simple filtering checkbox to the `scatterPlot` module.

### The UI

```{r module-ui}
library(VizModules)

minimalModuleUI <- function(id) {
    ns <- NS(id)
    tagList(
        h4("Minimal Module Controls"),
        # Custom input - uses the module's namespace
        checkboxInput(ns("filter_setosa"), "Start with Setosa Only", value = FALSE),
        hr(),
        # Base module UI - pass the bare 'id', not ns(id)
        dittoViz_scatterPlotInputsUI(id, iris)
    )
}

minimalModuleOutput <- function(id) {
    # Simply delegate to the base module's output UI
    dittoViz_scatterPlotOutputUI(id)
}
```

Notice that `checkboxInput()` uses `ns("filter_setosa")` to namespace the custom input,
while `dittoViz_scatterPlotInputsUI()` receives the bare `id`. This ensures the base module creates its inputs in the correct namespace.

### The Server

```{r module-server}
minimalModuleServer <- function(id, data_reactive) {
    # Step 1: Process data inside a moduleServer block
    # This gives us access to inputs namespaced to 'id' (our module's inputs)
    filtered_data <- moduleServer(id, function(input, output, session) {
        reactive({
            req(data_reactive())
            df <- data_reactive()

            # Input specific to this custom module
            if (isTRUE(input$filter_setosa)) {
                if ("Species" %in% names(df)) {
                    df <- df[df$Species == "setosa", ]
                }
            }
            df
        })
    })

    # Step 2: Call the base module server OUTSIDE the moduleServer block
    # This is critical! If we called this inside the moduleServer above,
    # dittoViz_scatterPlotServer would look for inputs at id-id-inputName instead of id-inputName
    dittoViz_scatterPlotServer(id, filtered_data)
}
```

**Why this pattern?**

- `moduleServer(id, ...)` gives us access to `input$filter_setosa`, which is namespaced to our wrapper's `id`.
- By calling `dittoViz_scatterPlotServer(id, filtered_data)` *outside* the `moduleServer()` closure, the base module attaches to the same namespace as our UI, not a nested one.
- If we called `dittoViz_scatterPlotServer()` inside the `moduleServer()` block, it would create nested namespaces like `id-id-x_axis`, which wouldn't match the actual input IDs in the UI.

### Putting It Together

```{r full-app}
ui <- fluidPage(
    titlePanel("Minimal Module Example"),
    sidebarLayout(
        sidebarPanel(
            minimalModuleUI("demo")
        ),
        mainPanel(
            minimalModuleOutput("demo")
        )
    )
)

server <- function(input, output, session) {
    # Pass a reactive data source to the module
    minimalModuleServer("demo", reactive({
        iris
    }))
}

shinyApp(ui, server)
```

## Hiding Base Module Inputs

If your module pre-sets certain parameters, you can hide those inputs from the user to keep them from being changed:

```{r hiding-inputs}
focusedModuleUI <- function(id) {
    ns <- NS(id)
    tagList(
        h4("Simplified Scatter Plot"),
        # The UI takes no hide.* arguments -- see the server below.
        dittoViz_scatterPlotInputsUI(id, iris)
    )
}

focusedModuleServer <- function(id, data_reactive) {
    # hide.inputs and hide.tabs are arguments of the *Server*, not the *InputsUI.
    dittoViz_scatterPlotServer(id, data_reactive,
        hide.inputs = c("shape.by", "color.by")
    )
}
```

## Driving Base Module Inputs From Your Wrapper

Hiding an input fixes it to one value. When a base module parameter instead needs
to *follow* something your wrapper computes, pass a `reactive()` as that entry of
`defaults` rather than reaching for `update*Input()`. The key must name an input the
module actually reads -- `get_default()` falls back silently, so a `defaults` entry for
an unexposed key does nothing at all. (`main` is the one to watch: no module exposes a
plot title, so `defaults = list(main = ...)` has no effect.)

```{r reactive-defaults}
colouredModuleServer <- function(id, data_reactive) {
    colour_col <- moduleServer(id, function(input, output, session) {
        reactive(if (isTRUE(input$filter_setosa)) "" else "Species")
    })

    # The colour mapping tracks the checkbox, but the user can still change it.
    dittoViz_scatterPlotServer(
        id, data_reactive,
        defaults = list(color.by = colour_col)
    )
}
```

This is the recommended parent-to-child channel for parameter values. It resolves
server-side, so the plot renders once per change instead of twice, and the control
stays user-editable. See `vignette("defaults-and-hiding", package = "VizModules")`
for the full semantics.

## Adding Layers to the Scatter Figure (`fig.fn`)

`defaults` sets what the base module draws; to draw something *more*, such as loading
arrows on a PCA biplot, a threshold label, or axis titles your wrapper works out, pass
`dittoViz_scatterPlotServer()` a `fig.fn`. It is a `function(fig, input, isolate_fn)` that
receives the built plotly figure and returns it. It runs in the figure's styling step (see
`apply_uniform_styling()`), after the legend is styled and before the axis titles become
draggable annotations, so what it adds reaches the rendered plot, manual-edit persistence and
the source download alike. The plot underneath is cached, but this step is not, so `fig.fn`
can read any of your wrapper's reactives as well as its inputs.

```{r fig-fn}
cutoffScatterInputsUI <- function(id, data, defaults = NULL) {
    ns <- NS(id)
    tagList(
        numericInput(ns("cutoff"), "Y Cutoff", value = NA),
        dittoViz_scatterPlotInputsUI(id, data, defaults = defaults)
    )
}

cutoffScatterServer <- function(id, data_reactive, defaults = NULL) {
    dittoViz_scatterPlotServer(
        id, data_reactive,
        defaults = defaults,
        fig.fn = function(fig, input, isolate_fn) {
            # The wrapper's input shares the module's id, so it is on this `input` too.
            # isolate_fn() respects the module's Auto Update setting.
            cutoff <- isolate_fn(input$cutoff)
            if (is.null(cutoff) || is.na(cutoff)) {
                return(fig)
            }
            # Append rather than replace: facet strips, brackets and line labels share this list.
            fig$x$layout$annotations <- c(fig$x$layout$annotations, list(list(
                text = paste("Cutoff:", cutoff), showarrow = FALSE,
                xref = "paper", x = 1, xanchor = "right",
                yref = "y", y = cutoff, yanchor = "bottom"
            )))
            fig$x$layout$yaxis$title$text <- paste0(isolate_fn(input$y.by), " (cutoff ", cutoff, ")")
            fig
        }
    )
}
```

Coordinates are those of the plot as drawn, so a cutoff on an adjusted (e.g. log10) Y axis is
in adjusted units, and anything you derive from the data should come from the plotted values
(`as_plotted()`), not raw `data()`. Edit `fig$x$layout` directly rather than queuing whole
elements through `plotly::layout()`, which would overwrite the module's own annotations and
shapes at build time.

## Automatically Preserving Manual Title/Legend/Annotation Edits

The base modules let users interactively drag and edit the plot title, legend,
annotations, draggable axis titles, and continuous-colour legend (colorbar), and
they re-apply those manual tweaks across re-renders. When you wrap a base
module (as in the examples above), this persistence is inherited for free.

If you build a brand-new `plotly` output from scratch inside a custom
module (rather than delegating to a base module's server), you can add the same
behaviour simply with 3 basic steps:

```{r manual-edits}
customPlotServer <- function(id, data_reactive) {
    moduleServer(id, function(input, output, session) {
        # 1. A unique event source + the edit store, created once.
        plot_source <- session$ns("customplot")
        edit_store <- setup_manual_edits(input, session, plot_source)

        output$plot <- renderPlotly({
            # 2. Create your plotly object with your plotting function
            fig <- build_my_plotly_figure(data_reactive(), input)
            # 3. Restore + capture edits on every render. Last: when only the
            #    layout changed, it sends just that and ends the render.
            finalize_manual_edits(fig, plot_source, edit_store, session)
        })
    })
}
```

See the "Adding a New Module" vignette for the full description of these helpers,
but this is pretty much all you need to do.

## Drawing a Custom Plot Once, Styling It Often

Wrapping a base module gets you this for free. A plot you draw yourself is
rebuilt whenever anything it reads changes, so if titles, fonts, and gridlines
are read alongside the data, changing a font redraws the plot. Split it in two,
as the base modules do: `cached_structure()` draws the plot and caches it, and
`apply_uniform_styling()` styles the cached figure.

```{r two-steps}
customPlotServer <- function(id, data_reactive, defaults = NULL) {
    moduleServer(id, function(input, output, session) {
        params <- setup_reactive_defaults(defaults, input, session)
        plot_source <- session$ns("customplot")
        edit_store <- setup_manual_edits(input, session, plot_source)

        # What is drawn: cached, keyed on the data and every input read here.
        draw_plot <- function(isolate_fn) {
            p <- my_plot_function(data_reactive(), x = isolate_fn(input$x.by))
            list(fig = plotly::ggplotly(p))
        }
        base_plot <- cached_structure(draw_plot, input, session, data_reactive, params = params)

        # How it looks: re-run for a styling change, without redrawing.
        generate_plot <- reactive({
            isolate_fn <- setup_auto_update_logic(input, params)
            apply_uniform_styling(base_plot()$fig, input, isolate_fn)
        })

        output$plot <- renderPlotly({
            finalize_manual_edits(apply_render_margins(generate_plot(), input), plot_source, edit_store, session)
        })
    })
}
```

The traces then stay the same across a styling change, so
`finalize_manual_edits()` sends the browser only the new layout. The cache is
only as good as what it can see, so the draw function has three rules. Breaking
one does not fail; it draws a stale plot:

- **Read every input as `isolate_fn(input$key)`.** A wrapped read such as
  `isolate_fn(as.numeric(input$size))`, or a bare `input$size`, never reaches the
  cache key. Convert outside the call.
- **Name every other reactive it reads in `extra`**: colour stores, a debounced
  text input, a reactive argument. A reactive that only derives from `data` is
  already covered.
- **No side effects.** A cache hit skips the function, so it must not show
  notifications or write `reactiveVal()`s. Return anything else you need in the
  list.

## Helpers for Modules in Extension Packages

A module built on VizModules' exports (in your own package, say) can reuse the helpers the
built-in modules share, rather than keeping copies:

```{r extension-helpers}
extendedServer <- function(id, data, defaults = NULL, hide.inputs = NULL) {
    moduleServer(id, function(input, output, session) {
        data <- require_data_frame(data)

        # A faceted plot has no editable main title: swap in the facet title inputs.
        observe({
            toggle_facet_title_inputs(session, nz_value(input$facet.by), hidden = hide.inputs)
        })

        output$plot <- renderPlotly({
            # "" (nothing chosen), NA and NULL all become NULL.
            facet.by <- blank_to_null(input$facet.by)
            # Jitter stays put across rebuilds instead of redrawing on every input change.
            with_stable_seed(build_my_plotly_figure(data(), facet.by = facet.by))
        })

        observeEvent(input$reset, {
            # Restore the group colour picker to the mapping in `defaults`.
            reset_group_colors(session, "palette.colours", defaults, groups(), default_palette_values)
            reset_plotly_inputs(session, defaults)
        })
    })
}
```

On the UI side, `uniform_subplot_spacing_inputs_ui(ns, defaults)` adds the subplot spacing
controls to a Facet tab (`reset_plotly_inputs()` already resets them, and
`apply_facet_subplot_spacing()` applies them), and `uniform_stats_inputs_ui(ns, defaults)` adds
the Stats tab (`reset_stats_inputs()` resets it). Seed a `multiColorPicker()` built inside
`renderUI()` from `default_group_colors(defaults, "palette.colours")`. See the "Adding a New
Module" vignette for the full list.

## Best Practices

When you add a free-text control of your own, debounce it. `textInput()` reports
on every keystroke, so a base module reading it directly is rebuilt once per
character — and if the text is an expression, most of those characters describe
something that cannot parse yet:

```{r debounce-wrapper}
myModuleServer <- function(id, data) {
    filtered <- moduleServer(id, function(input, output, session) {
        # Once, in the moduleServer body (where the wrapper's own input lives),
        # not inside the reactive below. 500-800ms is a good range.
        query <- debounce(reactive(input$query), 700)

        reactive({
            keep <- safe_eval_filter(query(), data())
            if (is.null(keep)) data() else data()[keep, , drop = FALSE]
        })
    })

    dittoViz_scatterPlotServer(id, filtered)
}
```

Select, numeric, and checkbox inputs need no such treatment — they report
discrete choices, not keystrokes. See the "Debouncing Free-Text Inputs" section
of `vignette("adding-a-new-module", package = "VizModules")` for the full
rationale.


1. **Keep wrapper logic focused**: Each wrapper should add a cohesive set of related functionality.

2. **Document the data requirements**: If your wrapper expects certain columns, a specific class or data structure, etc, document this clearly.

3. **Use reactive expressions**: Use reactive data inputs.
 
4. **Test the namespace**: If inputs aren't working, check that you're handling namespaces correctly.
A common symptom of namespace issues is that inputs seem to have no effect.

5. **Consider composability**: Design your wrappers so they could potentially be wrapped by even higher-level modules.

6. **Prefer reactive `defaults` over `update*Input()`**: To drive a base module parameter from your wrapper's state, pass a `reactive()` in `defaults`. Pushing values with `update*Input()` costs an extra render on every change. When you must update one of your *own* inputs from the server, call `freezeReactiveValue()` on it first so readers pause instead of rendering with the stale value. Freezing does not cover an input you rebuild with `renderUI()`, though. In those cases, resolve the value server-side and have the plot read that instead, as `setup_group_colors()` does for a colour picker (see the "Updating Your Own Inputs From the Server" section in `vignette("adding-a-new-module", package = "VizModules")` for more info).

See any of the existing modules for clear reference examples.
