Figaro is a pure client-side React web app (no backend). Users currently load data by
dragging CSV files or clicking "Load examples". The goal is an R package that lets R users
pass either data frames or plot objects to figaro() and immediately see them inside
the full Figaro UI — no manual file picking required.
Three input modes are supported:
- Data mode:
figaro(iris = iris)— data frame is loaded as a dataset; user builds plots interactively in the UI. - Plot mode:
figaro(scatter = my_ggplot)— an existing R plot object (ggplot2 or base R) is rasterized to PNG and inserted as an image panel. Simple plots are extracted as native Figaro plots (fully editable); complex ones get an interactive re-render API via the local R server. - File mode:
figaro(fig = "path/to/figure.png")orfigaro(fig = "path/to/figure.pdf")— a pre-rendered PNG, JPEG, or PDF file is loaded directly as an image panel. No R plot object required; useful when the figure was produced outside R or by a tool with no plot object API.
All modes can be mixed in a single call: figaro(data = iris, fig = my_plot, extra = "figure.pdf").
The R package bundles the Figaro production build and serves it locally via httpuv.
On each launch it injects the user's data directly into the page HTML so the browser
loads Figaro with the datasets and images already present.
R session
└─ figaro(data = iris, fig = my_ggplot, extra = "fig.pdf")
├─ iris (data frame) → dataset entry + rows in fileData
├─ my_ggplot (ggplot) → extract native plot OR rasterize PNG → imageRef
├─ "fig.pdf" (path) → read file, rasterize if PDF → PNG data URL → imageRef
├─ build session JSON (layout, panels, datasets, imageRefs, fileData)
├─ start httpuv on localhost:PORT
│ ├─ GET / → index.html with injected <script>
│ └─ GET /assets/* → static bundle files
└─ browseURL("http://localhost:PORT")
Browser
└─ React mounts → reads window.__FIGARO_INITIAL_SESSION__
└─ loadSession() + attachLoaded()
→ data frame visible in DataManager
→ ggplot visible as image panel, ready to label & compose
File: vite.config.js
Currently base: '/figaro-/' — assets are served at /figaro-/assets/...,
which breaks when the app is served from a plain localhost:PORT.
Change: support a second build mode with base: './' (relative paths).
// vite.config.js
base: process.env.VITE_BASE_PATH ?? '/figaro-/',Add an npm script using cross-env, which normalizes environment-variable syntax across
all platforms (macOS, Linux, Windows):
"build:r": "cross-env VITE_BASE_PATH=./ vite build --outDir dist-r"Install cross-env as a dev dependency (npm i -D cross-env).
This works identically on Mac/Linux (where VAR=val cmd already works natively) and
on Windows (where cmd.exe requires a different syntax). Using cross-env means any
developer can run npm run build:r regardless of their OS.
File: src/App.jsx
Add one block at the top of the existing restoreSession useEffect that checks for
window.__FIGARO_INITIAL_SESSION__ (set by the R server at page-load time).
If present, load it instead of falling back to IndexedDB.
useEffect(() => {
const injected = window.__FIGARO_INITIAL_SESSION__;
if (injected) {
const { session, loaded } = injected;
loadSession(session);
for (const [id, payload] of Object.entries(loaded || {})) {
attachLoaded(id, payload);
}
requestCanvasFit();
return; // skip IndexedDB restore
}
// ... existing restoreSession() logic unchanged
}, []);loadSession and attachLoaded are already in the Zustand store.
After this change, rebuild: npm run build:r → output lands in dist-r/.
Create a new directory figaro-r/ (sibling to figaro-/).
figaro-r/
├── DESCRIPTION
├── NAMESPACE
├── R/
│ ├── figaro.R # figaro() — main entry point
│ ├── session.R # build_session() — R → session JSON
│ ├── server.R # start_server() / stop_server()
│ └── utils.R # id generators, type inference helpers
├── inst/
│ └── www/ # copy of dist-r/ after npm run build:r
│ ├── index.html
│ └── assets/
├── man/ # auto-generated by roxygen2
└── tests/
└── testthat/
├── test-session.R
└── test-server.R
Package: figaro
Title: Interactive Scientific Figure Composer
Version: 0.1.0
Authors@R: c(
person("Yuval", "Bloch", role = "aut"),
person("Kesem", "Abramov", role = "aut"),
person("Natan", "Edelman", role = "aut"))
Imports: jsonlite, httpuv, utils
Suggests: ggplot2, pdftools, png, testthat (>= 3.0.0)
License: MIT
Encoding: UTF-8
Each item in ... is classified before processing:
classify_input <- function(x) {
if (is.data.frame(x)) return("dataset")
if (inherits(x, "gg")) return("ggplot") # ggplot2 object
if (inherits(x, "recordedplot")) return("baseplot") # base R recorded plot
if (is.character(x) && length(x) == 1 && file.exists(x)) {
ext <- tolower(tools::file_ext(x))
if (ext %in% c("png", "jpg", "jpeg")) return("image_file")
if (ext == "pdf") return("pdf_file")
stop("Unsupported file type: .", ext, ". Supported: png, jpg, jpeg, pdf.")
}
stop("Unsupported input type: ", class(x)[1],
". Pass a data frame, a ggplot/recordedPlot object, or a path to a PNG/PDF file.")
}Converts an image/PDF file to a PNG data URL.
- PNG / JPEG: read raw bytes, base64-encode directly:
raw_bytes <- readBin(path, "raw", file.info(path)$size) mime <- if (ext %in% c("jpg","jpeg")) "image/jpeg" else "image/png" paste0("data:", mime, ";base64,", jsonlite::base64_enc(raw_bytes))
- PDF: rasterize the first page using
pdftools::pdf_render_page()(addspdftoolstoSuggests; emit a helpful error if not installed):Only the first page is imported. Aimg_matrix <- pdftools::pdf_render_page(path, page = 1, dpi = 150) tmp <- tempfile(fileext = ".png") on.exit(unlink(tmp)) png::writePNG(img_matrix, tmp) # 'png' package (base R on Mac/Linux; Suggests on Windows) raw_bytes <- readBin(tmp, "raw", file.info(tmp)$size) paste0("data:image/png;base64,", jsonlite::base64_enc(raw_bytes))
message()informs the user if a multi-page PDF is detected.
When an R plot object is passed, Figaro uses the richest path available:
Path A — Native Figaro plot (ggplot2, single-layer, supported geom):
Extract the underlying data, aesthetic mappings, labels and theme settings from the ggplot2
object and create a real Figaro plot entry. The plot lands in the UI exactly as if the
user had dragged the data and configured the chart type themselves — font sizes, titles,
axis labels, and column assignments are all editable via the Plot Inspector.
Path B — Rasterized image (fallback):
Used when the ggplot is too complex to extract (multi-layer, unsupported geom, no data
embedded) or when the input is a base R recordedPlot. The plot is rasterized to a PNG
data URL and inserted as an image panel. The user can resize, label, and compose it, but
cannot edit internal chart elements.
Attempts Path A. Returns list(plot = <plot_obj>, dataset = <df>, ok = TRUE) on success,
or list(ok = FALSE, reason = "...") on fallback.
Steps:
- Check simplicity: exactly one layer, layer data is a data frame (or inherits from
the top-level
p$data), no faceting (is.null(p$facet)orFacetNull). If any condition fails → returnok = FALSE. - Detect chart type from
class(p$layers[[1]]$geom)[1]:GeomPoint→"scatter"GeomLine,GeomPath→"line"GeomBar,GeomCol→"bar"GeomBoxplot→"box"GeomHistogram→"histogram"- Anything else → return
ok = FALSE(fallback to image)
- Extract column names from
p$mapping:x,y,colour/color,size,fill(userlang::as_label()ordeparse(p$mapping$x)to get the column name string). - Extract labels from
p$labels:title,x(→xLabel),y(→yLabel). - Extract base font size from
p$theme$text$size(fall back to 12 if absent). - Return a Figaro
plotlist mirroring the shape insrc/store/plotsSlice.js:Implementation note: before writing this function, readlist( id = plot_id, type = chart_type, datasetId = ds_id, xCol = x_col, yCol = y_col, colorCol = color_col, # NULL if not mapped title = title, xLabel = x_label, yLabel = y_label, fontSize = font_size # remaining fields use Figaro defaults (colors, marker size, etc.) )
src/store/plotsSlice.jsandsrc/components/controls/fields.jsxto confirm exact field names and defaults for each chart type. The R list must match those field names exactly so Figaro deserialises them correctly.
Fallback rasterizer. Returns a PNG data URL using jsonlite::base64_enc().
plot_to_data_url <- function(plot_obj, type, width_in = 7, height_in = 5, dpi = 150) {
tmp <- tempfile(fileext = ".png")
on.exit(unlink(tmp))
if (type == "ggplot") {
ggplot2::ggsave(tmp, plot = plot_obj, width = width_in,
height = height_in, units = "in", dpi = dpi)
} else { # recordedplot
grDevices::png(tmp, width = width_in * dpi, height = height_in * dpi)
grDevices::replayPlot(plot_obj)
grDevices::dev.off()
}
raw_bytes <- readBin(tmp, "raw", file.info(tmp)$size)
paste0("data:image/png;base64,", jsonlite::base64_enc(raw_bytes))
}Processes a named list where each item is a data frame or plot object.
Key steps:
-
Data frames → dataset pipeline:
- Generate
ds_<id>; infer column types (numeric →"number", others →"string") - Add to
datasetsmetadata +fileDataaslist(type="dataset", rows=...) - Panel type:
"empty"(user wires a chart type in the UI)
- Generate
-
ggplot2 objects → try Path A first:
- Call
ggplot_to_figaro(p, ds_id, plot_id). - Success: add extracted data frame as a dataset (
ds_<id>) +fileDatarows; add the Figaroplotentry toplots; panel type ="plot"referencingplot_id. Result: fully editable chart with pre-filled column assignments, title, font size. - Fallback: call
plot_to_data_url()→imageRef+fileDataimage entry; panel type ="image"; emit amessage()telling the user what was not extracted.
- Call
-
Base R
recordedPlot→ always Path B (image):plot_to_data_url()→imageRef+fileDataimage; panel type ="image". -
File paths (PNG / JPEG / PDF) → image pipeline:
- Call
file_to_data_url(path, type)→ PNG/JPEG data URL - Add to
imageRefs+fileDataas image entry; panel type ="image" - For PDF: rasterize first page; warn if multi-page
- Call
-
Build layout: 1 × N grid where N = total number of items.
-
Return a list matching the
.figaro.jsonschema:list( schemaVersion = "1.1.0", meta = list(name = name, createdAt = iso_now(), modifiedAt = iso_now()), canvas = canvas_preset_to_list(canvas_preset), layout = default_layout(n_cols), panels = default_panels(regions, panel_specs), plots = plots_list, datasets = datasets_meta, imageRefs = image_refs_meta, theme = default_theme(), labeling = default_labeling(), customPalette = default_palette(), fileData = file_data )
Writes session to a .figaro.json file via jsonlite::write_json(session, path, auto_unbox = TRUE).
For ggplot2 objects that cannot be extracted natively (multi-layer, faceted, unsupported geom), Figaro uses image fallback but still allows editing visual properties through a dedicated R Plot Style inspector in the web UI.
-
The R server stores each ggplot object in a named in-memory list (
r_plots_env) keyed by the sameimg_<id>used inimageRefs. The stored object is the original, unmodified ggplot2 value. -
At page-load time, the server URL is injected alongside the session:
window.__FIGARO_INITIAL_SESSION__ = { session, loaded }; window.__FIGARO_R_SERVER__ = "http://localhost:<PORT>";
The web app reads
window.__FIGARO_R_SERVER__and stores it in a module constant. When this value isnull/undefined(normal browser use), all R-server features are silently disabled. -
When the user clicks an image panel whose
imageRefhasrPlot: true, Figaro shows a "R Plot Style" inspector panel (web-side addition) with controls:- Title text field
- X / Y axis label text fields
- Base font size slider (8–24)
- Legend position selector (right / bottom / none)
- ggplot2 colour palette selector (default / viridis / RColorBrewer palettes)
- Width × Height (inches) for the rasterisation
-
On any change, the web app sends:
POST <R_SERVER>/restyle { "plotId": "img_xxx", "title": "...", "fontSize": 14, ... }and replaces the panel's
src/blobURLwith the returned PNG data URL.
New endpoint in start_server():
if (path == "/restyle" && req$REQUEST_METHOD == "POST") {
body <- jsonlite::fromJSON(rawToChar(req$rook.input$read()))
plot_id <- body$plotId
p <- r_plots_env[[plot_id]]
if (is.null(p)) return(list(status = 404L, headers = list(), body = "Not found"))
if (!is.null(body$title)) p <- p + ggplot2::ggtitle(body$title)
if (!is.null(body$xLabel)) p <- p + ggplot2::xlab(body$xLabel)
if (!is.null(body$yLabel)) p <- p + ggplot2::ylab(body$yLabel)
if (!is.null(body$fontSize)) p <- p + ggplot2::theme(text = ggplot2::element_text(size = body$fontSize))
if (!is.null(body$legendPos)) p <- p + ggplot2::theme(legend.position = body$legendPos)
data_url <- plot_to_data_url(p, "ggplot",
width_in = body$widthIn %||% 7,
height_in = body$heightIn %||% 5)
return(list(status = 200L,
headers = list("Content-Type" = "application/json",
"Access-Control-Allow-Origin" = "*"),
body = jsonlite::toJSON(list(dataUrl = data_url), auto_unbox = TRUE)))
}A small new component RPlotStyleInspector (in src/components/inspect/) is shown in
the right-side inspector when the selected panel is an image panel with rPlot: true in
its imageRef. Controls map to the /restyle API fields above.
The imageRefs schema gains one optional field:
{ "id": "img_xxx", "name": "complex", "sourceFile": "", "rPlot": true }This is backwards-compatible — existing image refs without rPlot behave as before.
The re-render API is session-only: the user's ggplot object lives in the R process's
memory and is lost if the server stops. The modified PNG is embedded in the session JSON
if the user saves (File > Save) — so the result is portable even without R.
start_server <- function(session_json, port = NULL) {
if (is.null(port)) port <- httpuv::randomPort()
www_dir <- system.file("www", package = "figaro")
server <- httpuv::startServer("0.0.0.0", port, list(
call = function(req) {
path <- req$PATH_INFO
if (path == "/" || path == "/index.html") {
html <- readLines(file.path(www_dir, "index.html"), warn = FALSE)
inject <- sprintf(
'<script>window.__FIGARO_INITIAL_SESSION__ = %s; window.__FIGARO_R_SERVER__ = "http://localhost:%d";</script>',
session_json, port
)
html <- sub("</body>", paste0(inject, "\n</body>"),
paste(html, collapse = "\n"))
return(list(status = 200L,
headers = list("Content-Type" = "text/html"),
body = html))
}
file_path <- file.path(www_dir, sub("^/", "", path))
if (file.exists(file_path)) {
return(list(status = 200L,
headers = list("Content-Type" = mime_type(file_path)),
body = readBin(file_path, "raw", file.info(file_path)$size)))
}
list(status = 404L, headers = list(), body = "Not found")
}
))
list(server = server, port = port)
}#' Open the Figaro figure composer with pre-loaded data or plots
#'
#' Each named argument can be:
#' - a data frame (loaded as a dataset the user can assign to plots)
#' - an R plot object (ggplot2 or base R recordedPlot)
#' - a character string: path to an existing PNG, JPEG, or PDF file
#' All three types can be mixed in one call:
#' figaro(data = iris, scatter = my_ggplot, extra = "figure.pdf")
#'
#' @param ... Named inputs: data frames, ggplot/recordedPlot objects, or file paths.
#' @param session Path to an existing .figaro.json file to open (overrides ...)
#' @param canvas Canvas size preset. One of "A4_portrait", "A4_landscape",
#' "letter_portrait", "slide_16_9", etc.
#' @param port Local port. Auto-assigned if NULL.
#' @param launch Open the browser automatically. Default TRUE.
#' @export
figaro <- function(..., session = NULL, canvas = "A4_portrait",
port = NULL, launch = TRUE) {
inputs <- list(...)
if (!is.null(session)) {
sess_obj <- jsonlite::read_json(session)
} else {
sess_obj <- build_session(inputs, canvas_preset = canvas)
}
loaded <- build_loaded(sess_obj)
payload <- jsonlite::toJSON(
list(session = sess_obj, loaded = loaded),
auto_unbox = TRUE, null = "null"
)
srv <- start_server(payload, port)
url <- paste0("http://localhost:", srv$port)
message("Figaro running at ", url)
message("Press Ctrl+C (or call figaro_stop()) to stop the server.")
.figaro_env$server <- srv$server
.figaro_env$port <- srv$port
if (launch) utils::browseURL(url)
invisible(srv)
}
#' @export
figaro_stop <- function() {
if (!is.null(.figaro_env$server)) {
.figaro_env$server$stop()
.figaro_env$server <- NULL
message("Figaro server stopped.")
}
}
.figaro_env <- new.env(parent = emptyenv())test_that("build_session produces valid schema version", {
sess <- build_session(list(iris = iris))
expect_equal(sess$schemaVersion, "1.1.0")
})
test_that("all iris rows are present in fileData", {
sess <- build_session(list(iris = iris))
ds_id <- names(sess$datasets)[1]
expect_equal(length(sess$fileData[[ds_id]]$rows), nrow(iris))
})
test_that("column types are inferred correctly", {
sess <- build_session(list(iris = iris))
ds_id <- names(sess$datasets)[1]
cols <- sess$datasets[[ds_id]]$columns
sepal_col <- Filter(function(c) c$name == "Sepal.Length", cols)[[1]]
expect_equal(sepal_col$type, "number")
species_col <- Filter(function(c) c$name == "Species", cols)[[1]]
expect_equal(species_col$type, "string")
})
test_that("figaro_save round-trips correctly", {
sess <- build_session(list(iris = iris))
tmp <- tempfile(fileext = ".figaro.json")
figaro_save(sess, tmp)
reloaded <- jsonlite::read_json(tmp)
expect_equal(reloaded$schemaVersion, "1.1.0")
})
test_that("simple ggplot2 scatter is extracted as a native Figaro plot", {
skip_if_not_installed("ggplot2")
p <- ggplot2::ggplot(iris, ggplot2::aes(Sepal.Length, Sepal.Width)) +
ggplot2::geom_point()
sess <- build_session(list(scatter = p))
expect_length(sess$plots, 1)
expect_equal(sess$plots[[1]]$type, "scatter")
expect_equal(sess$plots[[1]]$xCol, "Sepal.Length")
expect_equal(sess$plots[[1]]$yCol, "Sepal.Width")
panel_types <- vapply(sess$panels, `[[`, character(1), "type")
expect_true("plot" %in% panel_types)
})
test_that("ggplot labels and font size are transferred to Figaro plot", {
skip_if_not_installed("ggplot2")
p <- ggplot2::ggplot(iris, ggplot2::aes(Sepal.Length, Sepal.Width)) +
ggplot2::geom_point() +
ggplot2::labs(title = "Iris scatter", x = "Sepal L", y = "Sepal W") +
ggplot2::theme(text = ggplot2::element_text(size = 14))
sess <- build_session(list(p = p))
plt <- sess$plots[[1]]
expect_equal(plt$title, "Iris scatter")
expect_equal(plt$xLabel, "Sepal L")
expect_equal(plt$fontSize, 14)
})
test_that("complex ggplot (multi-layer) falls back to image", {
skip_if_not_installed("ggplot2")
p <- ggplot2::ggplot(iris, ggplot2::aes(Sepal.Length, Sepal.Width)) +
ggplot2::geom_point() +
ggplot2::geom_smooth()
sess <- build_session(list(complex = p))
expect_length(sess$imageRefs, 1)
img_id <- names(sess$imageRefs)[1]
expect_true(startsWith(sess$fileData[[img_id]]$dataUrl, "data:image/png;base64,"))
})
test_that("PNG file path is converted to imageRef with data URL", {
skip_if_not_installed("ggplot2")
tmp_png <- tempfile(fileext = ".png")
ggplot2::ggsave(tmp_png,
ggplot2::ggplot(iris, ggplot2::aes(Sepal.Length, Sepal.Width)) +
ggplot2::geom_point(),
width = 5, height = 4)
sess <- build_session(list(fig = tmp_png))
img_id <- names(sess$imageRefs)[1]
expect_true(startsWith(sess$fileData[[img_id]]$dataUrl, "data:image/png;base64,"))
panel_types <- vapply(sess$panels, `[[`, character(1), "type")
expect_true("image" %in% panel_types)
})
test_that("mixed data frame + ggplot call produces plot + empty panel", {
skip_if_not_installed("ggplot2")
p <- ggplot2::ggplot(iris, ggplot2::aes(Sepal.Length, Sepal.Width)) +
ggplot2::geom_point()
sess <- build_session(list(data = iris, fig = p))
panel_types <- vapply(sess$panels, `[[`, character(1), "type")
expect_true("empty" %in% panel_types)
expect_true("plot" %in% panel_types)
})test_that("server serves index.html with injected session", {
sess <- build_session(list(iris = iris))
payload <- jsonlite::toJSON(list(session = sess, loaded = build_loaded(sess)),
auto_unbox = TRUE)
srv <- start_server(payload, port = 7654)
on.exit(srv$server$stop())
resp <- httr::GET("http://localhost:7654/")
expect_equal(httr::status_code(resp), 200)
body <- httr::content(resp, "text")
expect_true(grepl("__FIGARO_INITIAL_SESSION__", body))
})library(figaro)
# Test 1: data frame → user builds plots interactively
figaro(iris = iris)
# → browser opens; iris appears in DataManager
# → drag iris onto panel → assign scatter → verify render
# → File > Save → downloads .figaro.json
# Test 2: ggplot2 → extracted as native Figaro plot (fully editable)
library(ggplot2)
p <- ggplot(iris, aes(Sepal.Length, Sepal.Width, color = Species)) + geom_point() +
labs(title = "Iris", x = "Sepal L", y = "Sepal W") +
theme(text = element_text(size = 14))
figaro(scatter = p)
# → scatter panel with pre-filled axes, title, font size
# → Plot Inspector shows chart type + columns pre-set; font size slider works
# Test 3: complex ggplot → image fallback + R Plot Style inspector
p2 <- ggplot(iris, aes(Sepal.Length, Sepal.Width)) + geom_point() + geom_smooth()
figaro(complex = p2)
# → R console: "figaro: 'complex' has multiple layers — inserted as image panel"
# → click panel → R Plot Style inspector; change font size → re-renders
# Test 4: mixed call
figaro(data = iris, fig = p)
# → editable scatter panel + empty dataset panel; drag iris → assign bar chart
# Test 5: PNG file path
figaro(fig = "path/to/fig.png")
# → image panel renders immediately
# Test 6: PDF file path
figaro(fig = "path/to/fig.pdf")
# → first page imported; warning shown if multi-page## Phase R1 — Vite: R-package build mode [ ]
- [ ] Add VITE_BASE_PATH env var to vite.config.js
- [ ] Install cross-env dev dependency (works on Mac/Linux/Windows)
- [ ] Add `build:r` npm script (outputs to dist-r/)
## Phase R2 — Web app: initial session injection [ ]
- [ ] Add window.__FIGARO_INITIAL_SESSION__ hook to src/App.jsx
- [ ] Run `npm run build:r` and copy dist-r/ → figaro-r/inst/www/
## Phase R3 — R package scaffold [ ]
- [ ] Create figaro-r/ directory with DESCRIPTION (authors: Bloch, Abramov, Edelman), NAMESPACE, R/, inst/www/, tests/
- [ ] R/utils.R: classify_input(), plot_to_data_url(), file_to_data_url(), id generator, ISO timestamp
- [ ] R/session.R: ggplot_to_figaro(), build_session(), figaro_save(), build_loaded()
- [ ] R/server.R: start_server(), mime_type(), /restyle endpoint, r_plots_env storage
- [ ] R/figaro.R: figaro(), figaro_stop(), .figaro_env
- [ ] src/components/inspect/RPlotStyleInspector.jsx — style controls for image-backed R plots
- [ ] Inject window.__FIGARO_R_SERVER__ alongside session in start_server()
## Phase R4 — Tests [ ]
- [ ] Unit: session schema, column types, round-trip JSON
- [ ] Unit: simple ggplot2 → native plot with pre-filled xCol/yCol/title/fontSize
- [ ] Unit: ggplot labels + font size transfer
- [ ] Unit: complex multi-layer ggplot → image fallback
- [ ] Unit: mixed data frame + ggplot → correct panel types
- [ ] Unit: PNG file path → imageRef with data URL
- [ ] Integration: server serves index.html with injected session
- [ ] Manual 1: figaro(iris) → drag → assign scatter → export PNG
- [ ] Manual 2: figaro(scatter = ggplot) → all fields editable via Plot Inspector
- [ ] Manual 3: figaro(complex) → image fallback; R Plot Style inspector re-renders on change
- [ ] Manual 4: figaro(data = iris, fig = ggplot) → mixed layout
- [ ] Manual 5: figaro(fig = "fig.png") → image panel
- [ ] Manual 6: figaro(fig = "fig.pdf") → first page; multi-page warning
- [ ] R CMD check: 0 errors, 0 warnings
## Phase R5 — Documentation & CRAN prep [ ]
- [ ] roxygen2 @param/@export docs on all exported functions
- [ ] vignette: "Getting started with figaro"
- [ ] README.md with install + quick-start example| File | Change |
|---|---|
figaro-/vite.config.js |
Add VITE_BASE_PATH env support |
figaro-/package.json |
Add cross-env dev dep + build:r script |
figaro-/src/App.jsx |
Add window.__FIGARO_INITIAL_SESSION__ hook |
figaro-r/R/utils.R |
New — classify_input, plot_to_data_url, file_to_data_url |
figaro-r/R/session.R |
New — ggplot_to_figaro, build_session, figaro_save |
figaro-r/R/server.R |
New — httpuv server + /restyle endpoint |
figaro-r/R/figaro.R |
New — main entry point |
figaro-r/inst/www/ |
Copy of dist-r/ build |
figaro-/src/components/inspect/RPlotStyleInspector.jsx |
New — style controls for image-backed R plots |
R package (Imports): jsonlite, httpuv, tools
R package (Suggests):
ggplot2— for passing ggplot2 objectspdftools— for PDF file path input (rasterizes first page)png— for PDF rasterization on WindowsgrDevices— base R; forrecordedPlotrasterization
Dev only: testthat, httr
Web app: cross-env (dev dep — cross-platform env var syntax for build:r script)
devtools::create("figaro-r")scaffolds DESCRIPTION + NAMESPACE automaticallydevtools::document()generates man/ from roxygen2 commentsdevtools::load_all()lets you test without installingdevtools::check()runs R CMD checkdevtools::install()installs locally for end-to-end testing- The
inst/www/folder is copied verbatim into the installed package;system.file("www", package = "figaro")locates it at runtime