Skip to content

Commit a445a86

Browse files
zorroozclaude
andcommitted
feat: add make_mark and make_theme factory functions
- make_mark: registers custom S7 mark from any ggplot2 geom - make_theme: creates reusable theme preset functions - New file R/factory.R with full roxygen2 docs - Added factory.R to DESCRIPTION Collate - Tests: create custom mark, non-mark_ name warning, custom theme, custom base theme - 383 pass, 0 fail Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6b2bfd0 commit a445a86

6 files changed

Lines changed: 182 additions & 1 deletion

File tree

DESCRIPTION

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Imports:
2626
S7 (>= 0.1.0),
2727
cli,
2828
patchwork
29-
Collate:
29+
Collate:
3030
'class.R'
3131
'utils.R'
3232
'style.R'
@@ -35,6 +35,7 @@ Collate:
3535
'compose.R'
3636
'encode.R'
3737
'mark.R'
38+
'factory.R'
3839
'plot.R'
3940
'project.R'
4041
'scale.R'

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export(label_caption)
1515
export(label_legend)
1616
export(label_subtitle)
1717
export(label_title)
18+
export(make_mark)
19+
export(make_theme)
1820
export(mark_area)
1921
export(mark_bar)
2022
export(mark_boxplot)

R/factory.R

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#' @include class.R mark.R
2+
NULL
3+
4+
# ---- make_mark ----
5+
#' Create a custom mark
6+
#'
7+
#' Registers a new S7 generic + method from any ggplot2 geom function,
8+
#' making it available in the plotit pipeline. The new mark behaves
9+
#' identically to built-in marks: it supports `mapping`, `data`,
10+
#' `position`, auto-dodge, and rasterization.
11+
#'
12+
#' @param name Mark name as a string (e.g. `"mark_spoke"`).
13+
#' Should start with `"mark_"`.
14+
#' @param geom_fun A ggplot2 geom function
15+
#' (e.g. `ggplot2::geom_spoke`).
16+
#' @return Invisibly returns the registered S7 generic.
17+
#' @examples
18+
#' make_mark("mark_spoke", ggplot2::geom_spoke)
19+
#' # Now usable in pipeline:
20+
#' \dontrun{
21+
#' df |> plotit(encode(x = x, y = y, radius = r, angle = a)) |> mark_spoke()
22+
#' }
23+
#' @export
24+
make_mark <- function(name, geom_fun) {
25+
if (!is.character(name) || length(name) != 1) {
26+
cli::cli_abort("{.arg name} must be a single string.")
27+
}
28+
if (!grepl("^mark_", name)) {
29+
cli::cli_warn(
30+
"{.arg name} should start with 'mark_', got {.val {name}}."
31+
)
32+
}
33+
34+
generic <- S7::new_generic(
35+
name, "plot",
36+
function(plot, mapping = NULL, data = NULL, position = NULL, ...,
37+
rasterize = FALSE, rasterize_dpi = 300, rasterize_dev = "cairo") {
38+
S7::S7_dispatch()
39+
}
40+
)
41+
._register_mark_method(generic, geom_fun)
42+
invisible(generic)
43+
}
44+
45+
# ---- make_theme ----
46+
#' Create a reusable theme preset
47+
#'
48+
#' Builds a theme function from `ggplot2::theme()` elements and
49+
#' an optional base theme. The returned function applies the theme
50+
#' to a plotit object and can be used anywhere `style()` is used.
51+
#'
52+
#' @param name Name for the theme function as a string
53+
#' (e.g. `"style_dark"`).
54+
#' @param ... Theme elements passed to [ggplot2::theme()].
55+
#' @param base_theme A base ggplot2 theme function
56+
#' (default: [ggplot2::theme_minimal]).
57+
#' @return Invisibly returns the created function.
58+
#' @examples
59+
#' style_dark <- make_theme("style_dark",
60+
#' plot.background = ggplot2::element_rect(fill = "#1a1a1a"),
61+
#' text = ggplot2::element_text(colour = "white"))
62+
#' \dontrun{
63+
#' plotit(iris, encode(x = Sepal.Width, y = Sepal.Length)) |>
64+
#' mark_point() |> style_dark()
65+
#' }
66+
#' @export
67+
make_theme <- function(name, ..., base_theme = ggplot2::theme_minimal) {
68+
force(name)
69+
force(base_theme)
70+
dots <- rlang::list2(...)
71+
72+
fun <- function(plot, base_size = NULL, base_family = NULL) {
73+
thm <- base_theme(
74+
base_size = base_size %||% 11,
75+
base_family = base_family %||% ""
76+
) + do.call(ggplot2::theme, dots)
77+
plot@gg <- plot@gg + thm
78+
plot
79+
}
80+
assign(name, fun, envir = parent.frame())
81+
invisible(fun)
82+
}

man/make_mark.Rd

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/make_theme.Rd

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-mark.R

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,3 +469,34 @@ test_that("mark_corr supports no-reorder", {
469469
p <- plotit(mtcars, encode()) |> mark_corr(reorder = FALSE)
470470
expect_s3_class(p, "plotit::plotit")
471471
})
472+
473+
# ---- make_mark / make_theme ----
474+
test_that("make_mark creates a usable custom mark", {
475+
generic <- make_mark("mark_tile_test", ggplot2::geom_tile)
476+
df <- expand.grid(x = 1:3, y = 1:3)
477+
df$z <- 1:9
478+
p <- generic(plotit(df, encode(x = x, y = y, fill = z)))
479+
expect_s3_class(p, "plotit::plotit")
480+
expect_length(.built(p)$data, 1)
481+
})
482+
483+
test_that("make_mark warns on non-mark_ name", {
484+
expect_warning(make_mark("foo_bar", ggplot2::geom_point))
485+
})
486+
487+
test_that("make_theme creates a usable theme function", {
488+
style_test <- make_theme("style_test",
489+
plot.title = ggplot2::element_text(colour = "blue"))
490+
p <- plotit(iris, encode(x = Sepal.Width, y = Sepal.Length)) |>
491+
mark_point() |> label_title("Test") |> style_test()
492+
expect_s3_class(p, "plotit::plotit")
493+
})
494+
495+
test_that("make_theme with custom base theme works", {
496+
style_custom <- make_theme("style_custom",
497+
panel.grid.major = ggplot2::element_line(colour = "grey90"),
498+
base_theme = ggplot2::theme_bw)
499+
p <- plotit(iris, encode(x = Sepal.Width, y = Sepal.Length)) |>
500+
mark_point() |> style_custom()
501+
expect_s3_class(p, "plotit::plotit")
502+
})

0 commit comments

Comments
 (0)