ggpaintr turns one ggplot-like formula string into a small Shiny app with generated controls, rendered plots, generated code, upload support, copy customization, custom Shiny integration hooks, and custom placeholder registries.
Installation
# install.packages("pak")
pak::pkg_install("willju-wangqian/ggpaintr")Core concepts
ggpaintr treats a ggplot-like formula string as a template with placeholders that become Shiny inputs.
-
varselects a data column in the generated UI -
textcollects free text -
numcollects numeric input -
exprcollects raw R code for places like faceting or labels -
uploadlets the app use uploaded data -
ptr_normalize_column_names()cleans local column names beforevarselection when the source data is not already syntactic - you can register your own placeholder types per app with
ptr_define_placeholder()
upload currently supports .csv and .rds.
"ggplot(data = iris, aes(x = var, y = var)) +
geom_point(size = num, color = text) +
labs(title = text) +
facet_wrap(expr)"Why this design
ggpaintr keeps the author-facing input as one formula string on purpose.
- formula strings stay close to the way many R users already sketch ggplot code
- the same parsed object drives generated controls, runtime completion, and generated code
- one placeholder registry powers parse, UI, runtime, and copy rules for both built-in and custom placeholders
- the package gives you both a default wrapper and a supported Shiny embedding layer, so you can start simple and grow into a more custom app
Quick start
Most users should start with ptr_app().
Supported public API
The maintained public path is intentionally narrow.
- Start with
ptr_app()for the default app wrapper, orptr_app_bslib()for abslib-themed variant that doubles as a worked example of building custom shells from the public API. - Use
ptr_server(),ptr_server_state(),ptr_build_ids(), and theptr_register_*()helpers when you need to embedggpaintrin a larger Shiny app. - Use
ptr_define_placeholder()andptr_merge_placeholders()for custom placeholder types. - Use
ptr_runtime_input_spec(),ptr_parse_formula(),ptr_exec(), andptr_assemble_plot()for advanced runtime or testing workflows.
Other helpers in R/ are internal implementation support rather than part of the maintained community-facing API.
Feature tour
Work with uploaded data
Use upload when the app should let users supply a dataset at runtime.
ptr_app("
ggplot(data = upload, aes(x = var, y = var)) +
geom_point(size = num) +
labs(title = text)
")The upload control currently supports .csv and .rds.
Uploads are normalized through the same column-name helper automatically after read-in. .rds uploads must already be tabular or be coercible with as.data.frame().
Prepare local data with non-syntactic column names
If your local data uses spaces or punctuation in column names, normalize it once before you build the app.
messy_sales <- data.frame(
left = 1:4,
right = c(2, 4, 6, 8),
check.names = FALSE
)
names(messy_sales) <- c("first column", "second-column")
sales <- ptr_normalize_column_names(messy_sales)
names(sales)
ptr_app("
ggplot(data = sales, aes(x = var, y = var)) +
geom_point()
")Put transforms around var in the formula
var is a column picker. When you want derived mappings, wrap the placeholder in the formula itself and let the selected column slot into that expression.
ptr_app("
ggplot(data = mtcars, aes(x = var + 1, y = log(var))) +
geom_point()
")Customize control text with ui_text
Use ui_text to change user-facing labels without changing the runtime behavior of the generated app.
custom_copy <- list(
shell = list(
draw_button = list(label = "Render plot")
),
params = list(
x = list(var = list(label = "X axis variable")),
y = list(var = list(label = "Y axis variable")),
title = list(text = list(label = "Plot title"))
)
)
ptr_app(
"ggplot(data = iris, aes(x = var, y = var)) +
geom_point() +
labs(title = text)",
ui_text = custom_copy
)Embed ggpaintr in your own Shiny app
The supported integration layer lets you keep your own layout while reusing the ggpaintr state and bind helpers.
library(shiny)
ids <- ptr_build_ids(
control_panel = "builder_controls",
draw_button = "render_plot",
plot_output = "main_plot",
error_output = "main_error",
code_output = "main_code"
)
ui <- fluidPage(
sidebarLayout(
sidebarPanel(ptr_input_ui(ids = ids)),
mainPanel(ptr_output_ui(ids = ids))
)
)
server <- function(input, output, session) {
ptr_state <- ptr_server_state(
"ggplot(data = iris, aes(x = var, y = var)) +
geom_point() +
labs(title = text)",
ids = ids
)
ptr_setup_controls(input, output, ptr_state, ids = ids)
ptr_register_draw(input, ptr_state, ids = ids)
ptr_register_plot(output, ptr_state, ids = ids)
ptr_register_error(output, ptr_state, ids = ids)
ptr_register_code(output, ptr_state, ids = ids)
}
shinyApp(ui, server)Customize the plot in your own renderPlot()
If you want to keep the ggpaintr runtime but take over plot rendering, use ptr_extract_plot() inside your own renderPlot().
server <- function(input, output, session) {
ptr_state <- ptr_server_state(
"ggplot(data = iris, aes(x = var, y = var)) + geom_point()"
)
ptr_setup_controls(input, output, ptr_state)
ptr_register_draw(input, ptr_state)
ptr_register_error(output, ptr_state)
ptr_register_code(output, ptr_state)
output$outputPlot <- renderPlot({
plot_obj <- ptr_extract_plot(ptr_state$runtime())
if (is.null(plot_obj)) {
plot.new()
return(invisible(NULL))
}
plot_obj + ggplot2::theme_minimal()
})
}Register a custom placeholder type
Use ptr_define_placeholder() and ptr_merge_placeholders() when you want to add a new control type without editing package internals.
sales <- data.frame(
day = as.Date("2024-01-01") + 0:4,
value = c(10, 13, 12, 16, 18)
)
date_placeholder <- ptr_define_placeholder(
keyword = "date",
build_ui = function(id, copy, meta, context) {
shiny::dateInput(id, copy$label)
},
resolve_expr = function(value, meta, context) {
if (is.null(value) || identical(as.character(value), "")) {
return(ptr_missing_expr())
}
rlang::expr(as.Date(!!as.character(value)))
},
copy_defaults = list(label = "Choose a date for {param}")
)
placeholders <- ptr_merge_placeholders(
list(date = date_placeholder)
)
obj <- ptr_parse_formula(
"ggplot(data = sales, aes(x = day, y = value)) +
geom_line() +
geom_vline(xintercept = date)",
placeholders = placeholders
)
names(obj$placeholders)
#> [1] "var" "text" "num" "expr" "upload" "date"
ptr_app(
"ggplot(data = sales, aes(x = day, y = value)) +
geom_line() +
geom_vline(xintercept = date)",
placeholders = placeholders
)Advanced developer workflow
Use the low-level runtime helpers directly when you want to inspect generated code, write tests, or build developer tooling around parsed formulas.
ptr_runtime_input_spec() is the supported way to discover the runtime input ids needed by ptr_exec().
obj <- ptr_parse_formula(
"ggplot(data = mtcars, aes(x = var, y = var)) +
geom_point(size = num) +
labs(title = text)"
)
spec <- ptr_runtime_input_spec(obj)
spec
#> input_id role layer_name keyword param_key source_id
#> 1 ggplot+3+2 placeholder ggplot var x ggplot+3+2
#> 2 ggplot+3+3 placeholder ggplot var y ggplot+3+3
#> 3 geom_point+2 placeholder geom_point num linewidth geom_point+2
#> 4 labs+2 placeholder labs text title labs+2
#> 5 geom_point+checkbox layer_checkbox geom_point <NA> <NA> <NA>
#> 6 labs+checkbox layer_checkbox labs <NA> <NA> <NA>
inputs <- setNames(vector("list", nrow(spec)), spec$input_id)
checkbox_rows <- spec$role == "layer_checkbox"
inputs[checkbox_rows] <- rep(list(TRUE), sum(checkbox_rows))
inputs[["ggplot+3+2"]] <- "mpg"
inputs[["ggplot+3+3"]] <- "disp"
inputs[["geom_point+2"]] <- 2
inputs[["labs+2"]] <- "Mtcars scatter"
runtime <- ptr_exec(obj, inputs)
runtime$code_text
#> [1] "ggplot(data = mtcars, aes(x = mpg, y = disp)) +\n geom_point(size = 2) +\n labs(title = \"Mtcars scatter\")"
inherits(runtime$plot, "ggplot")
#> [1] TRUEFor upload-backed formulas, the spec also includes the derived dataset-name input that accompanies each upload control.
upload_obj <- ptr_parse_formula(
"ggplot(data = upload, aes(x = var, y = var)) + geom_point()"
)
ptr_runtime_input_spec(upload_obj)
#> input_id role layer_name keyword param_key source_id
#> 1 ggplot+2 placeholder ggplot upload data ggplot+2
#> 2 ggplot+2+name upload_name ggplot upload data ggplot+2
#> 3 ggplot+3+2 placeholder ggplot var x ggplot+3+2
#> 4 ggplot+3+3 placeholder ggplot var y ggplot+3+3
#> 5 geom_point+checkbox layer_checkbox geom_point <NA> <NA> <NA>Current stability guarantees
- built-in placeholders are
var,text,num,expr, andupload - custom placeholders share the same parse, UI, runtime, and copy-rule path as built-ins
-
uploadcurrently supports.csvand.rds - the supported app-facing surface is the current
ptr_*wrapper and Shiny-integration layer - only the five top-level ids exposed by
ptr_build_ids()are configurable - runtime failures are labeled as
Input errororPlot errorand stay on the shared inline error path
Not guaranteed / implementation details
- raw placeholder ids such as
"ggplot+3+2"are not a stable hand-authored API; discover them withptr_runtime_input_spec() - deeper traversal details such as
index_pathencoding and internal companion id conventions remain package internals - unsupported upload formats are outside the current boundary
- the formula-string model is the current author-facing interface; future hardening should compile it into a richer internal runtime contract rather than replace that authoring model outright
Current behavior boundary
- Structural formula errors fail early during parsing.
-
varwith no data source fails while preparing the UI. -
varis a column picker. Formula-level transforms such asvar + 1orlog(var)are supported inside the formula text, not as direct input values. - For local data with non-syntactic names, call
ptr_normalize_column_names()first; uploads apply the same normalization automatically. - Missing local data objects are deferred to draw-time inline errors.
- Advanced integrations can customize the plot through
ptr_extract_plot().
Where to go next
See vignette("ggpaintr-workflow") for the main workflow and vignette("ggpaintr-extensibility") for supported Shiny integration recipes. See vignette("ggpaintr-placeholder-registry") for the placeholder registry API and hook contract.