Skip to contents

ggpaintr 0.11.1

New features

  • Linked plotly selection helpers (ADR 0028). Two new exports turn the state returned by 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.

Bug fixes

  • Runtime 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.
  • The LLM docs tool argument was renamed nametopic. 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.

Documentation

ggpaintr 0.11.0

Breaking changes (ADR 0027)

  • The four public placeholder-constructor arguments were renamed across ptr_define_placeholder_value() / ptr_define_placeholder_consumer() / ptr_define_placeholder_source(): positional_argparse_positional_arg, named_argsparse_named_args, validate_inputvalidate_session_input, and runtimeembellish_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.

New features

  • RStudio command-palette addin. 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()).
  • Documentation now leads with the expression form. Roxygen @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()).

Bug fixes (formula / ppExpr codegen)

  • A ~ 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.
  • A two-sided formula renders with its infix ~ (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).

ggpaintr 0.10.0

New features

  • 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.)

Breaking changes

  • Source surface rename + auto-name + UI mutex + code prologue (ADR 0025). 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:
    • Auto-name (§2). Every source node now carries 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.
    • UI mutex (§3). For 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”.
    • Code-panel prologue (§4). Every active upload emits a one-line prologue at the top of the code panel — <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.
    • Vacate-on-empty + spec round-trip (§5, §7 A1). Clearing the shortcut textbox with no file present synchronously vacates 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.

Bug fixes — ADR 0025 follow-ups

  • Two 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.

Known limitation — ADR 0025 §7 A2 deferred (400 ms shortcut debounce)

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.

Breaking changes (ADR 0009)

  • Placeholder keywords renamed from 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.
  • Old keyword names are no longer reserved. The names 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.
  • Shiny input ids changed. ggpaintr’s internal Shiny input ids are not part of the public API; the keyword rename has shifted them (e.g. 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.

New features

  • 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.

Behavior changes

  • 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.
  • ggpaintr’s CSS/JS now ship as deduped 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.
  • Custom placeholder registration is now process-global. The legacy 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.
    • The hook signatures changed too: 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()).
    • Distribution: package authors should ship a setup function that calls 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.
    • Multi-instance limitation: two 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.
    • Test isolation: tests that register custom placeholders should restore state with ptr_registry_clear() + ptr_register_builtins() (currently internal) or run in a fresh R session.
  • Extended the curated empty-call cleanup list with 14 more ggplot2 names: 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.
  • Replaced the old name-prefix heuristic for pruning empty calls. The new rule is: a zero-argument call is dropped iff its bare function name is in a curated ggplot2 cleanup list (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.
  • An 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().
  • Behavior change vs. the prior implementation: user-authored literal + 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.

ggpaintr 0.9.1

Documentation and exports

  • 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.

ggpaintr 0.9.0

New features

  • 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).

Breaking changes

  • Removed the Shiny app export feature: 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.
  • The placeholder distribution parameters 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.
  • The last commit before these removals is tagged v0-pre-export-removal.

ggpaintr 0.1.0

  • repositioned the package around the maintained ggpaintr workflow
  • shortened API prefix: all exported functions now use the ptr_* prefix for conciseness (previously ggpaintr_*)
  • improved semantic clarity of function names across the public API (e.g., bind_*register_*, *_valueextract_*)
  • renamed copy_rules parameter/system to ui_text throughout
  • migrated error signaling from base::stop() to rlang::abort()
  • added @examples to all exported functions
  • clarified the maintained public API boundary in the README and pkgdown-facing docs so the beginner path stays centered on the wrapper, integration, export, placeholder, and intentionally exported runtime helpers
  • removed generated documentation topics for package-internal helper functions so internal implementation details are no longer presented as part of the public community-facing surface
  • updated ptr_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.
  • archived the legacy package implementation under archive/legacy-package/
  • replaced legacy package docs, vignette, and pkgdown content with ggpaintr-first documentation
  • added roxygen2-based package documentation for the active implementation
  • prepared the package structure for R CMD check and CRAN-oriented cleanup