Changelog
Source:NEWS.md
VizModules 0.5.0
CRAN release: 2026-09-29
The one where we make the heatmap module not suck and stop accidentally butchering app CSS.
Improved/New Functionality
All modules
-
Source Download now also saves a
<name>_plot.svgand a<name>_plot.pngof each plot (#353). The Figure Builder saves one pair per panel, named after the panel’s label.- The images are captured in the browser from the graph as displayed, so they include everything applied after the figure was built. The button pauses briefly while capturing; if capture fails, the archive still downloads without them.
- A WebGL plot (the scatter plot’s WebGL toggle) can only be captured as a raster, embedded in an otherwise editable SVG. Turn WebGL off for a fully editable file.
- A module whose output isn’t a plotly graph can draw itself instead, via
vector_svgand/orraster_pngfunctions of(width, height, res)on its summary list or on the reactive its server returns.ComplexHeatmap_HeatmapServer()provides both.
- Without pandoc, which
htmlwidgets::saveWidget()needs, the source download now warns and leaves out the self-contained HTML instead of writing an empty.htm, so the data and images still download. - The shared Legend tab gained Show Legend, Legend Font and Legend Font Color (#360, #362). The legend can now be hidden in
yPlot,freqPlot,linePlot,dumbbellPlotand theplotthismodules too, and hiding it also hides colorbars and the scatter/DotPlotsize legend. - Each module’s app now opens on defaults that show more of what it can do, with more interesting example datasets to match.
Module gallery and Figure Builder
- The module gallery is now a function,
moduleGalleryApp(), with a tab per module plus the Figure Builder. The bundledinst/apps/module-galleryapp is a thin deployment wrapper around it. - The
ComplexHeatmapmodule can now be added to the Figure Builder (#352).- The figure export read an SVG out of each panel’s plotly graph, so any other kind of panel contributed only its label. A module can now attach a
vector_svgattribute (afunction(width, height, res)returning an<svg>) to the reactive its server returns, and the export uses that instead.
- The figure export read an SVG out of each panel’s plotly graph, so any other kind of panel contributed only its label. A module can now attach a
-
figureBuilderServer(),figureBuilderApp()andcreateModuleApp()accept adata_listentry that is a list of data frames, as theComplexHeatmapmodule needs.createModuleApp()filters and displays only the primary table (primary.table, default the first) and passes the others through untouched. It also gainedsidebar.width.
BoxPlot, freqPlot and yPlot
-
stat.pairscan be set fromdefaults, as"A vs B"strings in either order.
ComplexHeatmap
- Added
ComplexHeatmap_HeatmapStaticOutputUI(), a plainplotOutput()of the same heatmap. It gives up cell hover/click, the sub-heatmap and the brush info panel, and with them all of the widget’s own chrome. The gallery andComplexHeatmap_HeatmapApp()keep the interactive widget. - Row and column splits gained an “Annotation” method (#349), slicing by one or more annotation columns (several give nested slices). With clustering off it is also the fast path, since no distance matrix is needed.
- New Filter tab with expression-based row and column filters (#346), so a subset can be plotted without the
dataFiltermodule. Row filters see the matrix data frame; column filters see acolumnfield plus any per-sample metadata joined viacolumn_key. Filtering runs first, so scaling, annotations, splits and the source download all describe the filtered matrix.- Both filters are debounced by 700ms, so typing doesn’t redraw the heatmap on every keystroke. The “Adding a New Module” and “Building Custom Modules” vignettes document the pattern for free-text inputs.
- New Show Row Slice Titles and Show Column Slice Titles checkboxes (#366). Clearing the title box never removed the group names a split titles its slices with (annotation values, or cluster numbers for k-means and hierarchical splits), because
Heatmap()reads a blank title as “use the group names”. Unticking one drops the titles and the space they take. A typed title still replaces them, and a%sin it is filled in with each group’s name. - Each annotation track gained Label Side, Label Size and Show Legend controls. Set the last from
defaultswith ashow_legendfield on arow_annotations/column_annotationsrow. - The output UI functions gained
fit.width(defaultTRUE), fitting the widget’s panels to their container on load (#350) instead ofInteractiveComplexHeatmap’s fixed pixel widths.heatmap_fit_width()does the same for apps callingInteractiveComplexHeatmap::InteractiveComplexHeatmapOutput()directly. -
ComplexHeatmap_HeatmapApp()’s default data now exercises the column annotation, split and filter features.
dumbbellPlot
- New Point Size input (
point.sizeindumbbellPlot(), default 12) (#361).
linePlot
-
Error Bars can now show the standard deviation (still the default), the standard error of the mean, or a 95% confidence interval (#368), through the new Error Bar Type input (
error.typeinlinePlot()). A confidence interval uses the normal approximation unless the Confidence Interval Method input (error.ci.method, shown only while a CI is selected) switches it to the t distribution, which is wider for small groups. A group with fewer than two observations is drawn without a bar. The tooltips say what the bars show (the plotted group mean plus or minus that amount).
parallelCoordinatesPlot, piePlot and radarPlot
- The shape-drawing controls are gone, since plotly’s drawing tools need cartesian axes and they did nothing here. The pie and radar modebars no longer offer the drawing buttons either.
uniform_plotly_inputs_ui()gainedinclude.shapesfor this.
scatterPlot and yPlot
- The Adjustment Function now runs before the z-score/relative-to-max Adjustment (log10, then z-score), and axis titles read accordingly (
z-score(log10(salary))). dittoViz does the reverse, which took the log of every below-average value, so those points vanished. The rescaling also skips non-finite values, so alog(0)drops that one point instead of making the whole columnNaN.
Helpers and exported functions
- Exported
draw_to_svg()(previously the internal.draw_to_svg()) and addeddraw_to_png(). They render any grid or base drawing, for thevector_svg/raster_pnghooks above. -
safe_eval_filter()andvalidate_expression()now share one allowlist and AST walker (each had its own copy), with a wider but still pure vocabulary:grepl,startsWith,endsWith,substr,nchar,toupper,tolower,trimws,abs,roundandxor. -
adjust_column_values()gainedx.adjustment,y.adjustmentandcolor.adjustment("z-score","relative.to.max"), reproducing exactly what the modules plot. Use it to compute anything drawn over such a plot. -
create_stat_annotations()gainedfree.y, stacking each facet panel’s brackets above that panel’s own data, andapply_stat_annotations()then raises each panel’s axis separately. -
apply_legend_styling()gainedshow,font.familyandfont.color, and the newapply_legend_inputs()applies the whole Legend tab from a module’s inputs. -
collect_source_data()acceptsinputs_reactiveas a reactive, as documented, as well as a plain list.
Deprecations and Removals
Module gallery and Figure Builder
- The Figure Builder no longer accepts
.rdsuploads. - Removed the bundled
inst/apps/figure-builderapp, since the builder is part of the gallery now.
BarPlot and SplitBarPlot
- Removed the Split By input. It returns a patchwork object, which
ggplotly()can’t convert, so it never did anything.
scatterPlot
- Removed the built-in
nlsbackend for custom model lines. An nls formula names its parameters, which the formula safety check rejects, so it could never fit.
ViolinPlot
- Removed the
plotthis_ViolinPlotmodule (plotthis_ViolinPlotApp(),plotthis_ViolinPlotInputsUI(),plotthis_ViolinPlotOutputUI(),plotthis_ViolinPlotServer()) (#358).plotthis0.14.0 draws violins with a geom of its own thatggplotly()can’t convert, andyPlotcovers the same ground. UsedittoViz_yPlotwithdefaults = list(plots = "vlnplot"), adding"boxplot"/"jitter"for the inner box and points.
Bug Fixes
All modules
-
Reset now discards manually dragged legends, annotations, axis titles and colorbars. It also returns every control to the value it started at; several fallbacks disagreed with the UI (
stat.hide.ns, the top and right margins, subplot spacing, and a few module-specific ones). - Stopped the package’s stylesheets leaking into host apps (#355). CSS still sucks.
-
multiColorPicker’s dropdown, which is parented to<body>, was styled through selectize’s generic class names. That restyled everyselectInput()and DT column filter on the page, most visibly rendering long dropdowns as an empty panel. Every plot module has a colour picker, so this affected any app using any module. The rules are now scoped to the picker’s own.mc-palette-dropdown. - The Figure Builder styled
.well, so embedding it respaced everysidebarPanel()on the page. Its rules are now scoped to.pb-app. -
organize_inputs()used inline negative margins that assume a padded parent, so in a narrow sidebar the grid overhung and scrolled sideways, and undoing that took!important. It now uses a stylesheet and a column gap; only the column count is still inline, as--viz-input-columns.
-
- Faceted plots no longer offer an editable main title, whose empty placeholder sat over the facet panel titles and caught the clicks meant for them. The Title inputs are hidden and the facet title inputs shown while a plot is faceted, without re-showing anything hidden via
hide.inputs.yPlotnow also counts several Y variables split into panels as faceted. - Multi-selects no longer drop a deselection made from a value tag’s x (or the clear-all x).
viz_select_input()reports a multi-select when its dropdown closes, and removing a tag never opens it. - An input that has not reported yet (
NULL) no longer throws “argument is of length zero” while a module initializes. - The source download no longer writes the captured plot images (up to tens of MB) into
*_ui_inputs.csv. - The Lines tab’s line-type tooltips list the names actually accepted.
Module gallery and Figure Builder
- The module gallery passes its
defaultsto the module servers too, so Reset matches the initial state.
AreaPlot
-
Group By and Facet By no longer leave out the dataset’s first categorical column when it isn’t the X column, which silently dropped a
group.by/facet.bydefault naming it.
BarPlot, BoxPlot, SplitBarPlot and yPlot
- Axis limits given in
defaults(y.min/y.max, orx.min/x.maxinSplitBarPlot) are no longer replaced by the data’s range at startup; they stand until the plotted columns change.yPlot’s keys are nowy.min/y.maxtoo, matching its inputs and Reset (its UI readmin/max). -
Reset restores the
BoxPlotandBarPloty limits for the column it resetsy.datato. It measured the first numeric column, which clipped a plot whose defaulty.datawas another one.
BoxPlot
-
Security: Sort X By was passed straight to
plotthis::BoxPlot(), which evaluates it, so R code typed there ran on the server. It now goes through the same expression check as every other user-typed expression, allowing summary functions (mean,median,sd, …) and ignoring anything else with a notification.
BoxPlot, freqPlot and yPlot
- Boxes, points and significance brackets now line up when a
color.bygroup is missing from somegroup.bycategories (#356). The new.align_box_positions()puts the boxes back on ggplot’s coordinates under plotly’sboxmode = "overlay", replacing the old faceting workaround.- Brackets now use the same dodge as the boxes rather than a formula of their own, and a comparison against a group with no data in that category (an NA p-value) is no longer drawn as “NA” at an invented position.
- A panel is now identified by its pair of axes, which mattered for any facet grid more than one row deep (so most
freqPlots). Box width comes from the most crowded x position in the whole figure, so boxes no longer change size between facets. -
boxmode = "overlay"is only set once every box has an explicit position, where it could leave boxes stacked on a categorical tick. Theboxgap/boxgroupgapattributes plotly’s schema rejects are no longer set, which silences a pile of rebuild warnings.
- Overlays are now computed from the values drawn, after any Y adjustment, rather than from the raw columns (#319, #365).
-
yPlotbrackets were drawn at raw heights under a Y adjustment (log10, z-score, …), far above the axis, and tested on the raw values. Tests, brackets and the Y Axis Min/Max now all use the adjusted values; the limits used to be ignored whenever an adjustment was on. -
yPlotandfreqPlotbrackets could be clipped even without an adjustment.apply_subplot_axis_styling()queued a copy of each whole axis, which reverted the raised range at build time; it now queues only the styling. - Under a free y facet scale, each panel now keeps its own range and its brackets sit above its own data.
yPlotandfreqPlotpinned every panel to the Y Axis Min/Max, and all three drew brackets at the tallest panel’s height (and, after the first panel, on the first panel’s axis). - A rotated
BoxPlot, or ayPlot/freqPlotthat includes a ridge plot, puts the values on the x-axis, where brackets were drawn across the category axis. They are now skipped with a notification, and the test results still go in the source download.
-
- The source download no longer ships a stale statistics CSV after stats are turned off.
- Significance labels for a p-value below 0.0001 read “< 0.0001” rather than rounding to “0”.
ComplexHeatmap
- Every module-hosted heatmap silently never drew.
InteractiveComplexHeatmapregisters heatmaps undervalidate_heatmap_id(), which turns each non-word character into_, but the module looked up the raw namespaced id (mymod-heatmap-Heatmap), found nothing, and returned beforemakeInteractiveComplexHeatmap()could run. A module id always contains a-, so every instance rendered an empty shell with no error. - Default annotations now render their colour pickers and tracks on load.
multiDynamicInput()reports its initial rows to Shiny before deferred DOM binding, omitted fields backfill fromrow_spec, and the server resolves palettes immediately. - The
compact = TRUEwidget (and anyoutput_ui_float = TRUE) no longer adds a ~10,000px horizontal scrollbar to the host app. - With Auto Update off, the matrix columns, row names, filters and column key no longer redraw the heatmap before Update.
dataFilter
-
dataFilterServer()no longer applies the previous table’s row indices to newly supplied data while DT redraws.
dumbbellPlot
-
Colour By “Y variables” now matches the colour picker, keeps each category’s colour across facets, and gives every category a legend entry (only the first had one).
dumbbellPlot()matches a namedpalette.selectionby name. - A faceted plot now draws a border around every panel rather than only the first, which was the only one with a y axis to draw its edges.
dumbbellPlot and linePlot
-
Axis Title Size/Color/Font and Facet Title Size/Color/Font now work (#326), as do Show X/Y Gridlines and Gridline Color (
dumbbellPlotignored all three,linePlotthe colour).dumbbellPlot()andlinePlot()gained the matchingaxis.title.font.*,facet.title.font.*,show.grid.*andgrid.colorarguments, andbuild_facet_annotations()gainedaxis.title.fontandfacet.title.font. - Facet titles now sit directly above their own panel under shared (fixed) axes.
build_facet_annotations()fell back to an even grid that ignored the panel gaps, which put lower rows’ titles up against the row above. - No more plotly default zero line, which couldn’t be turned off. Add one from the Lines tab instead (#363).
freqPlot, scatterPlot and yPlot
- Values containing spaces (e.g.
"CD4 T") can now be highlighted when separated by commas or new lines. They were split on the space and so never matched. - Highlight and selection labels are no longer drawn for points an adjustment leaves undrawable (
NaN), which plotly placed at an arbitrary spot (#319, #365). Dumbest thing.
linePlot
- A faceted plot no longer repeats every series in the legend once per facet, and one legend click now toggles that series in every panel (#357).
- Multi-axis plots no longer draw an empty placeholder trace that took up a nameless legend entry in every facet, and
show.legend = FALSEnow hides the legend box instead of leaving an empty one. - Logical and Date columns no longer break the plot;
is_pure_type()errored on them. -
Error Bars sat on the wrong points whenever Group By was set, so each series drew other series’ bars (the gallery’s default line plot included). plotly re-sorts the data by the colour column but not an error bar array handed to it, so
linePlot()now sorts the same way first. Bars also silently vanished when the data had a column namedy, which shadowed the argument insidesummarise().
parallelCoordinatesPlot
-
parallelCoordinatesPlot()tolerates a blank line width.
scatterPlot
- Linear, best-fit and custom model lines are now fit to the plotted values (#319, #365). They were fit to the raw columns, so on adjusted axes they were drawn off in their own space. A formula like
mpg ~ hpmodels what is shown (don’t repeat the adjustment in it), no lines are drawn whenas.factormakes an axis categorical, and a numeric Color By gives one line instead of one per distinct value. - Fit lines no longer land in the wrong facet panel. Character facets are laid out alphabetically, and multi-row or two-column
split.bylayouts share axes by row and column; lines are now matched to panels by their strip labels. - A custom model formula calling a function through a namespace (
y ~ base::log(x)) crashed the render with “the condition has length > 1” instead of reporting a disallowed term, whiley ~ log()(x)was accepted..safe_build_model()had its own copy of the AST walker and now shares the one inR/parse_utils.R. It also returnsNULLrather than an error for an empty formula. - With Auto Update off, the fit-line controls no longer redraw the plot before Update.
SplitBarPlot
- The axis no longer clips a long negative bar beside short positive ones, or inverts when every value is negative.
- Category Label Position can be negative again, moving the labels further out from their bars (#367).
Helpers and exported functions
-
validate_expression()andsafe_eval_filter()checked only the first statement of their input.validate_expression()returned the whole string for its caller to evaluate, so anything after a;or a newline went unexamined, andsafe_eval_filter()evaluated only the first clause, returning a row mask nobody asked for. Both now reject multi-statement input. -
draw_to_svg()returnsNULLinstead of erroring when the build has neithersvglitenor a working cairo, matchingdraw_to_png(). That includes macOS without XQuartz, wherecapabilities("cairo")isTRUEbut the device can’t load. -
multiColorPicker()applies unnamedcolorsin order instead of ignoring them. -
apply_axis_title_to_annotations()tolerates annotations without anxanchor, andsafe_resolve_adj_fxn("neg_log10")works from outside the package.
Documentation
- Added the
ComplexHeatmap_HeatmapanddittoViz_freqPlotmodules to the README, and refreshed the skills for the 0.4.0 changes they had missed (#348). - The
quick-start,custom-modulesandadding-a-new-modulevignettes now point at the bundled agent skill that covers their material, and atuse_vizmodules_skills()(#347). The skills were previously documented only in the README. - Fixed out of date vignette examples.
- The bundled skills and the
defaults-and-hidingandadding-a-new-modulevignettes now cover thelinePloterror bardefaultskeys, the heatmap slice title toggles,hide.inputsoutranking a module’s own show/hide, and two plotting traps behind the #368 fixes (plotly re-sorting by the colour column, and dplyr masking an argument).
VizModules 0.4.0
CRAN release: 2026-08-28
New Modules
- Added a
freqPlotmodule (dittoViz_freqPlotInputsUI(),dittoViz_freqPlotOutputUI(),dittoViz_freqPlotServer(),dittoViz_freqPlotApp()) wrapping [dittoViz::freqPlot()], for comparing the per-sample composition of a categorical variable across groups. Unlike the other modules it does not plot columns of the incoming data, it tabulates how often each level of the chosen variable occurs within each sample and plots those frequencies, one facet per level. The axis limits, statistics, point annotations and source download therefore all describe that summarised frequency table rather than the input rows.- Comes with a new
example_compositiondemo dataset containing 1800 simulated single-cell records over twelve donors nested inside two conditions (and crossed with two batches).
- Comes with a new
- Added a
ComplexHeatmapmodule (ComplexHeatmap_HeatmapInputsUI(),ComplexHeatmap_HeatmapOutputUI(),ComplexHeatmap_HeatmapServer(),ComplexHeatmap_HeatmapApp()) wrapping [ComplexHeatmap::Heatmap()]. Unlike the other plotly-based modules, its interactive output is delivered via theInteractiveComplexHeatmappackage (sub-heatmap zoom, cell hover/click/select). The incoming data frame is converted to a numeric matrix (user-selected columns, with an optional row-name column), and a curated subset ofHeatmap()parameters is exposed via UI inputs.- Row and column annotation tracks can be added on the “Annotations” tab dynamically. Row annotations come from extra columns in the input data frame; column annotations need a companion per-sample metadata table, supplied via
data = list(matrix = <data.frame>, column_annotations = <data.frame>)instead of a plain data frame. - The interactive output can be split into independently-placed pieces —
ComplexHeatmap_HeatmapMainOutputUI(),ComplexHeatmap_HeatmapSubOutputUI(),ComplexHeatmap_HeatmapInfoOutputUI()— for apps that want the main heatmap, sub-heatmap, and click/brush info panel in separate layout locations.ComplexHeatmap_HeatmapOutputUI()also has a...passthrough forInteractiveComplexHeatmapOutput()’slayout,compact(a smaller-footprint mode that drops the sub-heatmap panel and floats the click/brush info near the cursor), and other arguments. - Comes with a new
example_heatmap_matrix(30 genes x 12 samples, with row-annotation columns) andexample_heatmap_column_data(companion per-sample metadata, for column annotations) demo datasets.
- Row and column annotation tracks can be added on the “Annotations” tab dynamically. Row annotations come from extra columns in the input data frame; column annotations need a companion per-sample metadata table, supplied via
Improved/New Functionality
- The package now contains three agent skills under
inst/skills/, installable into a project with the new exporteduse_vizmodules_skills():vizmodules-app(wiring modules into an app),vizmodules-custom-module(building wrapper modules), andvizmodules-new-module(authoring a module in this package). They follow the Agent SkillsSKILL.mdconvention, so GitHub Copilot, OpenAI Codex, Claude Code, and compatible tools can discover them.use_vizmodules_skills()gains aclientargument ("agents"by default, or"github"/"claude") to install into.agents/skills/,.github/skills/, or.claude/skills/as needed.- Benchmarked against the README’s LLM-instruction prompt over 18 paired runs (#341). Building an app the clear win - half the tokens (66k vs 123k) and 40% of the wall time, with identical correctness. Wrapping a module was inconclusive, and authoring a module was cost-neutral. Every run in both arms passed every assertion, so the skills’ measured value is efficiency on lookup-heavy work rather than improved output.
- Each skill carries the traps that cost benchmark runs real time:
useShinyjs()being required in a hand-built app forhide.inputs/hide.tabsto work, pandoc being required bycreate_source_download_handler(),stat.hide.nsdefaulting toTRUEso an enabled Stats tab can draw nothing, andshiny::testServer()being unable to drive a plotly output.
- The
dittoViz_yPlotmodule gained an “Annotations” tab that highlights and labels individual jitter points, matching thedittoViz_scatterPlotmodule (#340).- The annotation controls are now the exported helpers
uniform_annotation_inputs_ui()andreset_annotation_inputs(), so other modules that draw individual points can pick them up, anddittoViz_scatterPlotnow uses them rather than its own copy. -
dittoViz_yPlot’s jitter positions are now drawn from a fixed seed, so they no longer reshuffle on every rebuild. Selections and annotations therefore stay attached to the points they were made on, and a plot redrawn with the same settings is reproducible. - Box/lasso selections are now matched to their points by index rather than by coordinates, so a label survives the rebuild that the selection itself triggers. Selections are cleared when the plot’s structure changes (Y data, grouping, color, shape, facet or plot types), since the indices only describe the layout they were captured on.
- The annotation controls are now the exported helpers
- Every module can now be initialized with an explicit group-to-color mapping via
defaults(#334). Pass a named character vector under the module’s color input key, e.g.defaults = list(palette.colours = c(setosa = "red", virginica = "#0072B2")), and the color picker is seeded with it.- Precedence runs picker >
defaults> the module’s stock palette, so a plot can open on a specific palette while every color stays editable, and groups the mapping does not name still get a sensible default. Reset restores the supplied mapping rather than the stock palette. - Keys are
palette.coloursfor most modules,color.panelfordittoViz_scatterPlot,slice.colorsforpiePlot, andtrace.colorsforradarPlot; the ungrouped single-color controls (single.point.color,single.fill.color,single.color) and the continuous palette selectors (palette.name,gradient.palette) are now seeded fromdefaultstoo. Since individualdefaultsentries may be reactive, a parent app can also drive the palette from its own state.resolve_palette()gains amanual_colorsargument implementing the layering.
- Precedence runs picker >
- The figure builder now passes each panel’s
defaultsto the module server as well as to its inputs UI, so registry defaults can seed server-rendered controls such as the color picker. - Source data downloads are now more robust and now limit to the data actually shown on the plot rather than the entire input dataframe. This makes download snappier and keeps plot source data contained, which is important for publication. The switch to
viz_select_input(described below) also required some changes to handle empty vectors/NULLvalues appropriately. - Individual
defaultsentries can now be areactive()orreactiveVal(), letting a parent app drive a module parameter from its own state (#325). Previously the only route wasupdate*Input()from the parent, which is an asynchronous client round-trip and so re-rendered the plot twice per change (a visible flicker). Reactive defaults are resolved server-side in the same reactive flush as the data, so the plot renders once, while the on-screen control stays populated and user-editable. An external change takes precedence over a value the user has typed, and Reset restores the reactive’s current value. Adds the exported helpersetup_reactive_defaults();setup_auto_update_logic()gains an optionalparamsargument to consume its store, andget_default()now resolves reactive entries withisolate(). Modules with purely staticdefaultsare unaffected. Not supported for the scatter module’s compoundcustom.modelsinput. - Wired up
hover.dataandhover.round.digitsin thedittoViz_yPlotmodule (#317). When no columns are selected, the module reproducesdittoViz::yPlot()’s default hover content, so existing plots are unchanged. - The
dittoViz_yPlotmodule’s “Y Data” input can now take several columns at once (selecting more than one previously errored while computing the y-axis range).- New “Multivar Aesthetic” and “Multivar Split Dir” controls on the Facet tab expose
dittoViz::yPlot()’smultivar.aes/multivar.split.dir, so the selected variables can each get their own facet (the default), sit side by side on the x-axis, or be mapped to the fill legend (in which case the colour picker keys off the variable names, since they are what is being coloured). The y-axis limits span every selected variable, the axis title drops the column name once it no longer describes the shared axis (keeping any adjustment, e.g.log2(z-score)), and the facet-specific handling (subplot spacing, boxplot dodging, shared axis titles) now also applies to variable facets. - Statistics are computed separately within each variable’s facet; the Stats tab is hidden for the “group” and “color” aesthetics, and when a
split.byfacet is combined with several variables, as significance brackets cannot be placed against those layouts without stuff getting hella complicated in ways the current stats implementation cannot yet handle. Ideally, this will be supported in the future but will take some thoughtful work to implement in a robust way.
- New “Multivar Aesthetic” and “Multivar Split Dir” controls on the Facet tab expose
- Every module select input is now a virtualised, searchable dropdown built on
shinyWidgets::virtualSelectInput()(#330). Previously a select fed by a high-cardinality column (e.g.varindittoViz_yPloton a genome-wide table) rendered every option, producing a dropdown that was both slow and impossible to pick from even with max options set. Only the visible slice is rendered now, so tens of thousands of options stay usable, and long lists gain a search box automatically. Adds the exported helpersviz_select_input()andupdate_viz_select()for use in custom modules. Three widgets deliberately stay native because client-side JavaScript reads them directly: the figure builder’s “Panel labels” menu,multiColorPicker()’s palette picker, andmultiDynamicInput()’s select rows. -
dataFilterServer()gainsfilter.max.options(default50), capping how many options a factor column’s DataTables filter dropdown renders at once. Typing still searches the full set. Note that DT serialises every level of a factor column into the page regardless, sofactor.char.cols = TRUEremains a poor fit for columns with very many distinct values. - The
dataFiltertable’s controls now sit on a single row for better use of space. Withcol.visibility = TRUEthe module used DataTables’Blfrtiplayout, which stacks the “Columns” button, the page-length select and the search box in three full-width blocks, wasting three rows of vertical space above the table. They now share one flex row with the search box aligned to the far end, styled by CSS the module ships itself. - Tweaked
multiColorPicker()layout slightly for easier tetrising into compact UIs. Elements should now reflow more appropriately to prevent label/control overlaps in narrow contexts. -
multiColorPicker()no longer reports a value for every step of a colour choice, so a dependent plot is rebuilt once per colour rather than dozens of times. A group’s swatch is a native<input type="color">, and the browser’s colour dialog previousl fired an event for each drag or click inside it (Chrome fireschangejust as often asinput, rather than only on close). Now, the value is only reported when the input loses focus or the user moves the mouse outside it, preventing most unnecessary re-renders. Typing in a hex field is coalesced until the user pauses instead, while one-shot actions, i.e. palette swatches, “Apply”, “Reset”, selecting another group, and a hex code committed with Enter or by clicking away, still report immediately. - Added ability to show/hide columns in the
dataFiltermodule with DataTables’ built-in column visibility controls. This is useful for hiding columns that are not relevant to the user, or for hiding columns that are used for internal logic but not meant to be displayed. Thehide.columnsargument can be used to specify which columns to hide by default (by name or position), which also removes their filter boxes for a simpler interface, andcol.visibility = TRUEadds a “Columns” button so users can toggle visibility via the DataTables UI. Hiding is display-only: hidden columns are still present in the returned filtered data, so downstream plotting modules can use them. The name/position lookup behindhide.columnsis exposed as the new exported helperresolve_column_targets(), which turns column names into the zero-basedtargetsindices any hand-rolled [DT::datatable()]columnDefsentry needs.
Deprecations and Removals
- Removed the
manual.colorsargument fromdittoViz_scatterPlotServer(). It was the only module with such an argument, and it hard-overrode the color picker, so the colors it supplied could not be edited. Pass the same named vector asdefaults = list(color.panel = ...)instead, which every module now understands and which leaves the colors editable.
Bug Fixes
Corrected two documentation errors that would mislead anyone following the vignettes.
quick-start,defaults-and-hiding, andcustom-modulesall useddefaults = list(main = ...)as the worked example for reactive defaults, but no module exposes a plot title: every server passesmain = NULLand none readsinput$main, so the example was a silent no-op. The examples now usecolor.by, which modules do read, and the reactive-defaults sections note that an unrecognised key is silently ignored byget_default(). Separately,custom-modules’ “Hiding Base Module Inputs” example passedhide.inputsto*InputsUI(); that argument belongs to*Server(), and since no*InputsUI()accepts...the example failed with an unused-argument error. Found while benchmarking agent skills against the docs (#341).Every module server (and
dataFilterServer()) now requires itsdatareactive to yield a data frame: values that are not data frames are coerced withas.data.frame(), and aNULLmakes the module wait for data rather than error. A parent app that briefly emitsNULLcan no longer take a plot down with it.-
Fixed modules rendering their plot two or three times for a single change (somewhat related to #325). Several modules compute a value on the server and push it into one of their own inputs with
update*Input(), which is an asynchronous client round-trip: the plot rendered once with the stale value and again when the client echoed the new one. On loaddittoViz_yPlotdid this three times over (y-axis range, stat comparison pairs, and the rebuiltmultiColorPicker).- These inputs are now wrapped in
freezeReactiveValue()so dependents pause until the new value lands, giving a single render. This still applies tostat.pairs(dittoViz_yPlot,plotthis_BoxPlot,plotthis_ViolinPlot) andfacet.scale(plotthis_BoxPlot). The colour picker and the y-axis range were handled this way too at first; both have since moved to a server-side store, for the reasons in the #338 entry below. - Added a section in the “Adding a New Module” vignette describing this pattern.
- These inputs are now wrapped in
Fixed an initialization bug in
multiColorPicker()due to string indexing rather than position, leading to out of bounds errors when a group label was an empty string.Fixed the
dittoViz_yPlotmodule re-rendering its plot when the user merely switched to the Data tab. The colour picker is built by arenderUI()on that tab, and Shiny suspends an output whose tab is hidden, so a change to the palette’s groups (setting “Multivar Aesthetic” to “color”, say, which keys the palette by variable name) could not rebuild the picker when it happened. The rebuild waited for the tab to be opened, and the value it reported then re-rendered the plot for what was only a tab click. The plot now depends on the resolved palette, the group-to-colour mapping it actually draws with (held in areactiveVal()), rather than on the picker’s raw value. A rebuilt picker re-seeded from that same resolution therefore changes nothing to re-render for, while a colour the user actually picks comes straight through.-
Fixed every module that uses
multiColorPicker()rendering its plot an extra time on initialization, and again the first time the user opened the tab the picker lives on (#338). The plot depended on the picker’s rawinput$<key>, which isNULLuntil the browser binds the widget and reports back — so the echo of a mapping the server had just seeded the picker with still counted as a change and rebuilt the plot. An attempt utilizingfreezeReactiveValue()to guard didn’t work: inside arenderUI()it pauses only the readers that run after it in the same flush, and at startup the plot output runs first, so the freeze landed too late to pause anything.- Every module now reads a server-resolved palette instead. The new exported helper
setup_group_colors()resolves the group-to-colour mapping as soon as the group set is known and holds it in areactiveVal(), which only invalidates on a real change. A rebuilt picker echoing the palette already in use costs nothing, while a colour the user picks comes straight through.piePlotandradarPlot, which had no guard at all, are covered for the first time. - The picker’s palette dropdown no longer carries an HTML
id. Shiny’s select binding claims every<select>with one, so each picker was quietly registering a strayinput[["<inputId>-palette"]]alongside its own value. The widget’s JavaScript and CSS both find that element by class, so nothing needed the id. - The “Adding a New Module” vignette’s “Updating Your Own Inputs From the Server” section now documents this pattern for
renderUI()-rebuilt widgets.
- Every module now reads a server-resolved palette instead. The new exported helper
-
The y-axis limits now leave more room for significance brackets, and no longer cost an extra render on the way in. The plot read the raw
input$y.min/input$y.max, which the module had just pushed to the browser, so their echo rebuilt it — the samefreezeReactiveValue()that could not cover the colour picker was covering these no better.-
dittoViz_yPlot,plotthis_BoxPlot,plotthis_BarPlotandplotthis_ViolinPlotnow read a server-side store, the new exportedsetup_axis_range(), so the echo of a limit the module itself set changes nothing while a limit the user types comes straight through. Startup drops a render in each. - Brackets are stacked above the data, and nothing had reserved room for them: the axis was silently rescaled at draw time to whatever they needed. Worse, that rescale was applied as an assignment rather than a maximum, so enabling statistics shrank a y-axis maximum the user had deliberately set — a plot limited to 0-20 was pulled back to the top of the brackets.
apply_stat_annotations()gains ay.maxargument and now only ever raises the top, never lowers it. - The new exported
stat_bracket_y_max()works out how high the brackets will reach, and the three modules that draw them reserve that room up front, so they.maxcontrol shows the limit actually in use. It shares the bracket packing with the drawing code, so the two agree exactly, and it honourshide.ns(on by default) rather than reserving room for brackets that are never drawn.
-
Fixed the
dittoViz_yPlotreset button callingupdateCheckboxGroupInput()on its “Plots” select, so resetting left the plot type selection untouched.Fixed axis titles not reflecting applied data adjustments in the
dittoViz_yPlot,dittoViz_scatterPlot, andlinePlotmodules (#321). The annotation-persistence feature added in 0.3.0 was re-applying the previously captured title text on every rebuild, clobbering the freshly generated adjustment-aware label (e.g.log2(units)). Axis titles carrying an active adjustment are now always regenerated, while a manually edited title with no adjustment still persists and the dragged title position persists in all cases.finalize_manual_edits()gains aregen_keysargument to drive this. Axis titles are also regenerated (rather than persisted) when the plotted variable for that axis changes, via the new exported helperreset_axis_title_text(), since a manual title only makes sense for the variable it was written for. Shared axis titles in facetedlinePlot/dumbbellPlotfigures (built viabuild_facet_annotations()) are now tagged as axis annotations so their dragged position survives label changes; as a result they now pick up the axis-title font settings rather than the facet-title font settings.The main plot title is now blank by default in the
dittoViz_yPlotanddittoViz_scatterPlotmodules (previously dittoViz’smain = "make"auto-generated a title from the variable name and regenerated it on every re-render). Users can still add a title interactively by editing it on the plot.
VizModules 0.3.0
CRAN release: 2026-07-27
New Modules
- Turned the Figure Builder into a reusable, namespaced Shiny module (
figureBuilderUI()/figureBuilderServer()), so it can be embedded inside a larger app and instantiated more than once, just like the plot modules.figureBuilderApp()is now a thin wrapper around this module and keeps its existing behaviour. The canvas CSS/JS was made namespace-safe (class-based, per-instance) so multiple builders can coexist on one page.- Panel labels (a, b, c …) now render live on the canvas as soon as they are chosen from the “Panel labels” menu (and renumber as panels are added, removed, or dragged), instead of only appearing in the exported SVG.
- Moved the Figure Builder app into an exported
figureBuilderApp()function so it can be launched directly (figureBuilderApp()), seeded with custom datasets viadata_list, extended with custom modules viamodule_registry, and returned either as ashinyApp()object or as separateui/servercomponents (return_components = TRUE). The bundledinst/apps/figure-builderapp is now a thin wrapper around this function. - Added to Gallery App.
Improved/New Functionality
- Facet/split selectors across all modules now only offer valid faceting variables. Faceting (or splitting) is restricted to categorical columns (character or factor) with fewer than 50 unique values; numeric columns and high-cardinality categoricals are no longer selectable, preventing accidental creation of an unwieldy number of panels. This is powered by a new internal helper,
.facet_check(), whose output populates the facet/split input choices. - Simplified boxplot outlier hiding to rely on native plotly
boxpoints = FALSEbehaviour (via ggplot2’soutlier.shape = NAin theplotthis_BoxPlotmodule anddittoViz::yPlot’sboxplot.show.outliersargument indittoViz_yPlot), rather than post-hoc marker manipulation. Removed the now-unused internal helper.remove_boxplot_outliers(). This is more robust with plotly 4.12.0+. - Added a new reusable custom Shiny input,
multiDynamicInput()(withupdateMultiDynamicInput()), that lets users dynamically add and remove rows of heterogeneous inputs. Each row is described by a genericrow_spec(a named list of field specs using either atypealias —select,text,numeric,slider,checkbox,colour— or an arbitrary input constructor viafn), a+ Addbutton appends rows, each row has anXdelete button, and fields wrap to a new line aftermax_per_row(default 4). The value returned to the server is a named list of rows (model1,model2, …), each a named list keyed by the field names. Add/delete are handled client-side, and values are read back generically via each field’s registered Shiny input binding, so any input type is supported.- Added vignette
vignette("using-custom-shiny-inputs")documentingmultiDynamicInput()usage: row_spec definition, pre-filling withelements, reading values, and server-side updates.
- Added vignette
- Added generic modeling capabilities to
dittoViz_scatterPlot module. The module’s custom-model feature now supports multiple models at once viamultiDynamicInput(): add as many rows as you like, each with its own model type (lm/glm/loess/nls), formula, line colour, and line width, and every valid model is fitted against the active (filtered) data and overlaid as its own line (respecting faceting). Formulas are validated by the internal.safe_build_model()helper to ensure safety.- This includes the ability to add custom model backends via
register_model_backend(),get_model_backend(),list_model_backends(), andbuild_model_row_spec(). Backends declare afitfunction, apredictfunction, validated output classes, and optional extra UIfieldsthat appear/hide dynamically based on the selected model type. The four built-in backends (lm, glm, loess, nls) are registered automatically at package load. Extra UI fields from backends are forwarded tofit()via.... - Added vignette
vignette("custom-model-lines")documenting the model backend registry: how the pipeline works, setting model defaults, registering custom backends (with drc and mgcv examples), and how extra fields flow through to the fit function.
- This includes the ability to add custom model backends via
- Pass
defaults,hide.inputs, andhide.tabsarguments to the module app factory functions in all module app wrappers, so that users can pre-fill or hide controls when testing modules in isolation. - More intelligent input hiding logic so that when individual inputs are hidden (via
hide.inputsor dynamically in response to other inputs), the remaining controls reflow to fill the space and no empty gaps are left in the UI. Input grids are now laid out with a wrapping flexbox container viaorganize_inputs(). Optional elements are handled gracefully. - Added continuous color-scale trimming controls (“Lower Quantile”, “Upper Quantile”, “Lower Cutoff”, and “Upper Cutoff”) to the
plotthis_DotPlot,plotthis_BarPlot, andplotthis_SplitBarPlotmodules, exposing the newlower_quantile/upper_quantile/lower_cutoff/upper_cutoffarguments from plotthis 0.13.0. These controls appear only when the selected fill column is numeric. - Added dot border controls (“Border Color” and “Border Size”) to the
plotthis_DotPlotmodule, exposing the newborder_colorandborder_sizearguments from plotthis 0.13.0.border_coloris limited to a single constant color in the module UI. - Updated the
plotthis_DotPlot“Fill Cutoff” control to pair a numeric value with a new “Fill Cutoff Direction” selector (<,<=,>,>=), matching plotthis 0.13.0’s string-expressionfill_cutoff(e.g."< 18"). - Added annotation persistence, i.e. annotation positions persist when the plot is re-rendered. This extends to axis/facet titles and custom annotations, which means much less finagling during iterative editing.
Bug Fixes
- Fixed broken input hiding when using
hide.inputsandhide.tabsarguments in module app wrappers due to lazy UI injection viarenderUI, which effectively overwrote thehidecalls.renderUIalso re-renders the input UIs every time a dataset changes - now if the dataset changes, the inputs are re-rendered but thehidecalls are re-applied to maintain the hidden state. - Fixed an error in
plotthis_SplitBarPlotwhere the categorical text position input was not respected if the axes were flipped. Now the text position input is respected regardless of axis orientation. - Export numerous internal helper functions for use in custom modules, particularly those related to axes, faceting, and layouts. It became apparent these were necessary as initial work began on
sciVizModules. - Fixed a bug in
dittoViz_yPlotwhere plot selection and outlier hiding were not respected appropriately due to a typo in theboxplot.show.outliersinput name. - Fixed a bug in
dittoViz_scatterPlotwhere 2split.byinputs caused an error due to improper checks for empty strings on a vector of elements. - Fixed a bug in
dittoViz_scatterPlotwhere highlight aesthetics weren’t applied when a categorical x-axis was used.
VizModules 0.2.0
CRAN release: 2026-06-16
- Created the Figure Builder app so that users can dynamically construct multi-panel figures using different data sets and plot types on a single page. Allows for full page SVG export, source data dump organized per panel, and full customization of plot position and size.
- All
*OutputUI()functions gained aresizableargument (defaultTRUE). WhenFALSE, the plot output is no longer wrapped inshinyjqui::jqui_resizable(), which avoids a redundant resize handle when the output is embedded in a container that already provides resizing (such as the Figure Builder app cards). - Added a new
plotthis_DotPlotmodule (plotthis_DotPlotInputsUI(),plotthis_DotPlotOutputUI(),plotthis_DotPlotServer(), and theplotthis_DotPlotApp()convenience wrapper) that wrapsplotthis::DotPlot()for interactive dot plots, including a custom dot-size legend since plotly still lacks that capability. - Added the
example_markersdataset, a simulated single-cell marker-gene expression table (immune cell types × marker genes) used as the default example data for the DotPlot module. - Added “Source Data” download button at the bottom of every module’s control panel. The button creates and downloads a ZIP file containing a self-contained HTML of the plotly plot, a CSV of the plot data (retrieved via
plotly::plotly_data()), and for modules with statistics enabled (Box / Violin / yPlot), a table of the statistics info. Source downloads are now built from the exportedcollect_source_data()andcreate_source_download_handler()helpers, and each module server returns its source reactive so it can be reused (e.g. by the Figure Builder). Given source data is now required by many journals, this is important. - Removed old interactive plot download button and associated helper function.
- Removed old dynamically hidden stats download button and associated logic, since stats are now included in the source download when applicable.
- Statistic helper functions are now exported allowing users to annotate plotly graphs with custom statistics:
compute_pairwise_stats(),create_stat_annotations(),apply_stat_annotations(),generate_pair_strings(), andparse_pair_strings(). - Exposed
empty_plot()for use as a placeholder, e.g. if parameters aren’t valid for a given plot type, to pass that info to user without ugly error messages. - Faceting improvements - new internal helpers that control subplot spacing, subplot size, and facet_scale handling. This fixes much of the wonkiness for plots with many panels. Uniform inputs added for panel spacing across all modules.
- Axis titles now uniformly added as annotations to allow interactive repositioning.
- Condensed package wide workflows with simple helpers, e.g.
apply_title_layout(), resulting in significantly less jank. - Axis adjustments are now properly reflected in axis/legend titles for appropriate modules, e.g.
yPlot,scatterPlot,linePlot. - Removed a handful of spurious/non-functional inputs, particularly for the
dittoViz_scatterPlotmodule. - Custom
size.bylegends added forplotthis_DotPlotanddittoViz_scatterPlotmodules, since plotly does not yet support these. - Update docstrings to reflect new inputs and features and clarify which parameters of underlying plotting functions may not be implemented.
- Various border fixes for faceted plots.