The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
VizModules ships two custom Shiny inputs that go beyond standard
shiny::*Input widgets: multiColorPicker
and multiDynamicInput. multiColorPicker
maps colors to the discrete levels of a variable using palettes or
per-group hex pickers, while multiDynamicInput is a
general-purpose widget that lets users dynamically add and remove rows
of heterogeneous inputs.
multiColorPicker assigns a color to each level of a
discrete variable. Users can apply a named palette to every group at
once, or fine-tune individual groups with a native color picker and an
editable hex field. The value reported to input[[inputId]]
is a named character vector of hex colors keyed by group. It is used
throughout the package wherever a plot maps a discrete variable to color
for more granular control than just a palette selection.
The widget renders a palette selector with Apply and Reset buttons, a preview row of swatches, and one row per group. Selecting a palette and clicking Apply recolors every group in order; the per-group color picker and hex input let users override any single group afterwards.
library(shiny)
library(VizModules)
ui <- fluidPage(
br(),
splitLayout(
multiColorPicker(
"dept_cols",
label = "Plot colors",
groups = c("Engineering", "Finance", "HR",
"Marketing", "Operations", "Sales"),
selected_palette = "dittoColors"
),
verbatimTextOutput("value")
)
)
server <- function(input, output, session) {
output$value <- renderPrint(input$dept_cols)
}
shinyApp(ui, server)| Argument | Description |
|---|---|
inputId |
Shiny input id. The value is accessed via
input[[inputId]]. |
label |
Optional label displayed above the control. |
groups |
Character or factor vector of group names — one color row is created per unique value. |
palette_options |
Named list of palettes (each a character vector of colors). Defaults
to default_palettes(). |
selected_palette |
Name of the palette to preselect. |
colors |
Optional named vector of starting colors, matched to
groups by name. |
show_text |
If TRUE (default), show editable hex text inputs beside
the color pickers. |
compact |
If TRUE, render a tighter layout with smaller
controls. |
panel |
If FALSE, remove the surrounding panel/well
styling. |
The value in input[[inputId]] is a named character
vector of hex colors keyed by group:
Use updateMultiColorPicker() to set specific colors,
apply a palette by name, or reset the widget to its initial state:
# Set specific group colors (only named groups change)
updateMultiColorPicker(session, "dept_cols",
colors = c(Engineering = "#1B9E77", Finance = "#D95F02"))
# Apply a named palette to all groups
updateMultiColorPicker(session, "dept_cols", palette = "ggplot2")
# Reset colors and palette back to the initial state
updateMultiColorPicker(session, "dept_cols", reset = TRUE)Exactly one of colors, palette, or
reset should be supplied per call; reset takes
precedence, followed by palette, then
colors.
multiColorPicker() — place in your UI, one color row
per groupinput[[inputId]] — read the named character vector of
hex colors in your serverupdateMultiColorPicker() — set colors, apply a palette,
or reset from the servermultiDynamicInput renders a “+ Add” button and a
container. Each time the user clicks Add, a new row appears with
whatever fields you define. An “×” button on each row deletes it. The
collected value is reported to input[[inputId]] as a named
list of rows.
Unlike insertUI/removeUI, rows are managed
entirely client-side via DOM template cloning — no server round-trips
for add/delete.
library(shiny)
library(VizModules)
ui <- fluidPage(
br(),
splitLayout(
multiDynamicInput(
"layers",
label = "Layers",
row_spec = list(
colour = list(
type = "colour",
args = list(value = "#000000")
),
size = list(
type = "numeric",
args = list(value = 2, min = 0.5, max = 10)
),
style = list(type = "select",
args = list(choices = c("solid", "dashed", "dotted")))
)
),
verbatimTextOutput("value")
)
)
server <- function(input, output, session) {
output$value <- renderPrint(input$layers)
}
shinyApp(ui, server)| Argument | Description |
|---|---|
inputId |
Shiny input id. The value is accessed via
input[[inputId]]. |
label |
Label shown next to the + Add button. Also determines row naming (lowercased). |
row_spec |
Named list defining one row’s fields. Each entry has
type (or fn) and args. |
elements |
Optional list of pre-filled rows to show on startup. |
max_per_row |
Number of fields per visual line before wrapping (default 4). |
panel |
If FALSE, removes the border/background styling. |
row_specEach field in row_spec is a named list with:
type — a shorthand alias:
"select", "text", "numeric",
"slider", "checkbox", "colour" /
"color".fn — alternatively, any input
constructor function (e.g. shiny::dateInput). Use this for
inputs without a built-in alias.args — a list of arguments passed to
the constructor (everything except inputId, which is
auto-generated per row).# Using type aliases
row_spec = list(
name = list(type = "text", args = list(placeholder = "Enter name")),
age = list(type = "numeric", args = list(value = 25, min = 0, max = 120))
)
# Using fn for a custom input
row_spec = list(
date = list(fn = shiny::dateInput, args = list(label = "Date", value = Sys.Date()))
)elementsTo show rows on startup with specific values:
multiDynamicInput(
"models",
label = "Models",
row_spec = list(
model_type = list(type = "select", args = list(choices = c("lm", "glm"))),
formula = list(type = "text", args = list(placeholder = "y ~ x"))
),
elements = list(
models1 = list(model_type = "lm", formula = "revenue ~ units"),
models2 = list(model_type = "glm", formula = "revenue ~ poly(units, 2)")
)
)The names (models1, models2) don’t matter
for input — they’re auto-generated from the label at runtime. The values
inside each element must match the row_spec field
names.
The value in input[[inputId]] is a named list of
rows:
# With label = "Models" and two rows filled in:
input$models
#> list(
#> models1 = list(model_type = "lm", formula = "revenue ~ units"),
#> models2 = list(model_type = "glm", formula = "revenue ~ poly(units, 2)")
#> )Row names are derived from the label argument
(lowercased) plus an index: models1, models2,
etc.
Use updateMultiDynamicInput() to replace all rows or
clear them:
When used with the model backend system (see
vignette("custom-model-lines")), fields can be tagged with
a backend attribute. These fields are hidden by default and
only appear when the matching model type is selected:
row_spec = list(
model_type = list(type = "select", args = list(choices = c("lm", "drm"))),
formula = list(type = "text", args = list(placeholder = "y ~ x")),
# Only visible when model_type == "drm"
drc_fct = list(type = "select", backend = "drm",
args = list(choices = c("LL.4", "LL.3")))
)In practice you don’t build this manually —
build_model_row_spec() does it for you by collecting
fields from all registered backends.
multiDynamicInput() — place in your UIinput[[inputId]] — read the named list of rows in your
serverupdateMultiDynamicInput() — set or clear rows from the
serverbuild_model_row_spec() — auto-build a row_spec from
registered model backendsBoth widgets above follow the same shape, and it is the one to copy
if you add a third: a constructor in R/, a JS binding and a
stylesheet in inst/src/, and an
htmlDependency() tying them together.
A custom input’s stylesheet is injected into the host app’s
document. There is no shadow DOM and no automatic scoping, so a
rule written for your widget applies to the whole page. Since every plot
module renders a multiColorPicker, attaching any module to
an app brings these stylesheets along — which is how a single unscoped
selector came to break stock selectInput() dropdowns in
apps that never used the picker directly (#355).
Anchor every selector on a class you invented:
/* WRONG — `.selectize-dropdown`, `.option` and `.optgroup-header` all belong to
selectize, so this restyles every dropdown on the page, including plain
selectInput()s and DT's column filters. */
.selectize-dropdown .selectize-dropdown-content {
display: flex;
flex-direction: column;
}
/* RIGHT */
.mc-palette-dropdown .selectize-dropdown-content {
display: flex;
flex-direction: column;
}The mistake is easy to make because the wrong version looks scoped — a two-part selector reads like “the content inside my dropdown”. The test is the leftmost class: if you did not invent it, the rule is not scoped.
A dropdown or popover often has to be parented to
<body> so a panel with overflow: hidden
cannot clip it. That is precisely what makes it unreachable from a
.my-widget .thing selector — and precisely why the leaking
rules above were written unscoped. The answer is to give the escaped
element its own class:
$(select).selectize({
dropdownParent: "body",
// Selectize replaces its default dropdownClass wholesale, so restate its own
// class alongside ours.
dropdownClass: "selectize-dropdown mc-palette-dropdown",
...
});multiColorPicker does exactly this, which is why
multiColorPicker.css can scope all fourteen of its dropdown
rules to .mc-palette-dropdown.
Before trusting a rule, inspect the element and check the class is
really on it. One of the picker’s rules targeted .option,
but its custom render.option emits
.mc-palette-option and selectize never adds
.option to it — so the rule matched nothing in the picker
and only affected other widgets.
style=
attributes. They can only be overridden with
!important. Put layout in the stylesheet and pass
per-instance values as CSS custom properties
(style = "--my-columns: 3;").gap needs nothing from the parent.min-width: 0, or
long content refuses to shrink and pushes the widget wider than its
container.htmlDependency() pointing at
inst/src/, attached to the widget’s own markup with
htmltools::attachDependencies(), so the styles travel with
it through a runtime insertUI().tests/testthat/test-ui_utils.R parses every selector in
every stylesheet the package ships — bundled files and inline
<style> blocks alike — and fails on any that is not
anchored to a package-owned prefix. Add yours to that list when you
introduce a widget.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.