Skip to content

Commit 8bbcaa0

Browse files
committed
Merge branch 'dev'
2 parents 9908053 + 921dda3 commit 8bbcaa0

13 files changed

Lines changed: 1489 additions & 26 deletions

File tree

AGENTS.md

Lines changed: 985 additions & 0 deletions
Large diffs are not rendered by default.

NAMESPACE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ export(label_caption)
1515
export(label_legend)
1616
export(label_subtitle)
1717
export(label_title)
18+
export(mark_area)
1819
export(mark_bar)
1920
export(mark_boxplot)
2021
export(mark_density)
2122
export(mark_histogram)
2223
export(mark_line)
24+
export(mark_map)
2325
export(mark_point)
26+
export(mark_text)
27+
export(mark_violin)
2428
export(plotit)
2529
export(project_cartesian)
2630
export(project_map)

R/mark.R

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,169 @@ mark_density <- S7::new_generic(
197197
)
198198
._register_mark_method(mark_density, ggplot2::geom_density)
199199

200+
# ---- mark_area ----
201+
#' Area layer
202+
#'
203+
#' Adds a filled area layer. Use for stacked area charts, stream graphs,
204+
#' or error bands.
205+
#'
206+
#' @param plot A plotit object
207+
#' @param mapping Optional new aesthetics
208+
#' @param data Optional data for this layer
209+
#' @param position Position adjustment.
210+
#' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
211+
#' @param rasterize_dpi DPI for rasterization (default 300).
212+
#' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
213+
#' @param ... Other arguments passed to `geom_area`
214+
#' @return Modified plotit object
215+
#' @examples
216+
#' plotit(ggplot2::economics, encode(x = date, y = unemploy)) |>
217+
#' mark_area(alpha = 0.5)
218+
#' @export
219+
mark_area <- S7::new_generic(
220+
"mark_area", "plot",
221+
function(plot, mapping = NULL, data = NULL, position = NULL, ...,
222+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
223+
S7::S7_dispatch()
224+
}
225+
)
226+
._register_mark_method(mark_area, ggplot2::geom_area)
227+
228+
# ---- mark_text ----
229+
#' Text layer
230+
#'
231+
#' Adds a text label layer. For automatic label placement with collision
232+
#' avoidance, install the optional \pkg{ggrepel} package and set
233+
#' `repel = TRUE`.
234+
#'
235+
#' @param plot A plotit object
236+
#' @param mapping Optional new aesthetics (e.g. `encode(label = ...)`)
237+
#' @param data Optional data for this layer
238+
#' @param position Position adjustment.
239+
#' @param repel If `TRUE`, use `ggrepel::geom_text_repel` instead of
240+
#' `geom_text`. Requires the \pkg{ggrepel} package.
241+
#' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
242+
#' @param rasterize_dpi DPI for rasterization (default 300).
243+
#' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
244+
#' @param ... Other arguments passed to `geom_text` or `geom_text_repel`
245+
#' @return Modified plotit object
246+
#' @examples
247+
#' plotit(mtcars, encode(x = wt, y = mpg, label = rownames(mtcars))) |>
248+
#' mark_text(size = 3)
249+
#' @export
250+
mark_text <- S7::new_generic(
251+
"mark_text", "plot",
252+
function(plot, mapping = NULL, data = NULL, position = NULL, ...,
253+
repel = FALSE,
254+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
255+
S7::S7_dispatch()
256+
}
257+
)
258+
259+
#' @export
260+
S7::method(mark_text, plotit_class) <- function(
261+
plot, mapping = NULL, data = NULL, position = NULL, ...,
262+
repel = FALSE,
263+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
264+
if (repel) {
265+
if (!requireNamespace("ggrepel", quietly = TRUE)) {
266+
cli::cli_abort("{.arg repel = TRUE} requires the {.pkg ggrepel} package.")
267+
}
268+
geom_fun <- ggrepel::geom_text_repel
269+
} else {
270+
geom_fun <- ggplot2::geom_text
271+
}
272+
if (!is.null(mapping) && !is.null(mapping$colour)) {
273+
plot <- ._clear_default_color(plot, mapping)
274+
}
275+
._mark_impl(
276+
plot, mapping, data, position, geom_fun,
277+
rasterize, rasterize_dpi, rasterize_dev, ...
278+
)
279+
}
280+
281+
# ---- mark_violin ----
282+
#' Violin layer
283+
#'
284+
#' Adds a violin plot layer showing the kernel density estimate of the data
285+
#' at each position.
286+
#'
287+
#' @param plot A plotit object
288+
#' @param mapping Optional new aesthetics
289+
#' @param data Optional data for this layer
290+
#' @param position Position adjustment.
291+
#' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
292+
#' @param rasterize_dpi DPI for rasterization (default 300).
293+
#' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
294+
#' @param ... Other arguments passed to `geom_violin`
295+
#' @return Modified plotit object
296+
#' @examples
297+
#' plotit(iris, encode(x = Species, y = Sepal.Length)) |>
298+
#' mark_violin(draw_quantiles = 0.5)
299+
#' @export
300+
mark_violin <- S7::new_generic(
301+
"mark_violin", "plot",
302+
function(plot, mapping = NULL, data = NULL, position = NULL, ...,
303+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
304+
S7::S7_dispatch()
305+
}
306+
)
307+
._register_mark_method(mark_violin, ggplot2::geom_violin)
308+
309+
# ---- mark_map ----
310+
#' Map layer
311+
#'
312+
#' Adds a geographic map layer for \pkg{sf} spatial data frames.
313+
#' Requires the \pkg{sf} package.
314+
#'
315+
#' @param plot A plotit object
316+
#' @param mapping Optional new aesthetics
317+
#' @param data Optional sf data frame for this layer
318+
#' @param position Position adjustment.
319+
#' @param rasterize If `TRUE`, rasterize via `ggrastr::rasterise()`.
320+
#' @param rasterize_dpi DPI for rasterization (default 300).
321+
#' @param rasterize_dev Graphics device for rasterization (default `"cairo"`).
322+
#' @param ... Other arguments passed to `geom_sf`
323+
#' @return Modified plotit object
324+
#' @examples
325+
#' \dontrun{
326+
#' nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)
327+
#' plotit(nc, encode(geometry = geometry)) |> mark_map()
328+
#' }
329+
#' @export
330+
mark_map <- S7::new_generic(
331+
"mark_map", "plot",
332+
function(plot, mapping = NULL, data = NULL, position = NULL, ...,
333+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
334+
S7::S7_dispatch()
335+
}
336+
)
337+
338+
#' @export
339+
S7::method(mark_map, plotit_class) <- function(
340+
plot, mapping = NULL, data = NULL, position = NULL, ...,
341+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
342+
if (!requireNamespace("sf", quietly = TRUE)) {
343+
cli::cli_abort("{.fn mark_map} requires the {.pkg sf} package.")
344+
}
345+
layer_data <- data %||% plot@gg$data
346+
if (!inherits(layer_data, "sf")) {
347+
cli::cli_abort(c(
348+
"{.fn mark_map} requires {.pkg sf} spatial data.",
349+
"i" = "Use {.fn plotit} with an {.cls sf} data frame."
350+
))
351+
}
352+
# geom_sf does not support position adjustment; ignore dodge
353+
if (!is.null(mapping) && !is.null(mapping$colour)) {
354+
plot <- ._clear_default_color(plot, mapping)
355+
}
356+
geom <- ggplot2::geom_sf(mapping = mapping, data = data, ...)
357+
.add_geom(plot, geom,
358+
rasterize = rasterize, rasterize_dpi = rasterize_dpi,
359+
rasterize_dev = rasterize_dev
360+
)
361+
}
362+
200363
# ---- mark_bar (hand-written: geom_col vs geom_bar dispatch) ----
201364
#' Bar layer
202365
#'

