ptr_server() into an
interactive, brushable plot. ptr_ggplotly() converts the
state’s current ggplot into a plotly htmlwidget — minting
the per-row selection keys and wiring the crosstalk source
internally, so the call is state-first
(ptr_ggplotly(state)). ptr_plotly_selection()
returns a reactive of the rows the user brushed or clicked on that plot
(mode = "rows" and other projections), reset on each draw.
Together they support a full brush-to-table loop. See
?ptr_ggplotly and ?ptr_plotly_selection.ptr_options(gate_draw = ) toggles the
“Update plot” button. When TRUE (the default) the plot
re-renders only on the button click, batching placeholder changes; when
FALSE the button is omitted and the plot re-renders
reactively on every placeholder change (live mode). It also governs the
grid host’s “Draw all” button. Read once when the app is built, so set
it before ptr_app() / ptr_ui().ptr_options(suppress_warnings = )
silences R warnings emitted while the plot is drawn
(e.g. loess “all data on boundary of neighborhood” /
“Failed to fit group N”). Default FALSE. Only the
plot-drawing step is wrapped; errors still propagate to the inline error
pane.ppExpr safety messages are now
truthful — when a runtime ppExpr input is
rejected, the abort reports the actual reason the expression was blocked
rather than a generic message.name →
topic. The shipped primer instructs models to call
ggpaintr_docs(topic), but the registered
ellmer tool schema (and ptr_llm_topic()’s
parameter) named the argument name, so a model following
the primer emitted a key the schema rejected. The schema argument and
ptr_llm_topic() now agree on topic.ggpaintr-tutorial vignette as its single built
vignette.ptr_define_placeholder_value() /
ptr_define_placeholder_consumer() /
ptr_define_placeholder_source():
positional_arg → parse_positional_arg,
named_args → parse_named_args,
validate_input → validate_session_input, and
runtime → embellish_eval. The rename makes the
slot family legible — two slots parse the formula’s default
args into the widget seed (parse_positional_arg,
parse_named_args), one validates the live session
value (validate_session_input), and one defines the
plain-R evaluation of the embellished expression
(embellish_eval). This is a hard break with no deprecation
shim: passing an old name now raises R’s “unused argument” error. The
internal registry-record keys are unchanged
(entry$default_arg / named_args /
validate_input / runtime); only the public
argument names changed.embellish_eval defaults to the new
embellish_identity() for value/consumer
placeholders (the source role keeps its abort-guard default). Two
built-in helpers ship for the plain-R evaluation slot:
embellish_identity() (the identity
function(x, ...) x) and
embellish_symbol_to_string() (NSE-captures its argument and
returns column names as a character vector, so a column-selecting
consumer works inside a tidyselect verb when the placeholder-embellished
formula is run as ordinary R with no app).ptr_arg_numeric_vector() was removed.
Use ptr_arg_numeric(vector = TRUE). The argument-validator
factories (ptr_arg_numeric / ptr_arg_string /
ptr_arg_symbol_or_string) gained a
vector = FALSE flag (with length = honored in
vector mode for ptr_arg_numeric), and a new
ptr_arg_symbol() factory parses a bareword column name
(vector-capable) for multi-column positional defaults.ptr_app_bslib() and ptr_app_grid()
are no longer exported (changed to
@keywords internal). They remain available internally and
their help pages are still generated so existing cross-reference links
resolve, but they are no longer presented as public API; the public
turn-key entry point is ptr_app(). Write a thin wrapper on
the public primitives for a custom page shell or theme.ptr_wrap_placeholder_addin() wraps a highlighted selection
in a placeholder from the RStudio command palette (classifies the
selection, offers a placeholder picker, rewrites in place;
dark/light-aware via rstudioapi::getThemeInfo()).@examples for the formula-capturing entry points
(ptr_app, ptr_ui_controls,
ptr_ui_page, ptr_server) lead with the
unquoted ggplot expression (the canonical input), keeping the string
form as the documented fallback; Shiny examples are gated with
if (interactive()).~ formula is treated as a first-class atomic
in the canonical-pipeline lift, so the resugar pass no longer
shreds a formula inside ppExpr
(e.g. lm(ppExpr(y ~ x), data = ppUpload(d))); placeholders
inside the formula are still detected and wired because
translate_call() recurses into every argument
independently.~ (gear ~ cyl) instead of falling
through to a prefix ~(gear, cyl), and an explicit
parenthesis node (x) now renders as a balanced
(x) instead of the unparseable ((x).ptr_shared() now accepts quoted ggplot
expressions as formulas elements, alongside
strings. Each element of formulas may be a formula
string (as before) or a quoted expression built with
rlang::expr() / quote(); quoted expressions
are deparsed to their source and the two forms are interchangeable,
including in a single mixed list. This brings the multi-formula
coordinator in line with the string-or-expression input already accepted
by ptr_app() / ptr_server(). A built ggplot
object (whose source text is unrecoverable) is rejected with a message
pointing at expr(). Additive and fully backward-compatible
— existing all-string call sites are unchanged. (As with the
single-formula entries, a native pipe |> inside a quoted
expression is desugared by R before capture and so does not survive into
the generated code panel.)ptr_define_placeholder_source() no
longer accepts a
companion_id_fn = function(id) -> string callback; pass
shortcut = TRUE (single logical, default
FALSE) to opt into the env-shortcut sibling input. The
framework stamps
node$shortcut_id <- paste0(node$id, "_shortcut") on
every translated source node (the old node$companion_id
slot and the "_name" suffix are gone). Built-in
ppUpload migrates verbatim. The reserved shared key
"shortcut" aborts translation with a named-conflict error.
Hard rename, no lifecycle::deprecate_warn() shim. The same
surface ships four further behaviours:
node$auto_name, stamped at translate time for non-shared
sources (node$default %||% node$id) and at runtime for
shared sources (paste0(obj$id, "_", key)). When the
shortcut textbox is empty, substitute_pass splices
as.name(node$auto_name) so the rendered call always
references a real binding.ppUpload, picking a
file synchronously clears the shortcut textbox (the auto-name takes over
as the binding name); typing into the textbox synchronously clears any
pending file. Typing always means “look up this object in the caller
env”, never “rename the uploaded payload”.<auto-name> <- read.csv("<filename>") (or
read.delim, readxl::read_excel,
jsonlite::fromJSON, … per extension) — so the rendered
snippet reads top-to-bottom as a self-contained script.state$bound_names[[key]],
state$eval_env[[key]], and
state$active_uploads[[key]]. ptr_spec() dumps
node$auto_name (not "") as the bound name
whenever the textbox is empty, so reloading a spec re-establishes the
same binding the running app saw.ptr_shared(..., id = ...) coordinators on
one page sharing a panel-owned source key no longer collide on bare DOM
ids. When a panel-shared
ppUpload(shared = '<key>') was rendered under a
coordinator with a non-NULL id, the inner
fileInput + shortcut textInput still bound at
the bare shared_<key> /
shared_<key>_shortcut ids — so a second coordinator
(id = "right") on the same page emitted duplicate ids and
silently failed to bind one of the two uploads. The rendered widget now
binds at the coordinator-namespaced ids
(left-shared_<key> /
right-shared_<key>), matching the sibling render
paths in invoke_build_ui() and
ptr_setup_source_uis(). Single-coordinator apps (no
id = supplied) are byte-for-byte unchanged.ptr_define_placeholder_source(shortcut = TRUE) reads the
shortcut textbox at every keystroke. Two implementation attempts at the
ADR’s proposed 400 ms debounce (shiny::debounce() on the
shortcut input read) both broke unrelated test contracts — the bind-path
delay broke test-rewrite-pipeline-data-source.R (6 FAIL)
and the mutex-file-reset race in
test-shared-source-panel-multi-instance.R (2 FAIL). The
debounce is therefore not shipped in this release. User-visible
impact: typing a partial, unresolvable name into the shortcut
textbox may briefly surface a transient
Object 'm' not found-style error in the inline error pane
during the typing burst; the bind settles once the user finishes typing.
A debounce-with-race-preservation variant is tracked as a follow-up; A1
(vacate-on-empty) is unaffected and ships in this release. -
validate_input hook signature unified across value
and consumer placeholders. Both
ptr_define_placeholder_value() and
ptr_define_placeholder_consumer() now accept
validate_input = function(value, ctx). The consumer’s
previous function(value, upstream_cols) signature is gone —
registrations that still use it fail loudly via
validate_hook() (“must accept argument(s): ctx”).
ctx is a plain list with named fields: node
(the placeholder AST node), keyword (alias for
node$keyword), upstream_cols (character vector
or NULL), and data (the upstream data frame or
NULL). For value placeholders,
ctx$upstream_cols and ctx$data are always
NULL (value placeholders have no upstream column scope by
definition); for consumer placeholders, both fields are
populated when the upstream resolves, letting data-aware validators
inspect column types / ranges / levels and not just column names.
Migration: rename the second formal from upstream_cols to
ctx and read ctx$upstream_cols in the body.
Value placeholders that previously had no validator gain the option to
declare one.
var /
num / text / expr /
upload to ppVar / ppNum /
ppText / ppExpr /
ppUpload. Every formula passed to
ptr_app() / ptr_server() /
ptr_translate() must use the new vocabulary. The
pp-prefix avoids shadowing base R / ggplot2 names
(stats::var(), graphics::text(),
base::expression, etc.) and lets the same symbols double as
identity / guard plain-R functions exported from the package — so
ggplot(mtcars, aes(x = ppVar(mpg))) works both inside
ptr_app() (as a placeholder) and as a standalone ggplot
call (ppVar is identity, so
aes(x = ppVar(mpg)) evaluates to
aes(x = mpg)). See ADR 0009 for the full rationale.
Migration: rename every placeholder call inside your formula strings or
unquoted-expression formulas.var, num, text,
expr, upload are no longer registered by
ggpaintr and no longer trigger the shadow-check guard, so users who
previously had custom placeholders blocked by built-ins of these names
can now register them without conflict.ggplot_1_1_var_NA is now
ggplot_1_1_ppVar_NA). Bookmarked URLs /
setBookmarkExclude allow-lists / setInputs()
test fixtures from previous versions will need to be updated.Default-value arguments and code-mode toggle (ADR
0009). Built-in placeholders now accept a positional argument
that seeds the widget’s initial value: ppVar(mpg) opens the
column picker pre-selected on mpg, ppNum(5)
seeds the numeric input at 5, ppText("hello") pre-fills the
text input, and ppExpr(factor(cyl)) pre-fills the
expression box. Defaults survive the rename round-trip:
ptr_render(root, preserve_placeholders = TRUE) re-emits the
original ppX(default) call rather than the substituted
value. The ptr_app() code panel gained a “Final code” /
“Show placeholders” toggle that switches between the two render
modes.
Custom placeholders can opt in to the default/named-args
schema. ptr_define_placeholder_value() /
ptr_define_placeholder_consumer() /
ptr_define_placeholder_source() now accept
default_arg = <validator> and
named_args = list(<name> = <validator>, ...).
Use the bundled validator factories —
ptr_default_symbol_or_string(),
ptr_default_numeric(),
ptr_default_numeric_vector(),
ptr_default_string(), ptr_default_expression()
— or pass a custom closure. See
?ptr_define_placeholder_value.
L3 — own every piece of the UI. Every piece of
ggpaintr’s public UI now has its own exported builder:
ptr_ui_header(), ptr_ui_controls(),
ptr_ui_plot(), ptr_ui_error(),
ptr_ui_code(), ptr_ui_code_toggle(), and
ptr_ui_assets(). Compose exactly the pieces you want, place
each anywhere in your own layout, and wire them with the existing server
API (ptr_server() / ptr_module_server()). The
bundled ptr_app() / ptr_module_ui() /
ptr_outputs_ui() / ptr_controls_ui() paths are
reimplemented as thin composites of these pieces — same
<body> DOM, no behavior or performance change. The
pieces are deliberately bare (no assets, no
.ptr-app wrapper); the new
ptr_ui_page() shell wraps them in a
Bootstrap page + the single .ptr-app theme scope + the
(deduped) asset bundle in one call, so that is all an L3 user has to
remember. Swap the page builder with page =
(fluidPage default, also
fixedPage/fillPage/bootstrapPage/basicPage);
for a navbarPage or bslib root use the documented
decomposition recipe. See vignette("ggpaintr-use-cases") §
“L3 — Own every piece of the UI”.
ptr_server() now accepts a
ptr_shared_server() bundle directly via
shared_state =, mirroring ptr_module_server().
Wiring a page-level ptr_shared_ui() panel to a single
embedded or custom-rendered plot no longer requires spreading the four
bundle slots through ....
The custom-renderer pattern (reading state$runtime()
for your own renderPlotly() / renderGirafe())
is now documented as an L2 capability — both ptr_server()
and ptr_module_server() return the ptr_state,
so it needs nothing beyond embedding.
ptr_register_plot() /
ptr_register_error() / ptr_register_code() are
no longer exported. Post-rewrite they only ever ran inside
ptr_server() (their sole caller, via the internal runtime
setup) and rendered nothing without it, so the “compose outputs
manually” use case they advertised was impossible. They remain as
internal helpers; no replacement is needed (use the L3 pieces +
ptr_server(), or read state$runtime() for a
custom renderer). No deprecation cycle — this dev cycle’s own surface,
single pre-1.0 user.htmltools::htmlDependency() bundles.
ggpaintr.css, the code-window JS, and the
ptr_set_class handler + stage CSS are emitted as two
dependencies (ggpaintr, ggpaintr-layer)
instead of inline
<style>/<script>/<link>
tags. htmltools collapses each to a single <head>
injection no matter how many shells/pieces a page nests, so the old
window.__ptr_*_registered JS guards are gone. Rendered
<body> is unchanged; only <head>
asset emission differs. htmltools moves from Suggests to
Imports.placeholders = ptr_merge_placeholders(...) argument has
been removed from every public entry point (ptr_app(),
ptr_app_bslib(), ptr_app_grid(),
ptr_module_ui(), ptr_module_server(),
ptr_server(), ptr_translate(),
ptr_render()). Custom placeholders are now defined once per
R session via
ptr_define_placeholder_value() / _consumer() / _source(),
which write directly into an internal global registry. All instances in
that session see the same registry.
build_ui(node, label = NULL, ...) for value/source,
build_ui(node, cols, label = NULL, selected = character(0), ...)
for consumer. The id is on node$id (and, for sources
registered with shortcut = TRUE, the sibling-input id on
node$shortcut_id — see ADR 0025); there is no separate
id argument. resolve_expr(value, node, ...)
returning NULL is the new “drop this argument” signal
(replacing ptr_missing_expr()).ptr_define_placeholder_*() (caller-driven), or
auto-register from .onLoad (drop-in). Either pattern works;
pick by whether you want the registration to be visible at the call
site.ptr_module_server()
instances in the same Shiny app cannot have different
implementations of the same keyword. The second registration overwrites
the first (with a cli::cli_warn notice). If you need
divergent widgets per instance, prefix custom keywords with a
package-specific tag (mypkg_pct rather than
pct). A scoped per-instance registry override is on the
table for a future release.ptr_registry_clear() +
ptr_register_builtins() (currently internal) or run in a
fresh R session.annotation_custom(), annotation_map(),
annotation_raster() (their required args make zero-arg
calls error in stock ggplot2 — dropping prevents the crash);
aes(), aes_(), aes_q(),
aes_string(), vars() (empty mapping helpers
are render-identical to “absent” via inherit.aes); and
element_text(), element_line(),
element_rect(), element_point(),
element_polygon(), element_geom() (empty form
inherits from the active theme element). This means
e.g. geom_point(aes(colour = var)) with var
missing now reduces to + geom_point() (clean) instead of
+ geom_point(aes()). element_blank() is
intentionally excluded — empty form is a meaningful “suppress”
directive, not a no-op.theme(),
labs(),
xlab/ylab/ggtitle,
facet_wrap/facet_grid/facet_null,
xlim/ylim/lims,
expand_limits, guides, annotate)
or in the new safe_to_remove argument on
ptr_app(), ptr_app_bslib(),
ptr_app_components(), ptr_server(),
ptr_module_server(), ptr_server_state(),
ptr_complete_expr(), and ptr_exec(). Empty
calls whose name is not in the set (e.g. third-party helpers like
pcp_theme(), pcp_arrange(), or user-authored
aes_pcp()) are preserved by default — being absent from the
set is the “removal safety unknown” signal. Pass
safe_to_remove = c("pcp_theme") to opt a specific name into
the cleanup pass.expr placeholder, when the user supplies an
expression, always wins over the cleanup pass: whatever the user typed
into an expr input is honoured verbatim, even if its
top-level name is in safe_to_remove. The intent (“I want
this here”) overrides the curated list.geom_*() and stat_*() layers are kept
empty regardless of safe_to_remove, since they inherit
aesthetics from ggplot(). So
geom_point(colour = var) with var missing
still renders as geom_point().+ labs(), + theme(), + guides()
calls are now dropped (no semantic change — they’re no-ops in stock
ggplot2). The previous build kept them via a “diff guard” that has been
removed in favour of the cleaner curated-list rule.ptr_resolve_layer_data() and ptr_ns_id()
are now exported with full docs. Custom data-aware placeholders should
call these helpers from a bind_ui() callback instead of
replicating the source-symbol walk or the namespace-function dance.?ptr_define_placeholder now documents the
bind_ui contract, the contents of the placeholder
context, and the meta$id namespacing rule. A
second runnable example demonstrates a data-aware “numeric-columns-only”
placeholder using the public helpers.ptr_gg_extra() — advanced helper for embedded apps that
own their own renderPlot({...}) block. Captures ggplot
components (themes, scales, coords, …) added on top of
ptr_extract_plot() and stores them on a new
ptr_state$extras reactiveVal, so the default code binder
(ptr_register_code()) appends them to the generated-code
pane. Extras are suppressed automatically when the underlying runtime
reports a failure, so stale extras from a prior successful draw never
surface during an error state. See
vignette("ggpaintr-extensibility"), Recipe 3.ptr_llm_primer(), ptr_llm_topic(),
ptr_llm_topics(), ptr_llm_register() —
ellmer-facing helpers that expose the bundled inst/llm/
primer and topic files as plain strings or as registered ellmer tools,
so coding assistants can fetch just-in-time ggpaintr guidance. See
vignette("ggpaintr-llm").upload placeholder now accepts .tsv,
.xlsx, .xls, and .json in
addition to .csv and .rds. JSON uploads must
be an array of records; nested objects are flattened, nested arrays
error out. Excel and JSON readers require the new suggested packages
readxl and jsonlite. The “show me the code”
pane reflects the format-appropriate reader (read.delim,
readxl::read_excel, jsonlite::fromJSON).ptr_generate_shiny(), ptr_register_export(),
the download button, and ids$export_button are gone. Use
the public Shiny integration surface (ptr_server_state(),
ptr_register_*(), ptr_input_ui(),
ptr_output_ui(), ptr_app_bslib()) to compose
and distribute custom apps.source_file,
source_package, source_function, and
on_missing are removed from
ptr_define_placeholder(). They only existed to support the
exporter. Hook functions should be defined inline.v0-pre-export-removal.ggpaintr
workflowptr_* prefix for conciseness (previously
ggpaintr_*)bind_* → register_*,
*_value → extract_*)copy_rules parameter/system to
ui_text throughoutbase::stop() to
rlang::abort()@examples to all exported functionsptr_generate_shiny() so the maintained public
call path is now
ptr_generate_shiny(ptr_obj, output_file, ...) Note:
ptr_generate_shiny() was removed in 0.9.0 — see breaking
changes above.archive/legacy-package/ggpaintr-first documentationR CMD check and
CRAN-oriented cleanup