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.
When building a custom module, you need to handle Shiny’s namespacing correctly. The key insight is:
NS(id).id, not a namespaced version.moduleServer()
block.moduleServer() block to avoid
double-namespacing issues.Let’s build a custom module that adds a simple filtering checkbox to
the scatterPlot module.
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.
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.dittoViz_scatterPlotServer(id, filtered_data)
outside the moduleServer() closure, the base
module attaches to the same namespace as our UI, not a nested one.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.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)If your module pre-sets certain parameters, you can hide those inputs from the user to keep them from being changed:
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")
)
}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.)
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.
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:
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, then return the figure.
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.
Keep wrapper logic focused: Each wrapper should add a cohesive set of related functionality.
Document the data requirements: If your wrapper expects certain columns, a specific class or data structure, etc, document this clearly.
Use reactive expressions: Use reactive data inputs.
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.
Consider composability: Design your wrappers so they could potentially be wrapped by even higher-level modules.
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.