diff --git a/DESCRIPTION b/DESCRIPTION index 79205ce58e..cbe0a368e2 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -177,6 +177,8 @@ Collate: 'grob-dotstack.R' 'grob-null.R' 'grouping.R' + 'properties.R' + 'margins.R' 'theme-elements.R' 'guide-.R' 'guide-axis.R' @@ -201,7 +203,6 @@ Collate: 'layer-sf.R' 'layout.R' 'limits.R' - 'margins.R' 'performance.R' 'plot-build.R' 'plot-construction.R' diff --git a/NAMESPACE b/NAMESPACE index 3868c746f0..f2c0e9e5dd 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,19 +1,25 @@ # Generated by roxygen2: do not edit by hand +S3method("$","ggplot2::element") S3method("$","ggplot2::gg") S3method("$","ggplot2::theme") S3method("$",ggproto) S3method("$",ggproto_parent) +S3method("$<-","ggplot2::element") S3method("$<-","ggplot2::gg") S3method("$<-","ggplot2::mapping") +S3method("[","ggplot2::element") S3method("[","ggplot2::gg") S3method("[","ggplot2::mapping") S3method("[",mapped_discrete) +S3method("[<-","ggplot2::element") S3method("[<-","ggplot2::gg") S3method("[<-","ggplot2::mapping") S3method("[<-",mapped_discrete) +S3method("[[","ggplot2::element") S3method("[[","ggplot2::gg") S3method("[[",ggproto) +S3method("[[<-","ggplot2::element") S3method("[[<-","ggplot2::gg") S3method("[[<-","ggplot2::mapping") S3method(.DollarNames,ggproto) @@ -23,12 +29,6 @@ S3method(autolayer,default) S3method(autoplot,default) S3method(c,mapped_discrete) S3method(drawDetails,zeroGrob) -S3method(element_grob,element_blank) -S3method(element_grob,element_line) -S3method(element_grob,element_point) -S3method(element_grob,element_polygon) -S3method(element_grob,element_rect) -S3method(element_grob,element_text) S3method(format,ggproto) S3method(format,ggproto_method) S3method(fortify,"NULL") @@ -75,10 +75,6 @@ S3method(limits,character) S3method(limits,factor) S3method(limits,numeric) S3method(makeContext,dotstackGrob) -S3method(merge_element,default) -S3method(merge_element,element) -S3method(merge_element,element_blank) -S3method(merge_element,margin) S3method(pattern_alpha,GridPattern) S3method(pattern_alpha,GridTilingPattern) S3method(pattern_alpha,default) @@ -330,6 +326,7 @@ export(draw_key_vline) export(draw_key_vpath) export(dup_axis) export(el_def) +export(element) export(element_blank) export(element_geom) export(element_grob) diff --git a/R/coord-sf.R b/R/coord-sf.R index d603d57de7..63e5ed4a26 100644 --- a/R/coord-sf.R +++ b/R/coord-sf.R @@ -334,13 +334,13 @@ CoordSf <- ggproto("CoordSf", CoordCartesian, # we don't draw the graticules if the major panel grid is # turned off - if (inherits(el, "element_blank")) { + if (is_theme_element(el, "blank")) { grobs <- list(element_render(theme, "panel.background")) } else { line_gp <- gg_par( - col = el$colour, - lwd = el$linewidth, - lty = el$linetype + col = el@colour, + lwd = el@linewidth, + lty = el@linetype ) grobs <- c( list(element_render(theme, "panel.background")), diff --git a/R/geom-.R b/R/geom-.R index e2d8806b35..94a829f4a8 100644 --- a/R/geom-.R +++ b/R/geom-.R @@ -245,7 +245,7 @@ eval_from_theme <- function(aesthetics, theme, class = NULL) { return(aesthetics) } - element <- calc_element("geom", theme) %||% .default_geom_element + el <- calc_element("geom", theme) %||% .default_geom_element class <- setdiff(class, c("Geom", "ggproto", "gg")) if (length(class) > 0) { @@ -260,12 +260,12 @@ eval_from_theme <- function(aesthetics, theme, class = NULL) { # Inherit up to parent geom class if (length(class) > 0) { for (cls in rev(class)) { - element <- combine_elements(theme[[cls]], element) + el <- combine_elements(theme[[cls]], el) } } } - lapply(aesthetics[themed], eval_tidy, data = element) + lapply(aesthetics[themed], eval_tidy, data = S7::props(el)) } #' Graphical units diff --git a/R/geom-label.R b/R/geom-label.R index a9d288996f..652ae9b39b 100644 --- a/R/geom-label.R +++ b/R/geom-label.R @@ -88,7 +88,7 @@ GeomLabel <- ggproto("GeomLabel", Geom, data <- coord$transform(data, panel_params) data$vjust <- compute_just(data$vjust, data$y, data$x, data$angle) data$hjust <- compute_just(data$hjust, data$x, data$y, data$angle) - if (!is_margin("margin")) { + if (!is_margin(label.padding)) { label.padding <- rep(label.padding, length.out = 4) } diff --git a/R/guide-.R b/R/guide-.R index 8d26a95628..a888659cf1 100644 --- a/R/guide-.R +++ b/R/guide-.R @@ -376,6 +376,7 @@ Guide <- ggproto( # Renders tickmarks build_ticks = function(key, elements, params, position = params$position, length = elements$ticks_length) { + force(length) if (!is_theme_element(elements)) { elements <- elements$ticks } diff --git a/R/guide-axis-logticks.R b/R/guide-axis-logticks.R index 37273cba06..9da147870d 100644 --- a/R/guide-axis-logticks.R +++ b/R/guide-axis-logticks.R @@ -119,7 +119,7 @@ guide_axis_logticks <- function( allow_null = TRUE ) check_bool(expanded) - check_inherits(short.theme, c("element_blank", "element_line")) + check_inherits(short.theme, c("ggplot2::element_blank", "ggplot2::element_line")) new_guide( available_aes = c("x", "y"), diff --git a/R/guide-axis-theta.R b/R/guide-axis-theta.R index 5183e802fc..b75528d347 100644 --- a/R/guide-axis-theta.R +++ b/R/guide-axis-theta.R @@ -154,7 +154,7 @@ GuideAxisTheta <- ggproto( } offset <- max(unit(0, "pt"), elements$major_length, elements$minor_length) - elements$offset <- offset + max(elements$text$margin %||% unit(0, "pt")) + elements$offset <- offset + max(elements$text@margin %||% unit(0, "pt")) elements }, @@ -184,7 +184,7 @@ GuideAxisTheta <- ggproto( build_labels = function(key, elements, params) { - if (inherits(elements$text, "element_blank")) { + if (is_theme_element(elements$text, "blank")) { return(zeroGrob()) } @@ -198,7 +198,7 @@ GuideAxisTheta <- ggproto( # Resolve text angle if (is.waiver(params$angle) || is.null(params$angle)) { - angle <- elements$text$angle + angle <- elements$text@angle } else { angle <- flip_text_angle(params$angle - rad2deg(key$theta)) } @@ -268,20 +268,20 @@ GuideAxisTheta <- ggproto( key <- params$key key <- vec_slice(key, !is.na(key$.label) & nzchar(key$.label)) labels <- validate_labels(key$.label) - if (length(labels) == 0 || inherits(elements$text, "element_blank")) { + if (length(labels) == 0 || is_theme_element(elements$text, "blank")) { return(list(offset = offset)) } # Resolve text angle if (is.waiver(params$angle %||% waiver())) { - angle <- elements$text$angle + angle <- elements$text@angle } else { angle <- flip_text_angle(params$angle - rad2deg(key$theta)) } angle <- key$theta + deg2rad(angle) # Set margin - margin <- rep(max(elements$text$margin), length.out = 4) + margin <- rep(max(elements$text@margin), length.out = 4) # Measure size of each individual label single_labels <- lapply(labels, function(lab) { @@ -365,7 +365,7 @@ GuideAxisTheta <- ggproto( theta_tickmarks <- function(key, element, length, offset = NULL) { n_breaks <- nrow(key) - if (n_breaks < 1 || inherits(element, "element_blank")) { + if (n_breaks < 1 || is_theme_element(element, "blank")) { return(zeroGrob()) } diff --git a/R/guide-axis.R b/R/guide-axis.R index d445900071..03dffcaebd 100644 --- a/R/guide-axis.R +++ b/R/guide-axis.R @@ -259,10 +259,10 @@ GuideAxis <- ggproto( override_elements = function(params, elements, theme) { elements$text <- label_angle_heuristic(elements$text, params$position, params$angle) - if (inherits(elements$ticks, "element_blank")) { + if (is_theme_element(elements$ticks, "blank")) { elements$major_length <- unit(0, "cm") } - if (inherits(elements$minor, "element_blank") || isFALSE(params$minor.ticks)) { + if (is_theme_element(elements$minor, "blank") || isFALSE(params$minor.ticks)) { elements$minor_length <- unit(0, "cm") } return(elements) @@ -379,7 +379,7 @@ GuideAxis <- ggproto( # Ticks major_cm <- convertUnit(elements$major_length, "cm", valueOnly = TRUE) range <- range(0, major_cm) - if (params$minor.ticks && !inherits(elements$minor, "element_blank")) { + if (params$minor.ticks && !is_theme_element(elements$minor, "blank")) { minor_cm <- convertUnit(elements$minor_length, "cm", valueOnly = TRUE) range <- range(range, minor_cm) } @@ -450,13 +450,13 @@ GuideAxis <- ggproto( # rather than dimensions of this axis alone. if (has_labels && params$position %in% c("left", "right")) { where <- layout$l[-c(1, length(layout$l))] - just <- with(elements$text, rotate_just(angle, hjust, vjust))$hjust %||% 0.5 + just <- with(S7::props(elements$text), rotate_just(angle, hjust, vjust))$hjust %||% 0.5 gt <- gtable_add_cols(gt, unit(just, "null"), pos = min(where) - 1) gt <- gtable_add_cols(gt, unit(1 - just, "null"), pos = max(where) + 1) } if (has_labels && params$position %in% c("top", "bottom")) { where <- layout$t[-c(1, length(layout$t))] - just <- with(elements$text, rotate_just(angle, hjust, vjust))$vjust %||% 0.5 + just <- with(S7::props(elements$text), rotate_just(angle, hjust, vjust))$vjust %||% 0.5 gt <- gtable_add_rows(gt, unit(1 - just, "null"), pos = min(where) - 1) gt <- gtable_add_rows(gt, unit(just, "null"), pos = max(where) + 1) } @@ -590,7 +590,7 @@ axis_label_priority_between <- function(x, y) { #' overridden from the user- or theme-supplied element. #' @noRd label_angle_heuristic <- function(element, position, angle) { - if (!inherits(element, "element_text") + if (!is_theme_element(element, "text") || is.null(position) || is.null(angle %|W|% NULL)) { return(element) @@ -612,8 +612,8 @@ label_angle_heuristic <- function(element, position, angle) { hjust <- switch(position, left = cosine, right = 1 - cosine, top = 1 - sine, sine) vjust <- switch(position, left = 1 - sine, right = sine, top = 1 - cosine, cosine) - element$angle <- angle %||% element$angle - element$hjust <- hjust %||% element$hjust - element$vjust <- vjust %||% element$vjust + element@angle <- angle %||% element@angle + element@hjust <- hjust %||% element@hjust + element@vjust <- vjust %||% element@vjust element } diff --git a/R/guide-custom.R b/R/guide-custom.R index f602bfc843..1a6d977c7f 100644 --- a/R/guide-custom.R +++ b/R/guide-custom.R @@ -113,7 +113,7 @@ GuideCustom <- ggproto( gt <- self$add_title( gt, title, title_position, - with(elems$title, rotate_just(angle, hjust, vjust)) + with(S7::props(elems$title), rotate_just(angle, hjust, vjust)) ) # Add padding and background diff --git a/R/guide-legend.R b/R/guide-legend.R index b728752518..294c573725 100644 --- a/R/guide-legend.R +++ b/R/guide-legend.R @@ -325,7 +325,7 @@ GuideLegend <- ggproto( # Resolve title. The trick here is to override the main text element, so # that any settings declared in `legend.title` will be honoured but we have # custom defaults for the guide. - margin <- calc_element("text", theme)$margin + margin <- calc_element("text", theme)@margin title <- theme(text = element_text( hjust = 0, vjust = 0.5, margin = position_margin(title_position, margin, gap) @@ -573,7 +573,7 @@ GuideLegend <- ggproto( gt <- self$add_title( gt, grobs$title, elements$title_position, - with(elements$title, rotate_just(angle, hjust, vjust)) + with(S7::props(elements$title), rotate_just(angle, hjust, vjust)) ) gt <- gtable_add_padding(gt, unit(elements$padding, "cm")) @@ -690,13 +690,17 @@ keep_key_data <- function(key, data, aes, show) { position_margin <- function(position, margin = NULL, gap = unit(0, "pt")) { margin <- margin %||% margin() - switch( + margin <- switch( position, top = replace(margin, 3, margin[3] + gap), bottom = replace(margin, 1, margin[1] + gap), left = replace(margin, 2, margin[2] + gap), right = replace(margin, 4, margin[4] + gap) ) + # We have to manually reconstitute the class because the 'simpleUnit' class + # might be dropped by the replacement operation. + class(margin) <- c("ggplot2::margin", class(margin), "S7_object") + margin } # Function implementing backward compatibility with the old way of specifying diff --git a/R/margins.R b/R/margins.R index 561aefb7bd..a010a87738 100644 --- a/R/margins.R +++ b/R/margins.R @@ -1,17 +1,21 @@ +#' @include properties.R + #' @param t,r,b,l Dimensions of each margin. (To remember order, think trouble). #' @param unit Default units of dimensions. Defaults to "pt" so it #' can be most easily scaled with the text. #' @rdname element #' @export -margin <- function(t = 0, r = 0, b = 0, l = 0, unit = "pt") { - u <- unit(c(t, r, b, l), unit) - class(u) <- c("margin", class(u)) - u -} +margin <- S7::new_class( + "margin", parent = S7::new_S3_class(c("simpleUnit", "unit", "unit_v2")), + constructor = function(t = 0, r = 0, b = 0, l = 0, unit = "pt") { + u <- unit(c(t, r, b, l), unit) + S7::new_object(u) + } +) #' @export #' @rdname is_tests -is_margin <- function(x) inherits(x, "margin") +is_margin <- function(x) S7::S7_inherits(x, margin) is.margin <- function(x) lifecycle::deprecate_stop("3.5.2", "is.margin()", "is_margin()") #' @rdname element diff --git a/R/plot-build.R b/R/plot-build.R index 89e00abbd3..a8fd5498cc 100644 --- a/R/plot-build.R +++ b/R/plot-build.R @@ -337,7 +337,7 @@ table_add_tag <- function(table, label, theme) { return(table) } element <- calc_element("plot.tag", theme) - if (inherits(element, "element_blank")) { + if (is_theme_element(element, "blank")) { return(table) } @@ -382,20 +382,20 @@ table_add_tag <- function(table, label, theme) { if (location %in% c("plot", "panel")) { if (!is.numeric(position)) { if (right || left) { - x <- (1 - element$hjust) * width + x <- (1 - element@hjust) * width if (right) { x <- unit(1, "npc") - x } } else { - x <- unit(element$hjust, "npc") + x <- unit(element@hjust, "npc") } if (top || bottom) { - y <- (1 - element$vjust) * height + y <- (1 - element@vjust) * height if (top) { y <- unit(1, "npc") - y } } else { - y <- unit(element$vjust, "npc") + y <- unit(element@vjust, "npc") } } else { x <- unit(position[1], "npc") diff --git a/R/plot-construction.R b/R/plot-construction.R index 21c2838b4d..5d9b550812 100644 --- a/R/plot-construction.R +++ b/R/plot-construction.R @@ -96,7 +96,7 @@ add_ggplot <- function(p, object, objectname) { #' Add custom objects to ggplot #' #' This generic allows you to add your own methods for adding custom objects to -#' a ggplot with [+.gg]. +#' a ggplot with [+.gg][add_gg]. #' #' @param object An object to add to the plot #' @param plot The ggplot object to add `object` to @@ -115,7 +115,9 @@ add_ggplot <- function(p, object, objectname) { #' @keywords internal #' @export #' @examples -#' S7::method(ggplot_add, list(S7::new_S3_class("element_text"), class_ggplot)) <- +#' # making a new method for the generic +#' # in this example, we enable adding text elements +#' S7::method(ggplot_add, list(element_text, class_ggplot)) <- #' function(object, plot, ...) { #' plot + theme(text = object) #' } @@ -126,7 +128,6 @@ add_ggplot <- function(p, object, objectname) { #' element_text(colour = "red") #' #' # clean-up -#' rm("element_text", envir = ggplot_add@methods) ggplot_add <- S7::new_generic("ggplot_add", c("object", "plot")) S7::method(ggplot_add, list(S7::class_any, class_ggplot)) <- diff --git a/R/properties.R b/R/properties.R new file mode 100644 index 0000000000..493d787f05 --- /dev/null +++ b/R/properties.R @@ -0,0 +1,45 @@ +property_boolean <- function(allow_null = FALSE, default = TRUE) { + class <- S7::class_logical + class <- if (allow_null) S7::new_union(class, NULL) else class + validator <- function(value) { + if ((allow_null && is.null(value)) || is_bool(value)) { + return(character()) + } + "must be a boolean" + } + S7::new_property( + class = class, + validator = validator, + default = default + ) +} + +property_choice <- function(options, allow_null = FALSE, default = NULL) { + force(options) + class <- S7::class_character + class <- if (allow_null) S7::new_union(class, NULL) else class + validator <- function(value) { + if (allow_null && is.null(value)) { + return(character()) + } + if (!is_string(value)) { + return(as_cli("must be a string, not {.obj_type_friendly {value}}")) + } + if (value %in% options) { + return(character()) + } + as_cli("must be one of {.or {.val {options}}}") + } + S7::new_property( + class = class, + validator = validator, + default = default + ) +} + +property_nullable <- function(class = S7::class_any, ...) { + S7::new_property( + class = S7::new_union(NULL, class), + ... + ) +} diff --git a/R/save.R b/R/save.R index e8febcbb0c..b4b1cc7226 100644 --- a/R/save.R +++ b/R/save.R @@ -244,7 +244,8 @@ get_plot_background <- function(plot, bg = NULL, default = "transparent") { if (!is_ggplot(plot)) { return(default) } - calc_element("plot.background", plot_theme(plot))$fill %||% default + bg <- calc_element("plot.background", plot_theme(plot)) + try_prop(bg, "fill") %||% "transparent" } validate_device <- function(device, filename = NULL, dpi = 300, call = caller_env()) { diff --git a/R/theme-current.R b/R/theme-current.R index c6848c7d76..e42f8c1e68 100644 --- a/R/theme-current.R +++ b/R/theme-current.R @@ -40,7 +40,7 @@ NULL #' @return `set_theme()`, `update_theme()`, and `replace_theme()` #' invisibly return the previous theme so you can easily save it, then #' later restore it. -#' @seealso [+.gg()] +#' @seealso [add_gg()] #' @export #' @examples #' p <- ggplot(mtcars, aes(mpg, wt)) + diff --git a/R/theme-defaults.R b/R/theme-defaults.R index 7b5bb286f7..3823bf45c3 100644 --- a/R/theme-defaults.R +++ b/R/theme-defaults.R @@ -127,11 +127,12 @@ theme_grey <- function(base_size = 11, base_family = "", # by others line = element_line( colour = ink, linewidth = base_line_size, - linetype = 1, lineend = "butt" + linetype = 1, lineend = "butt", linejoin = "round" ), rect = element_rect( fill = paper, colour = ink, - linewidth = base_rect_size, linetype = 1 + linewidth = base_rect_size, linetype = 1, + linejoin = "round" ), text = element_text( family = base_family, face = "plain", @@ -153,7 +154,7 @@ theme_grey <- function(base_size = 11, base_family = "", polygon = element_polygon( fill = paper, colour = ink, - linewidth = base_rect_size, linetype = 1 + linewidth = base_rect_size, linetype = 1, linejoin = "round" ), geom = element_geom( @@ -525,7 +526,7 @@ theme_void <- function(base_size = 11, base_family = "", line = element_blank(), rect = element_rect( fill = paper, colour = NA, linewidth = 0, linetype = 1, - inherit.blank = FALSE + inherit.blank = FALSE, linejoin = "round" ), polygon = element_blank(), point = element_blank(), @@ -614,11 +615,11 @@ theme_test <- function(base_size = 11, base_family = "", t <- theme( line = element_line( colour = ink, linewidth = base_line_size, - linetype = 1, lineend = "butt" + linetype = 1, lineend = "butt", linejoin = "round" ), rect = element_rect( fill = paper, colour = ink, - linewidth = base_rect_size, linetype = 1 + linewidth = base_rect_size, linetype = 1, linejoin = "round" ), text = element_text( family = base_family, face = "plain", @@ -633,7 +634,7 @@ theme_test <- function(base_size = 11, base_family = "", ), polygon = element_polygon( fill = paper, colour = ink, - linewidth = base_rect_size, linetype = 1 + linewidth = base_rect_size, linetype = 1, linejoin = "round" ), title = element_text(family = header_family), spacing = unit(half_line, "pt"), diff --git a/R/theme-elements.R b/R/theme-elements.R index a16302b6db..0698bb12dd 100644 --- a/R/theme-elements.R +++ b/R/theme-elements.R @@ -81,55 +81,93 @@ NULL #' @export #' @rdname element -element_blank <- function() { - structure( - list(), - class = c("element_blank", "element") - ) -} +element <- S7::new_class("element", abstract = TRUE) #' @export #' @rdname element -element_rect <- function(fill = NULL, colour = NULL, linewidth = NULL, - linetype = NULL, color = NULL, inherit.blank = FALSE, size = deprecated()) { +element_blank <- S7::new_class("element_blank", parent = element) + +# All properties are listed here so they can easily be recycled in the different +# element classes +#' @include properties.R +#' @include margins.R +element_props <- list( + fill = property_nullable(S7::class_character | S7::new_S3_class("GridPattern") | S7::class_logical), + colour = property_nullable(S7::class_character | S7::class_logical), + family = property_nullable(S7::class_character), + hjust = property_nullable(S7::class_numeric), + vjust = property_nullable(S7::class_numeric), + angle = property_nullable(S7::class_numeric), + size = property_nullable(S7::class_numeric), + lineheight = property_nullable(S7::class_numeric), + margin = property_nullable(margin), + face = property_choice(c("plain", "bold", "italic", "oblique", "bold.italic"), allow_null = TRUE), + linewidth = property_nullable(S7::class_numeric), + linetype = property_nullable(S7::class_numeric | S7::class_character), + lineend = property_choice(c("round", "butt", "square"), allow_null = TRUE), + linejoin = property_choice(c("round", "mitre", "bevel"), allow_null = TRUE), + shape = property_nullable(S7::class_numeric | S7::class_character), + arrow = property_nullable(S7::new_S3_class("arrow") | S7::class_logical), + arrow.fill = property_nullable(S7::class_character | S7::class_logical), + debug = property_boolean(allow_null = TRUE, default = NULL), + inherit.blank = property_boolean(default = FALSE) +) - if (lifecycle::is_present(size)) { - deprecate_warn0("3.4.0", "element_rect(size)", "element_rect(linewidth)") - linewidth <- size +#' @export +#' @rdname element +element_rect <- S7::new_class( + "element_rect", parent = element, + properties = element_props[c("fill", "colour", + "linewidth", "linetype", "linejoin", + "inherit.blank")], + constructor = function(fill = NULL, colour = NULL, linewidth = NULL, + linetype = NULL, color = NULL, linejoin = NULL, + inherit.blank = FALSE, size = deprecated()){ + if (lifecycle::is_present(size)) { + deprecate_warn0("3.4.0", "element_rect(size)", "element_rect(linewidth)") + linewidth <- size + } + S7::new_object( + S7::S7_object(), + fill = fill, colour = color %||% colour, + linewidth = linewidth, linetype = linetype, linejoin = linejoin, + inherit.blank = inherit.blank + ) } - - if (!is.null(color)) colour <- color - structure( - list(fill = fill, colour = colour, linewidth = linewidth, linetype = linetype, - inherit.blank = inherit.blank), - class = c("element_rect", "element") - ) -} +) #' @export #' @rdname element -#' @param lineend Line end Line end style (round, butt, square) +#' @param linejoin Line join style, one of `"round"`, `"mitre"` or `"bevel"`. +#' @param lineend Line end style, one of `"round"`, `"butt"` or `"square"`. #' @param arrow Arrow specification, as created by [grid::arrow()] -element_line <- function(colour = NULL, linewidth = NULL, linetype = NULL, - lineend = NULL, color = NULL, arrow = NULL, arrow.fill = NULL, - inherit.blank = FALSE, size = deprecated()) { - - if (lifecycle::is_present(size)) { - deprecate_warn0("3.4.0", "element_line(size)", "element_line(linewidth)") - linewidth <- size +element_line <- S7::new_class( + "element_line", parent = element, + properties = element_props[c( + "colour", "linewidth", "linetype", "lineend", "linejoin", + "arrow", "arrow.fill", + "inherit.blank" + )], + constructor = function(colour = NULL, linewidth = NULL, linetype = NULL, + lineend = NULL, color = NULL, linejoin = NULL, + arrow = NULL, arrow.fill = NULL, + inherit.blank = FALSE, size = deprecated()) { + if (lifecycle::is_present(size)) { + deprecate_warn0("3.4.0", "element_line(size)", "element_line(linewidth)") + linewidth <- size + } + colour <- color %||% colour + S7::new_object( + S7::S7_object(), + colour = colour, + linewidth = linewidth, linetype = linetype, lineend = lineend, + linejoin = linejoin, + arrow = arrow %||% FALSE, + arrow.fill = arrow.fill %||% colour, + inherit.blank = inherit.blank + ) } - - colour <- color %||% colour - arrow.fill <- arrow.fill %||% colour - arrow <- arrow %||% FALSE - - structure( - list(colour = colour, linewidth = linewidth, linetype = linetype, lineend = lineend, - arrow = arrow, arrow.fill = arrow.fill, inherit.blank = inherit.blank), - class = c("element_line", "element") - ) -} - +) #' @param family Font family #' @param face Font face ("plain", "italic", "bold", "bold.italic") @@ -145,116 +183,121 @@ element_line <- function(colour = NULL, linewidth = NULL, linetype = NULL, #' is anchored. #' @export #' @rdname element -element_text <- function(family = NULL, face = NULL, colour = NULL, - size = NULL, hjust = NULL, vjust = NULL, angle = NULL, lineheight = NULL, - color = NULL, margin = NULL, debug = NULL, inherit.blank = FALSE) { - - if (!is.null(color)) colour <- color - - n <- max( - length(family), length(face), length(colour), length(size), - length(hjust), length(vjust), length(angle), length(lineheight) - ) - if (n > 1) { - cli::cli_warn(c( - "Vectorized input to {.fn element_text} is not officially supported.", - "i" = "Results may be unexpected or may change in future versions of ggplot2." - )) - } - - - structure( - list(family = family, face = face, colour = colour, size = size, +element_text <- S7::new_class( + "element_text", parent = element, + properties = element_props[c( + "family", "face", "colour", "size", "hjust", "vjust", "angle", "lineheight", + "margin", "debug", "inherit.blank" + )], + constructor = function(family = NULL, face = NULL, colour = NULL, + size = NULL, hjust = NULL, vjust = NULL, angle = NULL, + lineheight = NULL, color = NULL, margin = NULL, + debug = NULL, inherit.blank = FALSE) { + n <- max( + length(family), length(face), length(colour), length(size), + length(hjust), length(vjust), length(angle), length(lineheight) + ) + if (n > 1) { + cli::cli_warn(c( + "Vectorized input to {.fn element_text} is not officially supported.", + "i" = "Results may be unexpected or may change in future versions of ggplot2." + )) + } + + colour <- color %||% colour + S7::new_object( + S7::S7_object(), + family = family, face = face, colour = colour, size = size, hjust = hjust, vjust = vjust, angle = angle, lineheight = lineheight, - margin = margin, debug = debug, inherit.blank = inherit.blank), - class = c("element_text", "element") - ) -} - -#' @export -#' @param type For testing elements: the type of element to expect. One of -#' `"blank"`, `"rect"`, `"line"` or `"text"`. -#' @rdname is_tests -is_theme_element <- function(x, type = "any") { - switch( - type %||% "any", - any = inherits(x, "element"), - rect = inherits(x, "element_rect"), - line = inherits(x, "element_line"), - text = inherits(x, "element_text"), - blank = inherits(x, "element_blank"), - # TODO: ideally we accept more elements from extensions. We need to - # consider how this will work with S7 classes, where ggplot2 doesn't know - # about the extension's class objects. - FALSE - ) -} + margin = margin, debug = debug, inherit.blank = inherit.blank + ) + } +) #' @export #' @rdname element -element_polygon <- function(fill = NULL, colour = NULL, linewidth = NULL, - linetype = NULL, color = NULL, - inherit.blank = FALSE) { - structure( - list( +element_polygon <- S7::new_class( + "element_polygon", parent = element, + properties = element_props[c( + "fill", "colour", "linewidth", "linetype", "linejoin", "inherit.blank" + )], + constructor = function(fill = NULL, colour = NULL, linewidth = NULL, + linetype = NULL, color = NULL, linejoin = NULL, + inherit.blank = FALSE) { + colour <- color %||% colour + S7::new_object( + S7::S7_object(), fill = fill, colour = color %||% colour, linewidth = linewidth, - linetype = linetype, inherit.blank = inherit.blank - ), - class = c("element_polygon", "element") - ) -} + linetype = linetype, linejoin = linejoin, inherit.blank = inherit.blank + ) + } +) #' @export #' @rdname element -element_point <- function(colour = NULL, shape = NULL, size = NULL, fill = NULL, - stroke = NULL, color = NULL, inherit.blank = FALSE) { - structure( - list( +element_point <- S7::new_class( + "element_point", parent = element, + properties = rename( + element_props[c( + "colour", "shape", "size", "fill", "linewidth", "inherit.blank" + )], + c("linewidth" = "stroke") + ), + constructor = function(colour = NULL, shape = NULL, size = NULL, fill = NULL, + stroke = NULL, color = NULL, inherit.blank = FALSE) { + S7::new_object( + S7::S7_object(), colour = color %||% colour, fill = fill, shape = shape, size = size, stroke = stroke, inherit.blank = inherit.blank - ), - class = c("element_point", "element") - ) -} + ) + } +) #' @param ink Foreground colour. #' @param paper Background colour. #' @param accent Accent colour. #' @export #' @rdname element -element_geom <- function( - # colours - ink = NULL, paper = NULL, accent = NULL, - # linewidth - linewidth = NULL, borderwidth = NULL, - # linetype - linetype = NULL, bordertype = NULL, - # text - family = NULL, fontsize = NULL, - # points - pointsize = NULL, pointshape = NULL, - - colour = NULL, color = NULL, fill = NULL) { - - if (!is.null(fontsize)) { - fontsize <- fontsize / .pt - } - - structure( - list( - ink = ink, - paper = paper, - accent = accent, +element_geom <- S7::new_class( + "element_geom", parent = element, + properties = list( + ink = element_props$colour, + paper = element_props$colour, + accent = element_props$colour, + linewidth = element_props$linewidth, + borderwidth = element_props$linewidth, + linetype = element_props$linetype, + bordertype = element_props$linetype, + family = element_props$family, + fontsize = element_props$size, + pointsize = element_props$size, + pointshape = element_props$shape, + colour = element_props$colour, + fill = element_props$fill + ), + constructor = function( + ink = NULL, paper = NULL, accent = NULL, + linewidth = NULL, borderwidth = NULL, + linetype = NULL, bordertype = NULL, + family = NULL, fontsize = NULL, + pointsize = NULL, pointshape = NULL, + colour = NULL, color = NULL, fill = NULL) { + + if (!is.null(fontsize)) { + fontsize <- fontsize / .pt + } + + S7::new_object( + S7::S7_object(), + ink = ink, paper = paper, accent = accent, linewidth = linewidth, borderwidth = borderwidth, linetype = linetype, bordertype = bordertype, family = family, fontsize = fontsize, pointsize = pointsize, pointshape = pointshape, - colour = color %||% colour, - fill = fill - ), - class = c("element_geom", "element") - ) -} + colour = color %||% colour, fill = fill + ) + } +) .default_geom_element <- element_geom( ink = "black", paper = "white", accent = "#3366FF", @@ -268,6 +311,24 @@ element_geom <- function( #' @export print.element <- function(x, ...) utils::str(x) +#' @export +#' @param type For testing elements: the type of element to expect. One of +#' `"blank"`, `"rect"`, `"line"`, `"text"`, `"polygon"`, `"point"` or `"geom"`. +#' @rdname is_tests +is_theme_element <- function(x, type = "any") { + switch( + type %||% "any", + any = S7::S7_inherits(x, element), + blank = S7::S7_inherits(x, element_blank), + rect = S7::S7_inherits(x, element_rect), + line = S7::S7_inherits(x, element_line), + text = S7::S7_inherits(x, element_text), + polygon = S7::S7_inherits(x, element_polygon), + point = S7::S7_inherits(x, element_point), + geom = S7::S7_inherits(x, element_geom), + FALSE + ) +} #' @param x A single number specifying size relative to parent element. #' @rdname element @@ -276,6 +337,45 @@ rel <- function(x) { structure(x, class = "rel") } +#' @export +`$.ggplot2::element` <- function(x, i) { + # deprecate_soft0("4.1.0", I("`$i`"), I("`@i`")) + `[[`(S7::props(x), i) +} + +#' @export +`[.ggplot2::element` <- function(x, i) { + # deprecate_soft0("4.1.0", I("`[i]`"), I("`S7::props(, i)`")) + `[`(S7::props(x), i) +} + +#' @export +`[[.ggplot2::element` <- function(x, i) { + # deprecate_soft0("4.1.0", I("`[[i]]`"), I("`S7::prop(, i)`")) + `[[`(S7::props(x), i) +} + +#' @export +`$<-.ggplot2::element` <- function(x, i, value) { + # deprecate_soft0("4.1.0", I("`$i <- value`"), I("`@i <- value`")) + S7::props(x) <- `[[<-`(S7::props(x), i, value) + x +} + +#' @export +`[<-.ggplot2::element` <- function(x, i, value) { + # deprecate_soft0("4.1.0", I("`[i] <- value`"), I("`S7::props()[i] <- value`")) + S7::props(x) <- `[<-`(S7::props(x), i, value) + x +} + +#' @export +`[[<-.ggplot2::element` <- function(x, i, value) { + # deprecate_soft0("4.1.0", I("`[[i]] <- value`"), I("S7::prop(, i) <- value")) + S7::props(x) <- `[[<-`(S7::props(x), i, value) + x +} + #' @export print.rel <- function(x, ...) print(noquote(paste(x, " *", sep = ""))) @@ -314,134 +414,131 @@ element_render <- function(theme, element, ..., name = NULL) { #' usually at least position. See the source code for individual methods. #' @keywords internal #' @export -element_grob <- function(element, ...) { - UseMethod("element_grob") -} - -#' @export -element_grob.element_blank <- function(element, ...) zeroGrob() - -#' @export -element_grob.element_rect <- function(element, x = 0.5, y = 0.5, - width = 1, height = 1, - fill = NULL, colour = NULL, linewidth = NULL, linetype = NULL, ..., size = deprecated()) { +element_grob <- S7::new_generic("element_grob", "element") - if (lifecycle::is_present(size)) { - deprecate_warn0("3.4.0", "element_grob.element_rect(size)", "element_grob.element_rect(linewidth)") - linewidth <- size - } +S7::method(element_grob, element_blank) <- function(element, ...) zeroGrob() - # The gp settings can override element_gp - gp <- gg_par(lwd = linewidth, col = colour, fill = fill, lty = linetype) - element_gp <- gg_par(lwd = element$linewidth, col = element$colour, - fill = element$fill, lty = element$linetype) +S7::method(element_grob, element_rect) <- + function(element, x = 0.5, y = 0.5, width = 1, height = 1, + fill = NULL, colour = NULL, + linewidth = NULL, linetype = NULL, linejoin = NULL, + ..., size = deprecated()) { - rectGrob(x, y, width, height, gp = modify_list(element_gp, gp), ...) -} + if (lifecycle::is_present(size)) { + deprecate_warn0("3.4.0", "element_grob.element_rect(size)", "element_grob.element_rect(linewidth)") + linewidth <- size + } + gp <- gg_par(lwd = linewidth, col = colour, fill = fill, lty = linetype, linejoin = linejoin) + element_gp <- gg_par(lwd = element@linewidth, col = element@colour, + fill = element@fill, lty = element@linetype, + linejoin = element@linejoin) -#' @export -element_grob.element_text <- function(element, label = "", x = NULL, y = NULL, - family = NULL, face = NULL, colour = NULL, size = NULL, - hjust = NULL, vjust = NULL, angle = NULL, lineheight = NULL, - margin = NULL, margin_x = FALSE, margin_y = FALSE, ...) { + rectGrob(x, y, width, height, gp = modify_list(element_gp, gp), ...) + } - if (is.null(label)) - return(zeroGrob()) +S7::method(element_grob, element_text) <- + function(element, label = "", x = NULL, y = NULL, + family = NULL, face = NULL, colour = NULL, size = NULL, + hjust = NULL, vjust = NULL, angle = NULL, lineheight = NULL, + margin = NULL, margin_x = FALSE, margin_y = FALSE, ...) { - vj <- vjust %||% element$vjust - hj <- hjust %||% element$hjust - margin <- margin %||% element$margin + if (is.null(label)) + return(zeroGrob()) - angle <- angle %||% element$angle %||% 0 + vj <- vjust %||% element@vjust + hj <- hjust %||% element@hjust + margin <- margin %||% element@margin - # The gp settings can override element_gp - gp <- gg_par(fontsize = size, col = colour, - fontfamily = family, fontface = face, - lineheight = lineheight) - element_gp <- gg_par(fontsize = element$size, col = element$colour, - fontfamily = element$family, fontface = element$face, - lineheight = element$lineheight) + angle <- angle %||% element@angle %||% 0 - titleGrob(label, x, y, hjust = hj, vjust = vj, angle = angle, - gp = modify_list(element_gp, gp), margin = margin, - margin_x = margin_x, margin_y = margin_y, debug = element$debug, ...) -} + # The gp settings can override element_gp + gp <- gg_par(fontsize = size, col = colour, + fontfamily = family, fontface = face, + lineheight = lineheight) + element_gp <- gg_par(fontsize = element@size, col = element@colour, + fontfamily = element@family, fontface = element@face, + lineheight = element@lineheight) + titleGrob(label, x, y, hjust = hj, vjust = vj, angle = angle, + gp = modify_list(element_gp, gp), margin = margin, + margin_x = margin_x, margin_y = margin_y, debug = element@debug, ...) + } +S7::method(element_grob, element_line) <- + function(element, x = 0:1, y = 0:1, + colour = NULL, linewidth = NULL, linetype = NULL, lineend = NULL, + linejoin = NULL, arrow.fill = NULL, + default.units = "npc", id.lengths = NULL, ..., size = deprecated()) { + + if (lifecycle::is_present(size)) { + deprecate_warn0("3.4.0", "element_grob.element_line(size)", "element_grob.element_line(linewidth)") + linewidth <- size + } + + arrow <- if (is.logical(element@arrow) && !element@arrow) { + NULL + } else { + element@arrow + } + if (is.null(arrow)) { + arrow.fill <- colour + element@arrow.fill <- element@colour + } + + # The gp settings can override element_gp + gp <- gg_par( + col = colour, fill = arrow.fill %||% colour, + lwd = linewidth, lty = linetype, lineend = lineend, linejoin = linejoin + ) + element_gp <- gg_par( + col = element@colour, fill = element@arrow.fill %||% element@colour, + lwd = element@linewidth, lty = element@linetype, + lineend = element@lineend, linejoin = element@linejoin + ) -#' @export -element_grob.element_line <- function(element, x = 0:1, y = 0:1, - colour = NULL, linewidth = NULL, linetype = NULL, lineend = NULL, - arrow.fill = NULL, - default.units = "npc", id.lengths = NULL, ..., size = deprecated()) { - - if (lifecycle::is_present(size)) { - deprecate_warn0("3.4.0", "element_grob.element_line(size)", "element_grob.element_line(linewidth)") - linewidth <- size + polylineGrob( + x, y, default.units = default.units, + gp = modify_list(element_gp, gp), + id.lengths = id.lengths, arrow = arrow, ... + ) } - arrow <- if (is.logical(element$arrow) && !element$arrow) { - NULL - } else { - element$arrow - } - if (is.null(arrow)) { - arrow.fill <- colour - element$arrow.fill <- element$colour +S7::method(element_grob, element_polygon) <- + function(element, x = c(0, 0.5, 1, 0.5), + y = c(0.5, 1, 0.5, 0), fill = NULL, + colour = NULL, linewidth = NULL, + linetype = NULL, linejoin = NULL, ..., + id = NULL, id.lengths = NULL, + pathId = NULL, pathId.lengths = NULL) { + + gp <- gg_par(lwd = linewidth, col = colour, fill = fill, + lty = linetype, linejoin = linejoin) + element_gp <- gg_par(lwd = element@linewidth, col = element@colour, + fill = element@fill, lty = element@linetype, + linejoin = element@linejoin) + pathGrob( + x = x, y = y, gp = modify_list(element_gp, gp), ..., + # We swap the id logic so that `id` is always the (super)group id + # (consistent with `polygonGrob()`) and `pathId` always the subgroup id. + pathId = id, pathId.lengths = id.lengths, + id = pathId, id.lengths = pathId.lengths + ) } - # The gp settings can override element_gp - gp <- gg_par( - col = colour, fill = arrow.fill %||% colour, - lwd = linewidth, lty = linetype, lineend = lineend - ) - element_gp <- gg_par( - col = element$colour, fill = element$arrow.fill %||% element$colour, - lwd = element$linewidth, lty = element$linetype, - lineend = element$lineend - ) - - polylineGrob( - x, y, default.units = default.units, - gp = modify_list(element_gp, gp), - id.lengths = id.lengths, arrow = arrow, ... - ) -} - -#' @export -element_grob.element_polygon <- function(element, x = c(0, 0.5, 1, 0.5), - y = c(0.5, 1, 0.5, 0), fill = NULL, - colour = NULL, linewidth = NULL, - linetype = NULL, ..., - id = NULL, id.lengths = NULL, - pathId = NULL, pathId.lengths = NULL) { - - gp <- gg_par(lwd = linewidth, col = colour, fill = fill, lty = linetype) - element_gp <- gg_par(lwd = element$linewidth, col = element$colour, - fill = element$fill, lty = element$linetype) - pathGrob( - x = x, y = y, gp = modify_list(element_gp, gp), ..., - # We swap the id logic so that `id` is always the (super)group id - # (consistent with `polygonGrob()`) and `pathId` always the subgroup id. - pathId = id, pathId.lengths = id.lengths, - id = pathId, id.lengths = pathId.lengths - ) -} - -#' @export -element_grob.element_point <- function(element, x = 0.5, y = 0.5, colour = NULL, - shape = NULL, fill = NULL, size = NULL, - stroke = NULL, ..., - default.units = "npc") { - - gp <- gg_par(col = colour, fill = fill, pointsize = size, stroke = stroke) - element_gp <- gg_par(col = element$colour, fill = element$fill, - pointsize = element$size, stroke = element$stroke) - shape <- translate_shape_string(shape %||% element$shape %||% 19) - pointsGrob(x = x, y = y, pch = shape, gp = modify_list(element_gp, gp), - default.units = default.units, ...) -} +S7::method(element_grob, element_point) <- + function(element, x = 0.5, y = 0.5, colour = NULL, + shape = NULL, fill = NULL, size = NULL, + stroke = NULL, ..., + default.units = "npc") { + + gp <- gg_par(col = colour, fill = fill, pointsize = size, stroke = stroke) + element_gp <- gg_par(col = element@colour, fill = element@fill, + pointsize = element@size, stroke = element@stroke) + shape <- translate_shape_string(shape %||% element@shape %||% 19) + pointsGrob(x = x, y = y, pch = shape, gp = modify_list(element_gp, gp), + default.units = default.units, ...) + } #' Define and register new theme elements #' @@ -476,7 +573,7 @@ element_grob.element_point <- function(element, x = 0.5, y = 0.5, colour = NULL, #' # plot panels. To do so, it registers a new theme element `ggxyz.panel.annotation` #' register_theme_elements( #' ggxyz.panel.annotation = element_text(color = "blue", hjust = 0.95, vjust = 0.05), -#' element_tree = list(ggxyz.panel.annotation = el_def("element_text", "text")) +#' element_tree = list(ggxyz.panel.annotation = el_def(element_text, "text")) #' ) #' #' # Now the package can define a new coord that includes a panel annotation @@ -595,8 +692,8 @@ check_element_tree <- function(x, arg = caller_arg(x), call = caller_env()) { #' @details #' The function `el_def()` is used to define new or modified element types and #' element inheritance relationships for the element tree. -#' @param class The name of the element class. Examples are "element_line" or -#' "element_text" or "unit", or one of the two reserved keywords "character" or +#' @param class The name of the element class. Examples are `element_line` or +#' `element_text` or "unit", or one of the two reserved keywords "character" or #' "margin". The reserved keyword "character" implies a character #' or numeric vector, not a class called "character". The keyword #' "margin" implies a unit vector of length 4, as created by [margin()]. @@ -607,6 +704,27 @@ check_element_tree <- function(x, arg = caller_arg(x), call = caller_env()) { #' @keywords internal #' @export el_def <- function(class = NULL, inherit = NULL, description = NULL) { + if (is.character(class) && length(class) == 1) { + # Swap S3 class name for S7 class object + class <- switch( + class, + element = element, + element_blank = element_blank, + element_rect = element_rect, + element_line = element_line, + element_text = element_text, + element_polygon = element_polygon, + element_point = element_point, + element_geom = element_geom, + margin = margin, + class + ) + } + # margins often occur in c("unit", "margin", "rel"), we cannot use the + # S7 class here because we don't support heterogeneous lists + if (is.character(class) && length(class) > 1) { + class[class == "margin"] <- "ggplot2::margin" + } list(class = class, inherit = inherit, description = description) } @@ -615,43 +733,43 @@ el_def <- function(class = NULL, inherit = NULL, description = NULL) { # among them. It should not be read from directly, since users may modify the # current element tree stored in ggplot_global$element_tree .element_tree <- list( - line = el_def("element_line"), - rect = el_def("element_rect"), - text = el_def("element_text"), - point = el_def("element_point"), - polygon = el_def("element_polygon"), - geom = el_def("element_geom"), - title = el_def("element_text", "text"), + line = el_def(element_line), + rect = el_def(element_rect), + text = el_def(element_text), + point = el_def(element_point), + polygon = el_def(element_polygon), + geom = el_def(element_geom), + title = el_def(element_text, "text"), spacing = el_def("unit"), margins = el_def(c("margin", "unit")), - axis.line = el_def("element_line", "line"), - axis.text = el_def("element_text", "text"), - axis.title = el_def("element_text", "title"), - axis.ticks = el_def("element_line", "line"), + axis.line = el_def(element_line, "line"), + axis.text = el_def(element_text, "text"), + axis.title = el_def(element_text, "title"), + axis.ticks = el_def(element_line, "line"), legend.key.size = el_def(c("unit", "rel"), "spacing"), - panel.grid = el_def("element_line", "line"), - panel.grid.major = el_def("element_line", "panel.grid"), - panel.grid.minor = el_def("element_line", "panel.grid"), - strip.text = el_def("element_text", "text"), - - axis.line.x = el_def("element_line", "axis.line"), - axis.line.x.top = el_def("element_line", "axis.line.x"), - axis.line.x.bottom = el_def("element_line", "axis.line.x"), - axis.line.y = el_def("element_line", "axis.line"), - axis.line.y.left = el_def("element_line", "axis.line.y"), - axis.line.y.right = el_def("element_line", "axis.line.y"), - axis.line.theta = el_def("element_line", "axis.line.x"), - axis.line.r = el_def("element_line", "axis.line.y"), - - axis.text.x = el_def("element_text", "axis.text"), - axis.text.x.top = el_def("element_text", "axis.text.x"), - axis.text.x.bottom = el_def("element_text", "axis.text.x"), - axis.text.y = el_def("element_text", "axis.text"), - axis.text.y.left = el_def("element_text", "axis.text.y"), - axis.text.y.right = el_def("element_text", "axis.text.y"), - axis.text.theta = el_def("element_text", "axis.text.x"), - axis.text.r = el_def("element_text", "axis.text.y"), + panel.grid = el_def(element_line, "line"), + panel.grid.major = el_def(element_line, "panel.grid"), + panel.grid.minor = el_def(element_line, "panel.grid"), + strip.text = el_def(element_text, "text"), + + axis.line.x = el_def(element_line, "axis.line"), + axis.line.x.top = el_def(element_line, "axis.line.x"), + axis.line.x.bottom = el_def(element_line, "axis.line.x"), + axis.line.y = el_def(element_line, "axis.line"), + axis.line.y.left = el_def(element_line, "axis.line.y"), + axis.line.y.right = el_def(element_line, "axis.line.y"), + axis.line.theta = el_def(element_line, "axis.line.x"), + axis.line.r = el_def(element_line, "axis.line.y"), + + axis.text.x = el_def(element_text, "axis.text"), + axis.text.x.top = el_def(element_text, "axis.text.x"), + axis.text.x.bottom = el_def(element_text, "axis.text.x"), + axis.text.y = el_def(element_text, "axis.text"), + axis.text.y.left = el_def(element_text, "axis.text.y"), + axis.text.y.right = el_def(element_text, "axis.text.y"), + axis.text.theta = el_def(element_text, "axis.text.x"), + axis.text.r = el_def(element_text, "axis.text.y"), axis.ticks.length = el_def(c("unit", "rel"), "spacing"), axis.ticks.length.x = el_def(c("unit", "rel"), "axis.ticks.length"), @@ -663,28 +781,28 @@ el_def <- function(class = NULL, inherit = NULL, description = NULL) { axis.ticks.length.theta = el_def(c("unit", "rel"), "axis.ticks.length.x"), axis.ticks.length.r = el_def(c("unit", "rel"), "axis.ticks.length.y"), - axis.ticks.x = el_def("element_line", "axis.ticks"), - axis.ticks.x.top = el_def("element_line", "axis.ticks.x"), - axis.ticks.x.bottom = el_def("element_line", "axis.ticks.x"), - axis.ticks.y = el_def("element_line", "axis.ticks"), - axis.ticks.y.left = el_def("element_line", "axis.ticks.y"), - axis.ticks.y.right = el_def("element_line", "axis.ticks.y"), - axis.ticks.theta = el_def("element_line", "axis.ticks.x"), - axis.ticks.r = el_def("element_line", "axis.ticks.y"), - - axis.title.x = el_def("element_text", "axis.title"), - axis.title.x.top = el_def("element_text", "axis.title.x"), - axis.title.x.bottom = el_def("element_text", "axis.title.x"), - axis.title.y = el_def("element_text", "axis.title"), - axis.title.y.left = el_def("element_text", "axis.title.y"), - axis.title.y.right = el_def("element_text", "axis.title.y"), - - axis.minor.ticks.x.top = el_def("element_line", "axis.ticks.x.top"), - axis.minor.ticks.x.bottom = el_def("element_line", "axis.ticks.x.bottom"), - axis.minor.ticks.y.left = el_def("element_line", "axis.ticks.y.left"), - axis.minor.ticks.y.right = el_def("element_line", "axis.ticks.y.right"), - axis.minor.ticks.theta = el_def("element_line", "axis.ticks.theta"), - axis.minor.ticks.r = el_def("element_line", "axis.ticks.r"), + axis.ticks.x = el_def(element_line, "axis.ticks"), + axis.ticks.x.top = el_def(element_line, "axis.ticks.x"), + axis.ticks.x.bottom = el_def(element_line, "axis.ticks.x"), + axis.ticks.y = el_def(element_line, "axis.ticks"), + axis.ticks.y.left = el_def(element_line, "axis.ticks.y"), + axis.ticks.y.right = el_def(element_line, "axis.ticks.y"), + axis.ticks.theta = el_def(element_line, "axis.ticks.x"), + axis.ticks.r = el_def(element_line, "axis.ticks.y"), + + axis.title.x = el_def(element_text, "axis.title"), + axis.title.x.top = el_def(element_text, "axis.title.x"), + axis.title.x.bottom = el_def(element_text, "axis.title.x"), + axis.title.y = el_def(element_text, "axis.title"), + axis.title.y.left = el_def(element_text, "axis.title.y"), + axis.title.y.right = el_def(element_text, "axis.title.y"), + + axis.minor.ticks.x.top = el_def(element_line, "axis.ticks.x.top"), + axis.minor.ticks.x.bottom = el_def(element_line, "axis.ticks.x.bottom"), + axis.minor.ticks.y.left = el_def(element_line, "axis.ticks.y.left"), + axis.minor.ticks.y.right = el_def(element_line, "axis.ticks.y.right"), + axis.minor.ticks.theta = el_def(element_line, "axis.ticks.theta"), + axis.minor.ticks.r = el_def(element_line, "axis.ticks.r"), axis.minor.ticks.length = el_def(c("unit", "rel")), axis.minor.ticks.length.x = el_def(c("unit", "rel"), "axis.minor.ticks.length"), @@ -708,25 +826,25 @@ el_def <- function(class = NULL, inherit = NULL, description = NULL) { c("unit", "rel"), c("axis.minor.ticks.length.y", "axis.ticks.length.r") ), - legend.background = el_def("element_rect", "rect"), + legend.background = el_def(element_rect, "rect"), legend.margin = el_def(c("margin", "unit", "rel"), "margins"), legend.spacing = el_def(c("unit", "rel"), "spacing"), legend.spacing.x = el_def(c("unit", "rel"), "legend.spacing"), legend.spacing.y = el_def(c("unit", "rel"), "legend.spacing"), - legend.key = el_def("element_rect", "panel.background"), + legend.key = el_def(element_rect, "panel.background"), legend.key.height = el_def(c("unit", "rel"), "legend.key.size"), legend.key.width = el_def(c("unit", "rel"), "legend.key.size"), legend.key.spacing = el_def(c("unit", "rel"), "spacing"), legend.key.spacing.x = el_def(c("unit", "rel"), "legend.key.spacing"), legend.key.spacing.y = el_def(c("unit", "rel"), "legend.key.spacing"), legend.key.justification = el_def(c("character", "numeric", "integer")), - legend.frame = el_def("element_rect", "rect"), - legend.axis.line = el_def("element_line", "line"), - legend.ticks = el_def("element_line", "legend.axis.line"), + legend.frame = el_def(element_rect, "rect"), + legend.axis.line = el_def(element_line, "line"), + legend.ticks = el_def(element_line, "legend.axis.line"), legend.ticks.length = el_def(c("rel", "unit"), "legend.key.size"), - legend.text = el_def("element_text", "text"), + legend.text = el_def(element_text, "text"), legend.text.position = el_def("character"), - legend.title = el_def("element_text", "title"), + legend.title = el_def(element_text, "title"), legend.title.position = el_def("character"), legend.byrow = el_def("logical"), legend.position = el_def("character"), @@ -760,45 +878,45 @@ el_def <- function(class = NULL, inherit = NULL, description = NULL) { legend.box = el_def("character"), legend.box.just = el_def("character"), legend.box.margin = el_def(c("margin", "unit", "rel"), "margins"), - legend.box.background = el_def("element_rect", "rect"), + legend.box.background = el_def(element_rect, "rect"), legend.box.spacing = el_def(c("unit", "rel"), "spacing"), - panel.background = el_def("element_rect", "rect"), - panel.border = el_def("element_rect", "rect"), + panel.background = el_def(element_rect, "rect"), + panel.border = el_def(element_rect, "rect"), panel.spacing = el_def(c("unit", "rel"), "spacing"), panel.spacing.x = el_def(c("unit", "rel"), "panel.spacing"), panel.spacing.y = el_def(c("unit", "rel"), "panel.spacing"), - panel.grid.major.x = el_def("element_line", "panel.grid.major"), - panel.grid.major.y = el_def("element_line", "panel.grid.major"), - panel.grid.minor.x = el_def("element_line", "panel.grid.minor"), - panel.grid.minor.y = el_def("element_line", "panel.grid.minor"), + panel.grid.major.x = el_def(element_line, "panel.grid.major"), + panel.grid.major.y = el_def(element_line, "panel.grid.major"), + panel.grid.minor.x = el_def(element_line, "panel.grid.minor"), + panel.grid.minor.y = el_def(element_line, "panel.grid.minor"), panel.ontop = el_def("logical"), panel.widths = el_def("unit"), panel.heights = el_def("unit"), - strip.background = el_def("element_rect", "rect"), - strip.background.x = el_def("element_rect", "strip.background"), - strip.background.y = el_def("element_rect", "strip.background"), + strip.background = el_def(element_rect, "rect"), + strip.background.x = el_def(element_rect, "strip.background"), + strip.background.y = el_def(element_rect, "strip.background"), strip.clip = el_def("character"), - strip.text.x = el_def("element_text", "strip.text"), - strip.text.x.top = el_def("element_text", "strip.text.x"), - strip.text.x.bottom = el_def("element_text", "strip.text.x"), - strip.text.y = el_def("element_text", "strip.text"), - strip.text.y.left = el_def("element_text", "strip.text.y"), - strip.text.y.right = el_def("element_text", "strip.text.y"), + strip.text.x = el_def(element_text, "strip.text"), + strip.text.x.top = el_def(element_text, "strip.text.x"), + strip.text.x.bottom = el_def(element_text, "strip.text.x"), + strip.text.y = el_def(element_text, "strip.text"), + strip.text.y.left = el_def(element_text, "strip.text.y"), + strip.text.y.right = el_def(element_text, "strip.text.y"), strip.placement = el_def("character"), strip.placement.x = el_def("character", "strip.placement"), strip.placement.y = el_def("character", "strip.placement"), strip.switch.pad.grid = el_def(c("unit", "rel"), "spacing"), strip.switch.pad.wrap = el_def(c("unit", "rel"), "spacing"), - plot.background = el_def("element_rect", "rect"), - plot.title = el_def("element_text", "title"), + plot.background = el_def(element_rect, "rect"), + plot.title = el_def(element_text, "title"), plot.title.position = el_def("character"), - plot.subtitle = el_def("element_text", "text"), - plot.caption = el_def("element_text", "text"), + plot.subtitle = el_def(element_text, "text"), + plot.caption = el_def(element_text, "text"), plot.caption.position = el_def("character"), - plot.tag = el_def("element_text", "text"), + plot.tag = el_def(element_text, "text"), plot.tag.position = el_def(c("character", "numeric", "integer")), # Need to also accept numbers plot.tag.location = el_def("character"), plot.margin = el_def(c("margin", "unit", "rel"), "margins"), @@ -843,11 +961,35 @@ check_element <- function(el, elname, element_tree, call = caller_env()) { # NULL values for elements are OK if (is.null(el)) return() - if ("margin" %in% eldef$class) { - if (!is.unit(el) && length(el) == 4) - cli::cli_abort("The {.var {elname}} theme element must be a {.cls unit} vector of length 4.", call = call) - } else if (!inherits(el, eldef$class) && !inherits(el, "element_blank")) { - cli::cli_abort("The {.var {elname}} theme element must be a {.cls {eldef$class}} object.", call = call) + class <- eldef$class + if (inherits(class, "S7_class")) { + inherit_ok <- S7::S7_inherits(el, class) + } else { + inherit_ok <- inherits(el, class) + } + + if (is.character(class) && any(c("margin", "ggplot2::margin") %in% class)) { + if ("rel" %in% class && is.rel(el)) { + return() + } + if (is.unit(el) && length(el) == 4) { + return() + } + cli::cli_abort( + "The {.var {elname}} theme element must be a {.cls unit} vector of length 4", + call = call + ) + } + + # Maybe we should check that `class` is an element class before approving of + # blank elements? + if (inherit_ok || is_theme_element(el, "blank")) { + return() } - invisible() + + class_name <- if (inherits(class, "S7_class")) class@name else class + cli::cli_abort( + "The {.var {elname}} theme element must be a {.cls {class_name}} object.", + call = call + ) } diff --git a/R/theme.R b/R/theme.R index 471d030882..5f5121d4fd 100644 --- a/R/theme.R +++ b/R/theme.R @@ -217,7 +217,7 @@ #' @param validate `TRUE` to run `check_element()`, `FALSE` to bypass checks. #' @export #' @seealso -#' [+.gg()] and [%+replace%], +#' [add_gg()] and [%+replace%], #' [element_blank()], [element_line()], #' [element_rect()], and [element_text()] for #' details of the specific theme elements. @@ -475,8 +475,8 @@ theme <- function(..., # If complete theme set all non-blank elements to inherit from blanks if (complete) { elements <- lapply(elements, function(el) { - if (is_theme_element(el) && !is_theme_element(el, "blank")) { - el$inherit.blank <- TRUE + if (is_theme_element(el) && S7::prop_exists(el, "inherit.blank")) { + S7::prop(el, "inherit.blank") <- TRUE } el }) @@ -744,7 +744,7 @@ calc_element <- function(element, theme, verbose = FALSE, skip_blank = FALSE, # If result is element_blank, we skip it if `skip_blank` is `TRUE`, # and otherwise we don't inherit anything from parents - if (inherits(el_out, "element_blank")) { + if (is_theme_element(el_out, "blank")) { if (isTRUE(skip_blank)) { el_out <- NULL } else { @@ -758,9 +758,17 @@ calc_element <- function(element, theme, verbose = FALSE, skip_blank = FALSE, # If the element is defined (and not just inherited), check that # it is of the class specified in element_tree - if (!is.null(el_out) && - !inherits(el_out, element_tree[[element]]$class)) { - cli::cli_abort("Theme element {.var {element}} must have class {.cls {ggplot_global$element_tree[[element]]$class}}.", call = call) + if (!is.null(el_out)) { + class <- element_tree[[element]]$class + if (inherits(class, "S7_class")) { + if (!S7::S7_inherits(el_out, class)) { + cli::cli_abort("Theme element {.var {element}} must have class {.cls {class@name}}.", call = call) + } + } else { + if (!inherits(el_out, class)) { + cli::cli_abort("Theme element {.var {element}} must have class {.cls {ggplot_global$element_tree[[element]]$class}}.", call = call) + } + } } # Get the names of parents from the inheritance tree @@ -771,15 +779,23 @@ calc_element <- function(element, theme, verbose = FALSE, skip_blank = FALSE, if (verbose) cli::cli_inform("nothing (top level)") # Check that all the properties of this element are non-NULL - nullprops <- vapply(el_out, is.null, logical(1)) + if (is_theme_element(el_out)) { + nullprops <- lengths(S7::props(el_out)) == 0 + } else { + nullprops <- vapply(el_out, is.null, logical(1)) + } if (!any(nullprops)) { return(el_out) # no null properties, return element as is } # if we have null properties, try to fill in from ggplot_global$theme_default el_out <- combine_elements(el_out, ggplot_global$theme_default[[element]]) - nullprops <- vapply(el_out, is.null, logical(1)) - if (inherits(el_out, "element_geom")) { + if (is_theme_element(el_out)) { + nullprops <- lengths(S7::props(el_out)) == 0 + } else { + nullprops <- vapply(el_out, is.null, logical(1)) + } + if (is_theme_element(el_out, "geom")) { # Geom elements are expected to have NULL fill/colour, so allow these # to be missing nullprops[c("colour", "fill")] <- FALSE @@ -793,15 +809,19 @@ calc_element <- function(element, theme, verbose = FALSE, skip_blank = FALSE, # Calculate the parent objects' inheritance if (verbose) cli::cli_inform("{pnames}") + + # once we've started skipping blanks, we continue doing so until the end of the + # recursion; we initiate skipping blanks if we encounter an element that + # doesn't inherit blank. + skip_blank <- skip_blank || + (!is.null(el_out) && !isTRUE(try_prop(el_out, "inherit.blank"))) + parents <- lapply( pnames, calc_element, theme, verbose = verbose, - # once we've started skipping blanks, we continue doing so until the end of the - # recursion; we initiate skipping blanks if we encounter an element that - # doesn't inherit blank. - skip_blank = skip_blank || (!is.null(el_out) && !isTRUE(el_out$inherit.blank)), + skip_blank = skip_blank, call = call ) @@ -827,69 +847,63 @@ calc_element <- function(element, theme, verbose = FALSE, skip_blank = FALSE, #' # Adopt size but ignore colour #' merge_element(new, old) #' -merge_element <- function(new, old) { - UseMethod("merge_element") -} +merge_element <- S7::new_generic("merge_element", c("new", "old")) + +S7::method(merge_element, list(S7::class_any, S7::class_any)) <- + function(new, old, ...) { + if (is.null(old) || is_theme_element(old, "blank")) { + # If old is NULL or element_blank, then just return new + return(new) + } else if (is.null(new) || is.character(new) || is.numeric(new) || is.unit(new) || + is.logical(new) || is.function(new)) { + # If new is NULL, or a string, numeric vector, unit, or logical, just return it + return(new) + } -#' @rdname merge_element -#' @export -merge_element.default <- function(new, old) { - if (is.null(old) || inherits(old, "element_blank")) { - # If old is NULL or element_blank, then just return new - return(new) - } else if (is.null(new) || is.character(new) || is.numeric(new) || is.unit(new) || - is.logical(new) || is.function(new)) { - # If new is NULL, or a string, numeric vector, unit, or logical, just return it - return(new) + # otherwise we can't merge + cli::cli_abort("No method for merging {.cls {class(new)[1]}} into {.cls {class(old)[1]}}.") } - # otherwise we can't merge - cli::cli_abort("No method for merging {.cls {class(new)[1]}} into {.cls {class(old)[1]}}.") -} - -#' @rdname merge_element -#' @export -merge_element.element_blank <- function(new, old) { - # If new is element_blank, just return it - new -} - -#' @rdname merge_element -#' @export -merge_element.element <- function(new, old) { - if (is.null(old) || inherits(old, "element_blank")) { - # If old is NULL or element_blank, then just return new - return(new) +S7::method(merge_element, list(element_blank, S7::class_any)) <- + function(new, old, ...) { + # If new is element_blank, just return it + new } - # actual merging can only happen if classes match - if (!inherits(new, class(old)[1])) { - cli::cli_abort("Only elements of the same class can be merged.") - } +S7::method(merge_element, list(element, S7::class_any)) <- + function(new, old, ...) { + if (is.null(old) || is_theme_element(old, "blank")) { + # If old is NULL or element_blank, then just return new + return(new) + } - # Override NULL properties of new with the values in old - # Get logical vector of NULL properties in new - idx <- vapply(new, is.null, logical(1)) - # Get the names of TRUE items - idx <- names(idx[idx]) + # actual merging can only happen if classes match + if (!inherits(new, class(old)[1])) { + cli::cli_abort("Only elements of the same class can be merged.") + } + + # Override NULL properties of new with the values in old + # Get logical vector of NULL properties in new + idx <- lengths(S7::props(new)) == 0 + # Get the names of TRUE items + idx <- names(idx[idx]) - # Update non-NULL items - new[idx] <- old[idx] + # Update non-NULL items + S7::props(new)[idx] <- S7::props(old, idx) - new + new } -#' @rdname merge_element -#' @export -merge_element.margin <- function(new, old) { - if (is.null(old) || inherits(old, "element_blank")) { - return(new) - } - if (anyNA(new)) { - new[is.na(new)] <- old[is.na(new)] +S7::method(merge_element, list(margin, S7::class_any)) <- + function(new, old, ...) { + if (is.null(old) || is_theme_element(old, "blank")) { + return(new) + } + if (anyNA(new)) { + new[is.na(new)] <- old[is.na(new)] + } + new } - new -} #' Combine the properties of two elements #' @@ -901,7 +915,7 @@ merge_element.margin <- function(new, old) { combine_elements <- function(e1, e2) { # If e2 is NULL, nothing to inherit - if (is.null(e2) || inherits(e1, "element_blank")) { + if (is.null(e2) || is_theme_element(e1, "blank")) { return(e1) } @@ -924,7 +938,7 @@ combine_elements <- function(e1, e2) { return(e1) } - if (inherits(e1, "margin") && inherits(e2, "margin")) { + if (is_margin(e1) && is_margin(e2)) { if (anyNA(e2)) { e2[is.na(e2)] <- unit(0, "pt") } @@ -940,8 +954,8 @@ combine_elements <- function(e1, e2) { # If e2 is element_blank, and e1 inherits blank inherit everything from e2, # otherwise ignore e2 - if (inherits(e2, "element_blank")) { - if (e1$inherit.blank) { + if (is_theme_element(e2, "blank")) { + if (isTRUE(try_prop(e1, "inherit.blank"))) { return(e2) } else { return(e1) @@ -949,29 +963,29 @@ combine_elements <- function(e1, e2) { } # If e1 has any NULL properties, inherit them from e2 - n <- names(e1)[vapply(e1, is.null, logical(1))] - e1[n] <- e2[n] + n <- S7::prop_names(e1)[lengths(S7::props(e1)) == 0] + S7::props(e1)[n] <- S7::props(e2)[n] # Calculate relative sizes - if (is.rel(e1$size)) { - e1$size <- e2$size * unclass(e1$size) + if (is.rel(try_prop(e1, "size"))) { + e1@size <- e2@size * unclass(e1@size) } # Calculate relative linewidth - if (is.rel(e1$linewidth)) { - e1$linewidth <- e2$linewidth * unclass(e1$linewidth) + if (is.rel(try_prop(e1, "linewidth"))) { + e1@linewidth <- e2@linewidth * unclass(e1@linewidth) } - if (inherits(e1, "element_text")) { - e1$margin <- combine_elements(e1$margin, e2$margin) + if (is_theme_element(e1, "text")) { + e1@margin <- combine_elements(e1@margin, e2@margin) } # If e2 is 'richer' than e1, fill e2 with e1 parameters is_subclass <- !any(inherits(e2, class(e1), which = TRUE) == 0) is_subclass <- is_subclass && length(setdiff(class(e2), class(e1)) > 0) if (is_subclass) { - new <- defaults(e1, e2) - e2[names(new)] <- new + new <- defaults(S7::props(e1), S7::props(e2)) + S7::props(e2)[names(new)] <- new return(e2) } diff --git a/R/utilities.R b/R/utilities.R index 8e6f9d46e9..828c9ab711 100644 --- a/R/utilities.R +++ b/R/utilities.R @@ -909,3 +909,13 @@ compute_data_size <- function(data, size, default = 0.9, data[[target]] <- res * (default %||% 0.9) data } + +try_prop <- function(object, name, default = NULL) { + if (!S7::S7_inherits(object)) { + return(default) + } + if (!S7::prop_exists(object, name)) { + return(default) + } + S7::prop(object, name) +} diff --git a/R/zzz.R b/R/zzz.R index e15bcbd2af..9bfffde0a6 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -30,9 +30,11 @@ on_load( vars <- dplyr::vars } ) + on_load( if (getRversion() > "4.3.0") registerS3method("+", "gg", add_gg) ) + on_load(S7::methods_register()) .onLoad <- function(...) { run_on_load() diff --git a/man/element.Rd b/man/element.Rd index 84fe10fd20..92046f8267 100644 --- a/man/element.Rd +++ b/man/element.Rd @@ -1,6 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/theme-elements.R, R/margins.R -\name{element} +% Please edit documentation in R/margins.R, R/theme-elements.R +\name{margin} +\alias{margin} +\alias{margin_part} +\alias{margin_auto} +\alias{element} \alias{element_blank} \alias{element_rect} \alias{element_line} @@ -9,11 +13,16 @@ \alias{element_point} \alias{element_geom} \alias{rel} -\alias{margin} -\alias{margin_part} -\alias{margin_auto} \title{Theme elements} \usage{ +margin(t = 0, r = 0, b = 0, l = 0, unit = "pt") + +margin_part(t = NA, r = NA, b = NA, l = NA, unit = "pt") + +margin_auto(t = 0, r = t, b = t, l = r, unit = "pt") + +element() + element_blank() element_rect( @@ -22,6 +31,7 @@ element_rect( linewidth = NULL, linetype = NULL, color = NULL, + linejoin = NULL, inherit.blank = FALSE, size = deprecated() ) @@ -32,6 +42,7 @@ element_line( linetype = NULL, lineend = NULL, color = NULL, + linejoin = NULL, arrow = NULL, arrow.fill = NULL, inherit.blank = FALSE, @@ -59,6 +70,7 @@ element_polygon( linewidth = NULL, linetype = NULL, color = NULL, + linejoin = NULL, inherit.blank = FALSE ) @@ -90,14 +102,13 @@ element_geom( ) rel(x) - -margin(t = 0, r = 0, b = 0, l = 0, unit = "pt") - -margin_part(t = NA, r = NA, b = NA, l = NA, unit = "pt") - -margin_auto(t = 0, r = t, b = t, l = r, unit = "pt") } \arguments{ +\item{t, r, b, l}{Dimensions of each margin. (To remember order, think trouble).} + +\item{unit}{Default units of dimensions. Defaults to "pt" so it +can be most easily scaled with the text.} + \item{fill}{Fill colour. \code{fill_alpha()} can be used to set the transparency of the fill.} @@ -111,6 +122,8 @@ integer (0:8), a name (blank, solid, dashed, dotted, dotdash, longdash, twodash), or a string with an even number (up to eight) of hexadecimal digits which give the lengths in consecutive positions in the string.} +\item{linejoin}{Line join style, one of \code{"round"}, \code{"mitre"} or \code{"bevel"}.} + \item{inherit.blank}{Should this element inherit the existence of an \code{element_blank} among its parents? If \code{TRUE} the existence of a blank element among its parents will cause this element to be blank as @@ -119,7 +132,7 @@ calculating final element state.} \item{size, fontsize, pointsize}{text size in pts, point size in mm.} -\item{lineend}{Line end Line end style (round, butt, square)} +\item{lineend}{Line end style, one of \code{"round"}, \code{"butt"} or \code{"square"}.} \item{arrow}{Arrow specification, as created by \code{\link[grid:arrow]{grid::arrow()}}} @@ -154,11 +167,6 @@ is anchored.} \item{accent}{Accent colour.} \item{x}{A single number specifying size relative to parent element.} - -\item{t, r, b, l}{Dimensions of each margin. (To remember order, think trouble).} - -\item{unit}{Default units of dimensions. Defaults to "pt" so it -can be most easily scaled with the text.} } \value{ An S3 object of class \code{element}, \code{rel}, or \code{margin}. diff --git a/man/get_theme.Rd b/man/get_theme.Rd index d3283d0e67..d8198c4197 100644 --- a/man/get_theme.Rd +++ b/man/get_theme.Rd @@ -120,5 +120,5 @@ rep_el$text } \seealso{ -\code{\link[=+.gg]{+.gg()}} +\code{\link[=add_gg]{add_gg()}} } diff --git a/man/ggplot_add.Rd b/man/ggplot_add.Rd index af044e1748..d1133d26e2 100644 --- a/man/ggplot_add.Rd +++ b/man/ggplot_add.Rd @@ -16,7 +16,7 @@ A modified ggplot object } \description{ This generic allows you to add your own methods for adding custom objects to -a ggplot with \link{+.gg}. +a ggplot with \link[=add_gg]{+.gg}. } \details{ Custom methods for \code{ggplot_add()} are intended to update the \code{plot} variable @@ -29,7 +29,9 @@ exposed at this point, which comes with the responsibility of returning the plot intact. } \examples{ -S7::method(ggplot_add, list(S7::new_S3_class("element_text"), class_ggplot)) <- +# making a new method for the generic +# in this example, we enable adding text elements +S7::method(ggplot_add, list(element_text, class_ggplot)) <- function(object, plot, ...) { plot + theme(text = object) } @@ -40,6 +42,5 @@ ggplot(mpg, aes(displ, cty)) + element_text(colour = "red") # clean-up -rm("element_text", envir = ggplot_add@methods) } \keyword{internal} diff --git a/man/is_tests.Rd b/man/is_tests.Rd index bb0b25e799..c3c0630915 100644 --- a/man/is_tests.Rd +++ b/man/is_tests.Rd @@ -1,7 +1,7 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/ggproto.R, R/aes.R, R/geom-.R, R/coord-.R, -% R/facet-.R, R/stat-.R, R/theme-elements.R, R/guide-.R, R/layer.R, -% R/guides-.R, R/margins.R, R/plot.R, R/position-.R, R/scale-.R, R/theme.R +% R/facet-.R, R/stat-.R, R/margins.R, R/theme-elements.R, R/guide-.R, +% R/layer.R, R/guides-.R, R/plot.R, R/position-.R, R/scale-.R, R/theme.R \name{is_ggproto} \alias{is_ggproto} \alias{is.ggproto} @@ -12,11 +12,11 @@ \alias{is_facet} \alias{is.facet} \alias{is_stat} +\alias{is_margin} \alias{is_theme_element} \alias{is_guide} \alias{is_layer} \alias{is_guides} -\alias{is_margin} \alias{is_tests} \alias{is_ggplot} \alias{is.ggplot} @@ -44,6 +44,8 @@ is.facet(x) # Deprecated is_stat(x) +is_margin(x) + is_theme_element(x, type = "any") is_guide(x) @@ -52,8 +54,6 @@ is_layer(x) is_guides(x) -is_margin(x) - is_ggplot(x) is.ggplot(x) # Deprecated @@ -70,7 +70,7 @@ is.theme(x) # Deprecated \item{x}{An object to test} \item{type}{For testing elements: the type of element to expect. One of -\code{"blank"}, \code{"rect"}, \code{"line"} or \code{"text"}.} +\code{"blank"}, \code{"rect"}, \code{"line"}, \code{"text"}, \code{"polygon"}, \code{"point"} or \code{"geom"}.} } \description{ Reports wether \code{x} is a type of object diff --git a/man/merge_element.Rd b/man/merge_element.Rd index ca993eeec3..3060360dc0 100644 --- a/man/merge_element.Rd +++ b/man/merge_element.Rd @@ -2,21 +2,9 @@ % Please edit documentation in R/theme.R \name{merge_element} \alias{merge_element} -\alias{merge_element.default} -\alias{merge_element.element_blank} -\alias{merge_element.element} -\alias{merge_element.margin} \title{Merge a parent element into a child element} \usage{ -merge_element(new, old) - -\method{merge_element}{default}(new, old) - -\method{merge_element}{element_blank}(new, old) - -\method{merge_element}{element}(new, old) - -\method{merge_element}{margin}(new, old) +merge_element(new, old, ...) } \arguments{ \item{new}{The child element in the theme hierarchy} diff --git a/man/register_theme_elements.Rd b/man/register_theme_elements.Rd index cdbbb25d70..0cb822e686 100644 --- a/man/register_theme_elements.Rd +++ b/man/register_theme_elements.Rd @@ -27,8 +27,8 @@ a list of named element definitions created with el_def().} \item{reset_current}{If \code{TRUE} (the default), the currently active theme is reset to the default theme.} -\item{class}{The name of the element class. Examples are "element_line" or -"element_text" or "unit", or one of the two reserved keywords "character" or +\item{class}{The name of the element class. Examples are \code{element_line} or +\code{element_text} or "unit", or one of the two reserved keywords "character" or "margin". The reserved keyword "character" implies a character or numeric vector, not a class called "character". The keyword "margin" implies a unit vector of length 4, as created by \code{\link[=margin]{margin()}}.} @@ -76,7 +76,7 @@ element inheritance relationships for the element tree. # plot panels. To do so, it registers a new theme element `ggxyz.panel.annotation` register_theme_elements( ggxyz.panel.annotation = element_text(color = "blue", hjust = 0.95, vjust = 0.05), - element_tree = list(ggxyz.panel.annotation = el_def("element_text", "text")) + element_tree = list(ggxyz.panel.annotation = el_def(element_text, "text")) ) # Now the package can define a new coord that includes a panel annotation diff --git a/man/theme.Rd b/man/theme.Rd index 2766a3f8ca..f2a9bdf33f 100644 --- a/man/theme.Rd +++ b/man/theme.Rd @@ -519,7 +519,7 @@ p3 + theme(panel.spacing = unit(1, "lines")) } } \seealso{ -\code{\link[=+.gg]{+.gg()}} and \link{\%+replace\%}, +\code{\link[=add_gg]{add_gg()}} and \link{\%+replace\%}, \code{\link[=element_blank]{element_blank()}}, \code{\link[=element_line]{element_line()}}, \code{\link[=element_rect]{element_rect()}}, and \code{\link[=element_text]{element_text()}} for details of the specific theme elements. diff --git a/tests/testthat/_snaps/theme.md b/tests/testthat/_snaps/theme.md index 3694f73097..bd92d95024 100644 --- a/tests/testthat/_snaps/theme.md +++ b/tests/testthat/_snaps/theme.md @@ -9,7 +9,7 @@ # incorrect theme specifications throw meaningful errors Can't merge the `line` theme element. - Caused by error in `merge_element()`: + Caused by error in `method(merge_element, list(ggplot2::element, class_any))`: ! Only elements of the same class can be merged. --- @@ -18,7 +18,7 @@ --- - Theme element `test` has `NULL` property without default: fill, colour, linewidth, and linetype. + Theme element `test` has `NULL` property without default: fill, colour, linewidth, linetype, and linejoin. --- @@ -58,7 +58,7 @@ Code merge_element(text_base, rect_base) Condition - Error in `merge_element()`: + Error in `method(merge_element, list(ggplot2::element, class_any))`: ! Only elements of the same class can be merged. # Theme elements are checked during build diff --git a/tests/testthat/test-theme.R b/tests/testthat/test-theme.R index 7267fb48dc..5872e9f14a 100644 --- a/tests/testthat/test-theme.R +++ b/tests/testthat/test-theme.R @@ -21,8 +21,8 @@ test_that("modifying theme element properties with + operator works", { # Make sure the theme class didn't change or get dropped expect_s7_class(t, class_theme) # Make sure the element class didn't change or get dropped - expect_s3_class(t$axis.title.x, "element") - expect_s3_class(t$axis.title.x, "element_text") + expect_s7_class(t$axis.title.x, element) + expect_s7_class(t$axis.title.x, element_text) # Modifying an intermediate node works t <- theme_grey() + theme(axis.title = element_text(colour = 'red')) @@ -30,10 +30,10 @@ test_that("modifying theme element properties with + operator works", { # Modifying a root node changes only the specified properties t <- theme_grey() + theme(text = element_text(colour = 'red')) - expect_identical(t$text$colour, 'red') - expect_identical(t$text$family, theme_grey()$text$family) - expect_identical(t$text$face, theme_grey()$text$face) - expect_identical(t$text$size, theme_grey()$text$size) + expect_identical(t$text@colour, 'red') + expect_identical(t$text@family, theme_grey()$text@family) + expect_identical(t$text@face, theme_grey()$text@face) + expect_identical(t$text@size, theme_grey()$text@size) # Descendent is unchanged expect_identical(t$axis.title.x, theme_grey()$axis.title.x) @@ -60,34 +60,34 @@ test_that("adding theme object to ggplot object with + operator works", { ## test with complete theme p <- ggplot(data.frame(x = 1:3), aes(x, x)) + geom_point() + theme_grey() p <- p + theme(axis.title = element_text(size = 20)) - expect_true(p@theme$axis.title$size == 20) + expect_true(p@theme$axis.title@size == 20) # Should update specified properties, but not reset other properties p <- p + theme(text = element_text(colour = 'red')) - expect_true(p@theme$text$colour == 'red') + expect_true(p@theme$text@colour == 'red') tt <- theme_grey()$text - tt$colour <- 'red' - expect_true(tt$inherit.blank) - tt$inherit.blank <- FALSE + tt@colour <- 'red' + expect_true(tt@inherit.blank) + tt@inherit.blank <- FALSE expect_identical(p@theme$text, tt) ## test without complete theme p <- ggplot(data.frame(x = 1:3), aes(x, x)) + geom_point() p <- p + theme(axis.title = element_text(size = 20)) - expect_true(p@theme$axis.title$size == 20) + expect_true(p@theme$axis.title@size == 20) # Should update specified properties, but not reset other properties p <- p + theme(text = element_text(colour = 'red')) - expect_true(p@theme$text$colour == 'red') - expect_null(p@theme$text$family) - expect_null(p@theme$text$face) - expect_null(p@theme$text$size) - expect_null(p@theme$text$hjust) - expect_null(p@theme$text$vjust) - expect_null(p@theme$text$angle) - expect_null(p@theme$text$lineheight) - expect_null(p@theme$text$margin) - expect_null(p@theme$text$debug) + expect_true(p@theme$text@colour == 'red') + expect_null(p@theme$text@family) + expect_null(p@theme$text@face) + expect_null(p@theme$text@size) + expect_null(p@theme$text@hjust) + expect_null(p@theme$text@vjust) + expect_null(p@theme$text@angle) + expect_null(p@theme$text@lineheight) + expect_null(p@theme$text@margin) + expect_null(p@theme$text@debug) ## stepwise addition of partial themes is identical to one-step addition p <- ggplot(data.frame(x = 1:3), aes(x, x)) + geom_point() @@ -125,36 +125,34 @@ test_that("calculating theme element inheritance works", { # Check that properties are passed along from axis.title to axis.title.x e <- calc_element('axis.title.x', t) - expect_identical(e$colour, 'red') - expect_false(is.null(e$family)) - expect_false(is.null(e$face)) - expect_false(is.null(e$size)) + expect_identical(e@colour, 'red') + expect_false(is.null(e@family)) + expect_false(is.null(e@face)) + expect_false(is.null(e@size)) # Check that rel() works for relative sizing, and is applied at each level t <- theme_grey(base_size = 12) + theme(axis.title = element_text(size = rel(0.5))) + theme(axis.title.x = element_text(size = rel(0.5))) e <- calc_element('axis.title', t) - expect_identical(e$size, 6) + expect_identical(e@size, 6) ex <- calc_element('axis.title.x', t) - expect_identical(ex$size, 3) + expect_identical(ex@size, 3) # Check that a theme_blank in a parent node gets passed along to children t <- theme_grey() + theme(text = element_blank()) expect_identical(calc_element('axis.title.x', t), element_blank()) # Check that inheritance from derived class works - element_dummyrect <- function(dummy) { # like element_rect but w/ dummy argument - structure(list( - fill = NULL, colour = NULL, dummy = dummy, linewidth = NULL, - linetype = NULL, inherit.blank = FALSE - ), class = c("element_dummyrect", "element_rect", "element")) - } + element_dummyrect <- S7::new_class( + "element_dummyrect", parent = element_rect, + properties = c(element_rect@properties, list(dummy = S7::class_any)) + ) e <- calc_element( "panel.background", theme( - rect = element_rect(fill = "white", colour = "black", linewidth = 0.5, linetype = 1), + rect = element_rect(fill = "white", colour = "black", linewidth = 0.5, linetype = 1, linejoin = "round"), panel.background = element_dummyrect(dummy = 5), complete = TRUE # need to prevent pulling in default theme ) @@ -162,10 +160,10 @@ test_that("calculating theme element inheritance works", { expect_identical( e, - structure(list( - fill = "white", colour = "black", dummy = 5, linewidth = 0.5, linetype = 1, + element_dummyrect( + fill = "white", colour = "black", dummy = 5, linewidth = 0.5, linetype = 1, linejoin = "round", inherit.blank = TRUE # this is true because we're requesting a complete theme - ), class = c("element_dummyrect", "element_rect", "element")) + ) ) # Check that blank elements are skipped in inheritance tree if and only if elements @@ -177,7 +175,7 @@ test_that("calculating theme element inheritance works", { ) e1 <- calc_element("strip.text.x", t) e2 <- calc_element("text", t) - e2$inherit.blank <- FALSE # b/c inherit.blank = TRUE for complete themes + e2@inherit.blank <- FALSE # b/c inherit.blank = TRUE for complete themes expect_identical(e1, e2) theme <- theme_gray() + @@ -203,18 +201,18 @@ test_that("complete and non-complete themes interact correctly with each other", # But for _element properties_, the one on the right modifies the one on the left. t <- theme_bw() + theme(text = element_text(colour = 'red')) expect_true(attr(t, "complete")) - expect_equal(t$text$colour, 'red') + expect_equal(t$text@colour, 'red') # A complete theme object (like theme_bw) always trumps a non-complete theme object t <- theme(text = element_text(colour = 'red')) + theme_bw() expect_true(attr(t, "complete")) - expect_equal(t$text$colour, theme_bw()$text$colour) + expect_equal(t$text@colour, theme_bw()$text@colour) # Adding two non-complete themes: the one on the right modifies the one on the left. t <- theme(text = element_text(colour = 'blue')) + theme(text = element_text(colour = 'red')) expect_false(attr(t, "complete")) - expect_equal(t$text$colour, 'red') + expect_equal(t$text@colour, 'red') }) test_that("complete and non-complete themes interact correctly with ggplot objects", { @@ -242,14 +240,14 @@ test_that("complete and non-complete themes interact correctly with ggplot objec expect_identical(pt, tt) p <- ggplot_build(base + theme(text = element_text(colour = 'red', face = 'italic'))) - expect_equal(p@plot@theme$text$colour, "red") - expect_equal(p@plot@theme$text$face, "italic") + expect_equal(p@plot@theme$text@colour, "red") + expect_equal(p@plot@theme$text@face, "italic") p <- ggplot_build(base + theme(text = element_text(colour = 'red')) + theme(text = element_text(face = 'italic'))) - expect_equal(p@plot@theme$text$colour, "red") - expect_equal(p@plot@theme$text$face, "italic") + expect_equal(p@plot@theme$text@colour, "red") + expect_equal(p@plot@theme$text@face, "italic") }) test_that("theme(validate=FALSE) means do not check_element", { @@ -285,9 +283,10 @@ test_that("theme validation happens at build stage", { test_that("incorrect theme specifications throw meaningful errors", { expect_snapshot_error(add_theme(theme_grey(), theme(line = element_rect()))) expect_snapshot_error(calc_element("line", theme(line = element_rect()))) - register_theme_elements(element_tree = list(test = el_def("element_rect"))) + register_theme_elements(element_tree = list(test = el_def(element_rect))) expect_snapshot_error(calc_element("test", theme_gray() + theme(test = element_rect()))) expect_snapshot_error(set_theme("foo")) + reset_theme_settings() }) test_that("element tree can be modified", { @@ -307,7 +306,7 @@ test_that("element tree can be modified", { # things work once we add a new element to the element tree register_theme_elements( - element_tree = list(blablabla = el_def("element_text", "text")) + element_tree = list(blablabla = el_def(element_text, "text")) ) expect_silent(ggplotGrob(p)) @@ -329,31 +328,29 @@ test_that("element tree can be modified", { final_theme <- ggplot2:::plot_theme(p, theme_gray()) e1 <- calc_element("blablabla", final_theme) e2 <- calc_element("text", final_theme) - expect_identical(e1$family, e2$family) - expect_identical(e1$face, e2$face) - expect_identical(e1$size, e2$size) - expect_identical(e1$lineheight, e2$lineheight) - expect_identical(e1$colour, "red") # not inherited from element_text + expect_identical(e1@family, e2@family) + expect_identical(e1@face, e2@face) + expect_identical(e1@size, e2@size) + expect_identical(e1@lineheight, e2@lineheight) + expect_identical(e1@colour, "red") # not inherited from element_text # existing elements can be overwritten - ed <- el_def("element_rect", "rect") + ed <- el_def(element_rect, "rect") register_theme_elements( element_tree = list(axis.title = ed) ) expect_identical(get_element_tree()$axis.title, ed) - reset_theme_settings(reset_current = FALSE) # revert back to defaults + reset_theme_settings() # revert back to defaults }) test_that("all elements in complete themes have inherit.blank=TRUE", { inherit_blanks <- function(theme) { - all(vapply(theme, function(el) { - if (is_theme_element(el) && !is_theme_element(el, "blank")) { - el$inherit.blank - } else { - TRUE - } - }, logical(1))) + all(vapply( + theme, try_prop, + name = "inherit.blank", default = TRUE, + logical(1) + )) } expect_true(inherit_blanks(theme_grey())) expect_true(inherit_blanks(theme_bw())) @@ -395,7 +392,7 @@ test_that("complete plot themes shouldn't inherit from default", { base <- ggplot(data.frame(x = 1), aes(x, x)) + geom_point() ptheme <- plot_theme(base + theme(axis.text.x = element_text(colour = "blue")), default_theme) - expect_equal(ptheme$axis.text.x$colour, "blue") + expect_equal(ptheme$axis.text.x@colour, "blue") ptheme <- plot_theme(base + theme_void(), default_theme) expect_null(ptheme$axis.text.x) @@ -425,14 +422,14 @@ test_that("current theme can be updated with new elements", { # element tree gets merged properly register_theme_elements( abcde = element_text(color = "blue", hjust = 0, vjust = 1), - element_tree = list(abcde = el_def("element_text", "text")) + element_tree = list(abcde = el_def(element_text, "text")) ) e1 <- calc_element("abcde", plot_theme(b2)) e2 <- calc_element("text", plot_theme(b2)) - e2$colour <- "blue" - e2$hjust <- 0 - e2$vjust <- 1 + e2@colour <- "blue" + e2@hjust <- 0 + e2@vjust <- 1 expect_identical(e1, e2) reset_theme_settings() @@ -567,7 +564,7 @@ test_that("Element subclasses are inherited", { test <- combine_elements(poor, rich) expect_s3_class(test, "element_rich") expect_equal( - test[c("colour", "linetype", "linewidth")], + S7::props(test)[c("colour", "linetype", "linewidth")], list(colour = "red", linetype = 3, linewidth = 2) ) @@ -575,7 +572,7 @@ test_that("Element subclasses are inherited", { test <- combine_elements(rich, poor) expect_s3_class(test, "element_rich") expect_equal( - test[c("colour", "linetype", "linewidth")], + S7::props(test)[c("colour", "linetype", "linewidth")], list(colour = "red", linetype = 2, linewidth = 2) ) @@ -587,7 +584,7 @@ test_that("Element subclasses are inherited", { test <- combine_elements(sibling, rich) expect_s3_class(test, "element_sibling") expect_equal( - test[c("colour", "linetype", "linewidth")], + S7::props(test)[c("colour", "linetype", "linewidth")], list(colour = "red", linetype = 3, linewidth = 2) ) @@ -595,7 +592,7 @@ test_that("Element subclasses are inherited", { test <- combine_elements(rich, sibling) expect_s3_class(test, "element_rich") expect_equal( - test[c("colour", "linetype", "linewidth")], + S7::props(test)[c("colour", "linetype", "linewidth")], list(colour = "red", linetype = 2, linewidth = 2) ) }) @@ -622,10 +619,10 @@ test_that("header_family is passed on correctly", { td <- theme_dark(base_family = "x", header_family = "y") test <- calc_element("plot.title", td) - expect_equal(test$family, "y") + expect_equal(test@family, "y") test <- calc_element("plot.subtitle", td) - expect_equal(test$family, "x") + expect_equal(test@family, "x") }) test_that("complete_theme completes a theme", { @@ -636,19 +633,19 @@ test_that("complete_theme completes a theme", { # Elements are propagated new <- complete_theme(theme(axis.line = element_line("red")), gray) - expect_equal(new$axis.line$colour, "red") + expect_equal(new$axis.line@colour, "red") # Missing elements are filled in if default theme is incomplete new <- complete_theme(default = theme()) - expect_s3_class(new$axis.line, "element_blank") + expect_s3_class(new$axis.line, "ggplot2::element_blank") # Registered elements are included register_theme_elements( test = element_text(), - element_tree = list(test = el_def("element_text", "text")) + element_tree = list(test = el_def(element_text, "text")) ) new <- complete_theme(default = gray) - expect_s3_class(new$test, "element_text") + expect_s3_class(new$test, "ggplot2::element_text") reset_theme_settings() }) @@ -985,15 +982,11 @@ test_that("Legends can on all sides of the plot with custom justification", { }) test_that("Strips can render custom elements", { - element_test <- function(...) { - el <- element_text(...) - class(el) <- c("element_test", "element_text", "element") - el - } - element_grob.element_test <- function(element, label = "", x = NULL, y = NULL, ...) { - rectGrob(width = unit(1, "cm"), height = unit(1, "cm")) - } - registerS3method("element_grob", "element_test", element_grob.element_test) + element_test <- S7::new_class("element_test", element_text) + S7::method(element_grob, element_test) <- + function(element, label = "", x = NULL, y = NULL, ...) { + rectGrob(width = unit(1, "cm"), height = unit(1, "cm")) + } df <- data_frame(x = 1:3, y = 1:3, a = letters[1:3]) plot <- ggplot(df, aes(x, y)) + diff --git a/vignettes/articles/faq-axes.Rmd b/vignettes/articles/faq-axes.Rmd index cf88240cfa..1f08b48dce 100644 --- a/vignettes/articles/faq-axes.Rmd +++ b/vignettes/articles/faq-axes.Rmd @@ -444,6 +444,8 @@ ggplot(mpg, aes(x = cty^2, y = log(hwy, base = 10))) + ) ``` +