Overview
ggpaintr still ships the default ptr_app()
and ptr_server() wrappers, but it now also exposes a
supported integration layer for users who already have a Shiny app and
want to embed the generated ggpaintr controls and runtime
into their own UI.
The supported integration pieces are:
ptr_build_ids()ptr_server_state()ptr_setup_controls()ptr_register_draw()ptr_register_plot()ptr_register_error()ptr_register_code()ptr_extract_plot()ptr_extract_error()ptr_extract_code()ptr_input_ui()ptr_output_ui()
To compose and distribute your own Shiny app, use
ptr_server_state() together with the
ptr_register_* helpers; see ?ptr_app_bslib for
a worked example.
Choosing the right surface
The current package surface is intentionally layered.
- use
ptr_app()orptr_server()for most apps - use
ptr_server_state()plus theptr_register_*()helpers when you already own a larger Shiny layout and want to embedggpaintr - use the low-level
ptr_*runtime helpers, together withptr_runtime_input_spec(), when you are writing tests, tooling, or package-level extensions around parsed formulas
That split is a little opinionated, but it keeps the beginner path small while still exposing a supported integration seam for more advanced Shiny work.
Recipe 1: Embed ggpaintr with the default binders
library(ggpaintr)
library(shiny)
ui <- fluidPage(
titlePanel("Embedded ggpaintr"),
sidebarLayout(
sidebarPanel(
ptr_input_ui()
),
mainPanel(
ptr_output_ui()
)
)
)
server <- function(input, output, session) {
# Parse the formula and create reactive state — holds the placeholder map,
# the last runtime result, and the current input spec.
ptr_state <- ptr_server_state(
"ggplot(data = iris, aes(x = var, y = var)) +
geom_point() +
labs(title = text)"
)
# Register dynamic var-column selectors and the file upload handler.
# Must be called before ptr_register_draw() so data is available to var inputs.
ptr_setup_controls(input, output, ptr_state)
# Observe the draw button; re-run ptr_exec() and update ptr_state$runtime()
# each time it is clicked.
ptr_register_draw(input, ptr_state)
# renderPlot(): reads ptr_state$runtime()$plot and renders it.
ptr_register_plot(output, ptr_state)
# renderText() / renderUI(): reads ptr_state$runtime()$error and shows it.
ptr_register_error(output, ptr_state)
# renderText(): reads ptr_state$runtime()$code_text and shows generated code.
ptr_register_code(output, ptr_state)
}
shinyApp(ui, server)This is the closest supported equivalent to
ptr_server(), but it lets you place the controls and
outputs inside your own page layout.
Recipe 2: Customize top-level ids
Use ptr_build_ids() when your app already has its own
naming scheme or you want to mount ggpaintr outputs in
specific UI containers.
ids <- ptr_build_ids(
# The top-level ids that ggpaintr uses for its standard UI elements.
# Provide custom strings when your app already uses those default id names
# for something else, or when you want ggpaintr outputs in specific containers.
control_panel = "builder_controls", # tabsetPanel that holds per-layer control tabs
draw_button = "render_plot", # actionButton that triggers plot re-render
plot_output = "main_plot", # plotOutput widget
error_output = "main_error", # output for parse / plot error messages
code_output = "main_code" # verbatimTextOutput for the generated ggplot code
)
ids
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()",
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)
}Only the five top-level ids are configurable in this phase. Internal
placeholder ids and dynamic var-* output ids remain
package-owned.
Recipe 3: Customize the returned plot in
renderPlot()
The default ptr_register_plot() binder preserves the
current wrapper behavior. If you want to modify the built plot before
rendering, write your own renderPlot() and use
ptr_extract_plot().
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({
# ptr_state$runtime() is a reactive value updated each time the draw button
# is clicked. It holds the full result of ptr_exec() for the current inputs.
# ptr_extract_plot() pulls the ggplot object out of that result,
# returning NULL when the plot failed or the draw button hasn't been clicked.
plot_obj <- ptr_extract_plot(ptr_state$runtime())
if (is.null(plot_obj)) {
plot.new() # show a blank canvas instead of an error frame
return(invisible(NULL))
}
# Modify the ggplot object freely before it is rendered.
# Here we apply a theme; you could also add layers, scales, or annotations.
plot_obj + ggplot2::theme_minimal()
})
}ptr_extract_plot() returns the raw ggplot
object on success and NULL otherwise. That keeps the
advanced customization seam side-effect free and lets you decide how to
render failure states in your own app.
Pure helpers versus bind helpers
Use the bind helpers when you want the standard ggpaintr
behavior with custom layout or custom top-level ids.
Use the pure value helpers when you want to own the rendering details:
-
ptr_extract_plot()for plot customization -
ptr_extract_error()for customrenderUI()workflows -
ptr_extract_code()for customrenderText()or downstream processing
How to improve the advanced surface
The current advanced surface is powerful, but it is still closer to package developer tooling than to a polished app-builder DSL. The next improvements should stay incremental:
- improve docs and helper discoverability first
- add higher-level helper constructors for common placeholder patterns only after the current low-level registry contract has settled a bit more
- consider an optional Shiny module wrapper on top of the current id-based helpers rather than replacing the existing supported surface
- do not introduce a separate declarative end-user spec in this phase
Roadmap for the formula-string runtime
The formula-string model is still the author-facing interface. To make the runtime easier to reason about without giving up that authoring model, the next hardening step should happen internally:
- keep formula strings as the public authoring format
- compile each parsed
ptr_objonce into a richer internal runtime contract - have runtime completion consume that compiled contract instead of repeatedly relying on raw expression walks and companion-id conventions
That approach would tighten the runtime semantics without forcing users to rewrite formulas into a separate declarative language.