R/output.R

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,18 @@ S7::method(format, plotit_composite) <- function(x, ...) ""
3535
# pkgdown evaluates @examples via evaluate::evaluate(), which calls
3636
# pkgdown_print(value) as the output_handler `value` callback. S7 objects
3737
# hit pkgdown_print.default() → print.S7_object() → str.S7_object(), which
38-
# dumps the full ggproto tree. Intercept with a S3 method that renders the
39-
# plot to the device so evaluate records it, then returns invisible to suppress
40-
# the text dump.
38+
# dumps the full ggproto tree. Intercept with S3 methods that render the
39+
# plot to the device so evaluate records it, then return invisible to
40+
# suppress the text dump.
4141
#' @exportS3Method pkgdown::pkgdown_print
4242
pkgdown_print.plotit <- function(x, visible = TRUE) {
43-
x <- ._sync_labels(x)
44-
print(x@gg)
45-
invisible()
43+
if (!visible) return(invisible())
44+
._print_render(x)
4645
}
4746

4847
#' @exportS3Method pkgdown::pkgdown_print
4948
pkgdown_print.plotit_composite <- function(x, visible = TRUE) {
49+
if (!visible) return(invisible())
5050
._apply_annotations(x) |> print()
5151
invisible()
5252
}
@@ -105,12 +105,20 @@ print.plotit <- function(x, ...) {
105105
# ---- knit_print ----
106106
# S3 methods for knitr to capture plotit plots in vignettes / R Markdown.
107107
# Renders the underlying ggplot to knitr's active device.
108-
#' @exportS3Method knitr::knit_print
109-
knit_print.plotit <- function(x, ...) {
108+
109+
# Shared knit_print / pkgdown_print path: sync labels, render, suppress text.
110+
#' @noRd
111+
#' @keywords internal
112+
._print_render <- function(x) {
110113
x <- ._sync_labels(x)
111114
._render_plotit(x)
112115
}
113116

117+
#' @exportS3Method knitr::knit_print
118+
knit_print.plotit <- function(x, ...) {
119+
._print_render(x)
120+
}
121+
114122
#' @exportS3Method knitr::knit_print
115123
knit_print.plotit_composite <- function(x, ...) {
116124
x@gg <- ._apply_annotations(x)

R/scale.R

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,22 @@ NULL
144144
}
145145
}
146146

147+
# Internal helper: strip NULL entries from a list so that `breaks = NULL`
148+
# and friends do not coerce downstream ggplot2 scales into guide = "none".
149+
#' Strip NULL entries from a list.
150+
#' @noRd
151+
#' @keywords internal
152+
._strip_nulls <- function(args) {
153+
args[!vapply(args, is.null, logical(1L))]
154+
}
155+
147156
# Pick the right scale function for colour/fill given trans + range
148157
.scale_colour_fun <- function(aes, trans, range, ..., force_reverse = FALSE) {
149158
discrete <- trans == "discrete"
150159
binned <- trans == "binned"
151160
reverse <- trans == "reverse" || force_reverse
152161

153-
# Collect args passed through `...` and strip NULLs so that `breaks = NULL`
154-
# does not coerce ggplot2 scales into guide = "none".
155-
extra_args <- list(...)
156-
extra_args <- extra_args[!vapply(extra_args, is.null, logical(1L))]
162+
extra_args <- ._strip_nulls(list(...))
157163

158164
if (is.character(range) && length(range) >= 2) {
159165
do.call(._scale_custom, c(list(aes, range, discrete, binned, reverse), extra_args))
@@ -192,7 +198,7 @@ NULL
192198
alpha = ggplot2::scale_alpha_continuous
193199
)
194200
}
195-
args <- list(...)
201+
args <- ._strip_nulls(list(...))
196202
# Explicit defaults per AGENTS.md <U+00A7>3.3.4
197203
if (is.null(range) && !binned && !discrete) {
198204
range <- switch(aes,
@@ -210,7 +216,7 @@ NULL
210216
# Pick shape/linetype scale function
211217
.scale_discrete_fun <- function(aes, trans, range, ...) {
212218
reverse <- trans == "reverse"
213-
args <- list(...)
219+
args <- ._strip_nulls(list(...))
214220
if (reverse && !is.null(range)) range <- rev(range)
215221
if (!is.null(range)) {
216222
args$values <- range
@@ -289,8 +295,9 @@ NULL
289295
if (discrete && reverse && is.null(limits)) {
290296
args$limits <- rev
291297
}
292-
args <- args[!vapply(args, is.null, logical(1))]
293-
plot@gg <- plot@gg + do.call(scale_fun, c(args, list(...)))
298+
args <- ._strip_nulls(args)
299+
extra_args <- ._strip_nulls(list(...))
300+
plot@gg <- plot@gg + do.call(scale_fun, c(args, extra_args))
294301
plot
295302
}
296303

R/zzz.R

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ NULL
6767
# Registered here (loaded last) so all S7 generics exist.
6868
for (.generic_name in c(
6969
"mark_point", "mark_line", "mark_bar", "mark_boxplot",
70-
"mark_histogram", "mark_density",
70+
"mark_histogram", "mark_density", "mark_area", "mark_text",
71+
"mark_violin", "mark_map",
7172
"scale_color", "scale_fill", "scale_size", "scale_alpha",
7273
"scale_shape", "scale_linetype", "scale_x", "scale_y",
7374
"project_cartesian", "project_polar", "project_parallel",

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,17 @@ compose_grid(p1, p2, tag_levels = "A") |>
7979
Every plotit chart follows a consistent pipeline:
8080

8181
```
82-
data |> plotit(encode(...)) |> mark_*() |> scale_*() |> label_*() |> project_*() |> split_*() |> style() |> export()
82+
data |> plotit(encode(...)) |> mark_*() |> scale_*() |> split_*() |> project_*() |> label_*() |> style() |> export()
8383
```
8484

8585
| Step | Verb | Role |
8686
|:---|:---|:---|
8787
| 1. Initialise | `plotit()` + `encode()` | Bind data and aesthetic mappings |
8888
| 2. Layer | `mark_*()` | Add geometric layers (points, lines, bars, …) |
8989
| 3. Scale | `scale_*()` | Control how data maps to visual properties |
90-
| 4. Label | `label_*()` | Set titles, axis labels, legend titles |
90+
| 4. Facet | `split_*()` | Split into small multiples |
9191
| 5. Coordinate | `project_*()` | Choose coordinate system (cartesian, polar, map) |
92-
| 6. Facet | `split_*()` | Split into small multiples |
92+
| 6. Label | `label_*()` | Set titles, axis labels, legend titles |
9393
| 7. Theme | `style()` | Apply a complete theme |
9494
| 8. Export | `export()` | Render to file |
9595

@@ -107,10 +107,14 @@ compose_*(p1, p2, ...) |> label_*() |> style() |> export()
107107
|:---|:---|:---|
108108
| `mark_point()` | `geom_point()` | Scatter plots |
109109
| `mark_line()` | `geom_line()` | Lines and trends |
110+
| `mark_area()` | `geom_area()` | Filled area / stream graph |
110111
| `mark_bar()` | `geom_bar()` / `geom_col()` | Bar charts |
112+
| `mark_text()` | `geom_text()` / `ggrepel` | Text labels and annotations |
111113
| `mark_boxplot()` | `geom_boxplot()` | Box-and-whisker plots |
112114
| `mark_histogram()` | `geom_histogram()` | Histograms |
113-
| `mark_density()` | `geom_density()` | Kernel density estimates |
115+
| `mark_density()` | `geom_density()` | 1D kernel density |
116+
| `mark_violin()` | `geom_violin()` | Violin plots |
117+
| `mark_map()` | `geom_sf()` | Geographic maps |
114118

115119
### `scale_*` — Data-to-visual mapping
116120

@@ -167,6 +171,13 @@ compose_*(p1, p2, ...) |> label_*() |> style() |> export()
167171
| `style_default()` | Restore plotit's built-in theme |
168172
| `export()` | Render to file (pdf, png, svg, …) |
169173

174+
### Custom extensions
175+
176+
| Function | Description |
177+
|:---|:---|
178+
| `make_mark()` | Register a custom mark from any ggplot2 geom |
179+
| `make_theme()` | Create a reusable theme preset function |
180+
170181
## Documentation
171182

172183
Full documentation is available at [zorrooz.github.io/plotit](https://zorrooz.github.io/plotit/).

0 commit comments

Comments
 (0)