From 3f76efd5a2a0677a41b3bc7ec8c16179cc9786ea Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Fri, 13 Dec 2024 19:55:42 +0100 Subject: [PATCH] Add new `guide_circles()` (#35) * first draft * Use margin around glyphs * add tests * document * quick note --- NAMESPACE | 2 + NEWS.md | 5 +- R/gizmo-circles.R | 393 ++++++++++++++++++ R/themes.R | 6 + man/guide_axis_base.Rd | 1 + man/guide_axis_dendro.Rd | 1 + man/guide_axis_nested.Rd | 1 + man/guide_circles.Rd | 124 ++++++ man/guide_colbar.Rd | 1 + man/guide_colring.Rd | 1 + man/guide_colsteps.Rd | 1 + man/guide_legend_base.Rd | 1 + man/guide_legend_cross.Rd | 1 + man/guide_legend_group.Rd | 1 + man/legendry_extensions.Rd | 13 +- man/theme_guide.Rd | 4 + .../guide-circles-text-locations.svg | 198 +++++++++ .../guide-circles-text-placement.svg | 346 +++++++++++++++ tests/testthat/test-gizmo-circles.R | 66 +++ 19 files changed, 1159 insertions(+), 7 deletions(-) create mode 100644 R/gizmo-circles.R create mode 100644 man/guide_circles.Rd create mode 100644 tests/testthat/_snaps/gizmo-circles/guide-circles-text-locations.svg create mode 100644 tests/testthat/_snaps/gizmo-circles/guide-circles-text-placement.svg create mode 100644 tests/testthat/test-gizmo-circles.R diff --git a/NAMESPACE b/NAMESPACE index 3770794..74d1573 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,6 +10,7 @@ export(GizmoDensity) export(GizmoGrob) export(GizmoHistogram) export(GizmoStepcap) +export(GuideCircles) export(GuideColring) export(GuideLegendBase) export(GuideLegendGroup) @@ -45,6 +46,7 @@ export(gizmo_stepcap) export(guide_axis_base) export(guide_axis_dendro) export(guide_axis_nested) +export(guide_circles) export(guide_colbar) export(guide_colring) export(guide_colsteps) diff --git a/NEWS.md b/NEWS.md index 196b18e..fd70615 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,9 +6,12 @@ * New primitive guide function: `primitive_segments()` * New key functions: `key_segment_manual()`, `key_segment_map()` and `key_dendro()`. + +* Added new standalone guide `guide_circles()` (#14). + * New supporting theme element `legendry.legend.key.margin`. * Fixed bug where `guide_axis_nested(key = key_range_auto(...))` produced - duplicated labels (#31) + duplicated labels (#31). # legendry 0.1.0 diff --git a/R/gizmo-circles.R b/R/gizmo-circles.R new file mode 100644 index 0000000..c6c7813 --- /dev/null +++ b/R/gizmo-circles.R @@ -0,0 +1,393 @@ +# Constructor ------------------------------------------------------------- + +#' Circle size guide +#' +#' This guide displays the sizes of points as a series of circles. It is +#' typically paired with [`geom_point()`][ggplot2::geom_point] with +#' [`draw_key_point()`][ggplot2::draw_key_point] glyphs. +#' +#' @param key A [standard key][key_standard] specification. Defaults to +#' [`key_auto()`]. See more information in the linked topic. +#' @param hjust,vjust A `` between 0 and 1 giving the horizontal +#' and vertical justification, respectively, of the central shapes. It is +#' recommended `hjust = 0.5` when text is placed on the left or right and +#' `vjust = 0.5` is recommended when text is placed on top or in the bottom. +#' @param text_position A string, one of `"ontop"`, `"top"`, `"right"`, +#' `"bottom"`, or `"left"` do describe the placement of labels. The +#' default (`NULL`), will take the `legend.text.position` theme setting. +#' @param clip_text A `` whether to give text in the `"ontop"` +#' position a small rectangle of background colour. +#' @inheritParams common_parameters +#' +#' @return A `` object. +#' @export +#' @family standalone guides +#' @details +#' Please note that the default size scales scale to area, not radius, so +#' equidistant breaks will appear at irregularly spaced positions due to +#' labelling the diameter of a circle. +#' +#' This graph was designed with standard round [shapes][ggplot2::scale_shape] +#' in mind, i.e. shapes 1, 16, 19 and 21. For that reason, `shape = 1` is the +#' default `override.aes` argument. Other shapes will probably be drawn but the +#' quality of their alignment and label placement may be unsatisfactory. +#' +#' @examples +#' # A standard plot +#' p <- ggplot(mtcars, aes(disp, mpg)) + +#' geom_point(aes(size = hp), alpha = 0.3) +#' +#' # By default, the sizes aren't large enough to make this guide clear +#' p + scale_size_area(guide = "circles") +#' +#' # Update with a more approrpriate scale +#' p <- p + +#' scale_size_area( +#' max_size = 30, +#' limits = c(0, NA), +#' breaks = c(0, 25, 100, 250) +#' ) +#' p + guides(size = "circles") +#' +#' # Horizontal orientation +#' p + guides(size = guide_circles( +#' vjust = 0.5, hjust = 0, text_position = "bottom" +#' )) +#' +#' # Alternative text placement +#' p + guides(size = guide_circles( +#' text_position = "ontop", +#' clip_text = TRUE +#' )) +#' +#' # More styling options +#' p + guides(size = guide_circles(override.aes = aes(colour = "red")))+ +#' theme( +#' # Key background +#' legend.key = element_rect(colour = "black", fill = 'white'), +#' # Padding around central shapes +#' legendry.legend.key.margin = margin(1, 1, 1, 1, "cm"), +#' legend.ticks = element_line(colour = "blue"), +#' legend.text.position = "left" +#' ) +guide_circles <- function( + key = NULL, + title = waiver(), + theme = NULL, + hjust = 0.5, + vjust = 0, + text_position = NULL, + clip_text = FALSE, + override.aes = list(shape = 1), + position = waiver(), + direction = NULL +) { + + check_number_decimal(hjust, min = 0, max = 1, allow_infinite = FALSE) + check_number_decimal(vjust, min = 0, max = 1, allow_infinite = FALSE) + check_bool(clip_text) + check_argmatch(text_position, c(.trbl, "ontop"), allow_null = TRUE) + + new_guide( + key = key, + title = title, + theme = theme, + hjust = hjust, + vjust = vjust, + text_position = text_position, + clip_text = clip_text, + override.aes = override.aes, + position = position, + direction = direction, + super = GuideCircles + ) +} + +# Class ------------------------------------------------------------------- + +#' @export +#' @rdname legendry_extensions +#' @format NULL +#' @usage NULL +GuideCircles <- ggproto( + "GuideCircles", Guide, + + params = new_params(key = "auto", hjust = 0.5, vjust = 0, clip_text = TRUE, + text_position = "ontop", override.aes = list()), + + elements = list( + background = "legend.background", + margin = "legend.margin", + key = "legend.key", + text = "legend.text", + padding = "legendry.legend.key.margin", + ticks = "legend.ticks", + title = "legend.title", + title_position = "legend.title.position" + ), + + extract_params = function(scale, params, title = waiver(), ...) { + params$title <- scale$make_title(params$title %|W|% scale$name %|W|% title) + params$position <- params$position %|W|% NULL + params$limits <- scale$get_limits() + params + }, + + get_layer_key = GuideLegend$get_layer_key, + + build_decor = function(decor, grobs, elements, params) { + + key <- params$key + + glyphs <- lapply( + decor, draw_circles, + hjust = params$hjust, + vjust = params$vjust + ) + + x <- glyphs[[1]]$xpos + y <- glyphs[[1]]$ypos + + key_bg <- element_grob(elements$key) + padding <- cm(elements$padding) + width <- width_cm(glyphs[[1]]$width) + height <- height_cm(glyphs[[1]]$height) + width <- unit(c(padding[4], width, padding[2]), "cm") + height <- unit(c(padding[1], height, padding[3]), "cm") + + position <- params$text_position %||% elements$text_position + if (position == "ontop") { + text <- Map( + element_grob, label = key$.label, x = x, y = y, + MoreArgs = list(element = elements$text) + ) + if (isTRUE(params$clip_text)) { + glyphs <- censor_text_background( + x, y, text, glyphs, + vjust = elements$text$vjust, colour = elements$key$fill + ) + } + grob <- gTree(children = inject(gList(!!!glyphs, !!!text))) + } else { + ticks <- draw_circle_ticks(elements$ticks, x, y, padding, position) + + x <- switch(position, left = , right = NULL, x) + y <- switch(position, top = , bottom = NULL, y) + + text <- element_grob( + elements$text, x = x, y = y, label = key$.label, + margin_x = position %in% c("left", "right"), + margin_y = position %in% c("top", "bottom") + ) + grob <- gTree(children = inject(gList(ticks, !!!glyphs))) + } + + gt <- gtable(widths = width, heights = height) + gt <- gtable_add_grob( + gt, key_bg, clip = "off", name = "key-bg", + t = 1, l = 1, b = 3, r = 3 + ) + gt <- gtable_add_grob(gt, grob, t = 2, l = 2, clip = "off", name = "circles") + if (position == "ontop") { + return(gt) + } + + gt <- switch( + position, + left = gtable_add_cols(gt, unit(width_cm(text), "cm"), 0), + right = gtable_add_cols(gt, unit(width_cm(text), "cm"), -1), + bottom = gtable_add_rows(gt, unit(height_cm(text), "cm"), -1), + top = gtable_add_rows(gt, unit(height_cm(text), "cm"), 0) + ) + gt <- gtable_add_grob( + gt, text, clip = "off", name = "labels", + t = switch(position, top = 1, bottom = 4, 2), + l = switch(position, left = 1, right = 4, 2) + ) + gt + }, + + setup_elements = function(params, elements, theme) { + theme <- theme + params$theme + theme$legend.axis.line <- theme$legend.axis.line %||% calc_element("panel.grid", theme) + + # Setup title + title_position <- calc_element("legend.title.position", theme) %||% + switch(params$direction, horizontal = "left", vertical = "top") + elements$title <- setup_legend_title(theme, title_position, params$direction) + + text_position <- params$text_position %||% + calc_element("legend.text.position", theme) %||% "right" + if (text_position != "ontop") { + elements$text <- setup_legend_text(theme, text_position, params$direction) + } + + is_char <- is_each(elements, is.character) + elements[is_char] <- lapply(elements[is_char], calc_element, theme = theme) + elements$title_position <- title_position + elements$text_position <- text_position + elements + }, + + draw = function(self, theme, position = NULL, direction = NULL, + params = self$params) { + + params <- replace_null(params, position = position, direction = direction) + + elems <- self$setup_elements(params, self$elements, theme) + + title <- self$build_title(params$title, elems, params) + grob <- self$build_decor(params$decor, NULL, elems, params) + grobs <- list(circles = grob, title = title) + + self$assemble_drawing(grobs, params = params, elements = elems) + }, + + assemble_drawing = function(self, grobs, layout = NULL, sizes = NULL, + params = list(), elements = list()) { + + # Add title + gt <- self$add_title( + grobs$circles, grobs$title, elements$title_position, + with(elements$title, rotate_just(angle, hjust, vjust)) + ) + + # Add padding + padding <- rep(0, 4) + padding[c(1, 3)] <- height_cm(elements$margin[c(1, 3)]) + padding[c(2, 4)] <- width_cm(elements$margin[c(2, 4)]) + gt <- gtable_add_padding(gt, unit(padding, "cm")) + + # Add background + background <- element_grob(elements$background) + if (!is.zero(background)) { + gt <- gtable_add_grob( + gt, background, name = "background", clip = "off", + t = 1, r = -1, b = -1, l = 1, z = -Inf + ) + } + gt + } +) + + +# Helpers ----------------------------------------------------------------- + +censor_text_background <- function(x, y, text, background, + vjust = 0.5, padding = 0.1, + colour = "white") { + + width <- width_cm(text) + padding + height <- height_cm(text) + padding + + mask <- rectGrob( + x = x, y = y - unit((vjust - 0.5) * height, "cm"), + width = unit(width, "cm"), + height = unit(height, "cm"), + gp = gpar(fill = colour, col = NA, lty = 0), + vp = viewport(clip = "on") + ) + + c(background, list(mask)) +} + +draw_circle_ticks <- function(element, x, y, padding, position) { + + n <- length(x) + f <- function(x, i) unit(rep(x, n), "npc") + + xend <- switch( + position, + left = unit(rep(0, n), "npc") - unit(padding[4], "cm"), + right = unit(rep(1, n), "npc") + unit(padding[2], "cm"), + x + ) + + yend <- switch( + position, + top = unit(rep(1, n), "npc") + unit(padding[1], "cm"), + bottom = unit(rep(0, n), "npc") - unit(padding[3], "cm"), + y + ) + + interleave <- vec_interleave(seq_len(n), seq_len(n) + n) + x <- unit.c(x, xend)[interleave] + y <- unit.c(y, yend)[interleave] + + element_grob(element, x = x, y = y, id.lengths = rep(2, n)) +} + +draw_circles <- function(decor, vjust = 0, hjust = 0.5) { + + data <- vec_slice(decor$data, decor$data$.draw %||% TRUE) + glyph <- lapply(seq_len(nrow(data)), function(i) { + decor$draw_key(vec_slice(data, i), decor$params, 0) + }) + size <- map_dbl(glyph, function(x) max(x$gp$fontsize %||% 0)) + shape <- map_dbl(glyph, function(x) if (is.numeric(x$pch)) x$pch[1] else 1L) + + # These are some arcane incantations I've long forgotten why they work + size <- size * magic_num * pch_mult[shape] + ydiv <- pch_div0[shape] * (1 - vjust) + pch_div1[shape] * vjust + xdiv <- (pch_div0[shape] * (1 - hjust) + pch_div1[shape] * hjust) * asp[shape] + + yoffset <- (max(size) - size) / ydiv + xoffset <- (max(size) - size) / xdiv + + ypos <- (vjust - 0.5) * 2 + ypos <- unit(ynpc[shape], "npc") + unit(ypos * yoffset, "pt") + + xpos <- (hjust - 0.5) * 2 + xpos <- unit(0.5, "npc") + unit(xpos * xoffset, "pt") + + glyph <- Map(function(grob, x, y) { + vp <- editViewport(grob$vp %||% viewport(), x = x, y = y) + editGrob(grob, vp = vp) + }, grob = glyph, x = xpos, y = ypos) + + if (all(shape %in% c(0, 7, 12, 14, 15, 22))) { + xpos <- xpos + unit((hjust - 0.5) * -size, "pt") + ypos <- ypos + unit((vjust - 0.5) * -size, "pt") + } else { + angle <- 1.5 * pi - atan2(vjust * 2 - 1, hjust * 2 - 1) + xpos <- xpos + unit(sin(angle) * 0.5 * size, "pt") + ypos <- ypos + unit(cos(angle) * 0.5 * size * asp[shape], "pt") + } + + gTree( + height = unit(max(size), "pt"), + width = unit(max(size * asp[shape]), "pt"), + xpos = xpos, ypos = ypos, + children = inject(gList(!!!glyph)) + ) +} + +# Magic constants --------------------------------------------------------- + +# Don't bother asking me to implement other shapes. I've already forgotten +# why the calculations do what they do. + +magic_num <- .pt / .stroke + +pch_div <- rep( + c(2, 3, 2, 1.5, 2, 3, 2, 3, 1.5), + c(1L, 1L, 3L, 1L, 10L, 1L, 6L, 1L, 1L) +) + +pch_div0 <- pch_div +pch_div1 <- pch_div +pch_div1[c(2, 17, 24)] <- 1.5 +pch_div1[c(6, 25)] <- 3 + +ynpc <- rep(0.5, 25) +ynpc[c(2, 17, 24)] <- 0.35 +ynpc[c(6, 25)] <- 0.65 + +asp <- rep(1, 25) +asp[c(2, 6, 17, 24, 25)] <- 2 / sqrt(3) + +pch_mult <- c( + 1, 1.166, 1.41466666666667, 1, 1.41466666666667, 1.166, 1, + 1, 1.41466666666667, 1, 1.55466666666667, 1, 1, 1, 1, 1, 1.166, + 1, 1, 0.666666666666667, 1, 0.886, 1.25333333333333, 1.166, 1.166 +) diff --git a/R/themes.R b/R/themes.R index 72b1e2a..d2e1c9d 100644 --- a/R/themes.R +++ b/R/themes.R @@ -46,6 +46,8 @@ #' @param key.spacing,key.spacing.x,key.spacing.y A [``][grid::unit()] #' setting the `legend.key.spacing`, `legend.key.spacing.x` and #' `legend.key.spacing.y` elements respectively. +#' @param key.margin A [``][ggplot2::margin] setting the margin around +#' legend glyphs. #' @param frame An [``][ggplot2::element_rect] setting the #' `legend.frame` element. #' @param byrow A `` setting the `legend.byrow` element. @@ -105,6 +107,7 @@ theme_guide <- function( key.spacing = NULL, key.spacing.x = NULL, key.spacing.y = NULL, + key.margin = NULL, frame = NULL, byrow = NULL, @@ -168,6 +171,7 @@ theme_guide <- function( legend.key.spacing = key.spacing, legend.key.spacing.x = key.spacing.x, legend.key.spacing.y = key.spacing.y, + legendry.legend.key.margin = key.margin, legend.frame = frame, legend.byrow = byrow, @@ -200,6 +204,7 @@ register_legendry_elements <- function() { legendry.legend.mini.ticks.length = rel(0.5), legendry.legend.subtitle = element_text(size = rel(0.9)), legendry.legend.subtitle.position = "top", + legendry.legend.key.margin = NULL, legendry.axis.mini.ticks = element_line(), legendry.axis.mini.ticks.length = rel(0.5), legendry.guide.spacing = unit(2.25, "pt"), @@ -219,6 +224,7 @@ register_legendry_elements <- function() { legendry.legend.mini.ticks.length = el_unit("legendry.legend.minor.ticks.length"), legendry.legend.subtitle = el_def("element_text", "legend.title"), legendry.legend.subtitle.position = el_def("character"), + legendry.legend.key.margin = el_def("margin", "legend.margin"), legendry.axis.mini.ticks = el_line("axis.ticks"), legendry.axis.mini.ticks.length = el_unit("axis.minor.ticks.length"), legendry.guide.spacing = el_unit("axis.ticks.length"), diff --git a/man/guide_axis_base.Rd b/man/guide_axis_base.Rd index 55d7191..0e435ba 100644 --- a/man/guide_axis_base.Rd +++ b/man/guide_axis_base.Rd @@ -125,6 +125,7 @@ ggplot(msleep, aes(bodywt, brainwt)) + Other standalone guides: \code{\link{guide_axis_dendro}()}, \code{\link{guide_axis_nested}()}, +\code{\link{guide_circles}()}, \code{\link{guide_colbar}()}, \code{\link{guide_colring}()}, \code{\link{guide_colsteps}()}, diff --git a/man/guide_axis_dendro.Rd b/man/guide_axis_dendro.Rd index 2597c85..7922027 100644 --- a/man/guide_axis_dendro.Rd +++ b/man/guide_axis_dendro.Rd @@ -114,6 +114,7 @@ p + guides(y = primitive_segments(clust), y.sec = "axis") Other standalone guides: \code{\link{guide_axis_base}()}, \code{\link{guide_axis_nested}()}, +\code{\link{guide_circles}()}, \code{\link{guide_colbar}()}, \code{\link{guide_colring}()}, \code{\link{guide_colsteps}()}, diff --git a/man/guide_axis_nested.Rd b/man/guide_axis_nested.Rd index a42e997..8c37841 100644 --- a/man/guide_axis_nested.Rd +++ b/man/guide_axis_nested.Rd @@ -163,6 +163,7 @@ ggplot(mpg, aes(displ, hwy)) + Other standalone guides: \code{\link{guide_axis_base}()}, \code{\link{guide_axis_dendro}()}, +\code{\link{guide_circles}()}, \code{\link{guide_colbar}()}, \code{\link{guide_colring}()}, \code{\link{guide_colsteps}()}, diff --git a/man/guide_circles.Rd b/man/guide_circles.Rd new file mode 100644 index 0000000..62d13b6 --- /dev/null +++ b/man/guide_circles.Rd @@ -0,0 +1,124 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/gizmo-circles.R +\name{guide_circles} +\alias{guide_circles} +\title{Circle size guide} +\usage{ +guide_circles( + key = NULL, + title = waiver(), + theme = NULL, + hjust = 0.5, + vjust = 0, + text_position = NULL, + clip_text = FALSE, + override.aes = list(shape = 1), + position = waiver(), + direction = NULL +) +} +\arguments{ +\item{key}{A \link[=key_standard]{standard key} specification. Defaults to +\code{\link[=key_auto]{key_auto()}}. See more information in the linked topic.} + +\item{title}{A \verb{} or \verb{} indicating the title of +the guide. If \code{NULL}, the title is not shown. The default, +\code{\link[ggplot2:waiver]{waiver()}}, takes the name of the scale object or +the name specified in \code{\link[ggplot2:labs]{labs()}} as the title.} + +\item{theme}{A \code{\link[ggplot2:theme]{}} object to style the guide individually or +differently from the plot's theme settings. The \code{theme} argument in the +guide overrides and is combined with the plot's theme.} + +\item{hjust, vjust}{A \verb{} between 0 and 1 giving the horizontal +and vertical justification, respectively, of the central shapes. It is +recommended \code{hjust = 0.5} when text is placed on the left or right and +\code{vjust = 0.5} is recommended when text is placed on top or in the bottom.} + +\item{text_position}{A string, one of \code{"ontop"}, \code{"top"}, \code{"right"}, +\code{"bottom"}, or \code{"left"} do describe the placement of labels. The +default (\code{NULL}), will take the \code{legend.text.position} theme setting.} + +\item{clip_text}{A \verb{} whether to give text in the \code{"ontop"} +position a small rectangle of background colour.} + +\item{override.aes}{A named \verb{} specifying aesthetic parameters of the +key glyphs. See details and examples in +\code{\link[ggplot2:guide_legend]{guide_legend()}}.} + +\item{position}{A \verb{} giving the location of the guide. Can be one of \code{"top"}, +\code{"bottom"}, \code{"left"} or \code{"right"}.} + +\item{direction}{A \verb{} indicating the direction of the guide. Can be on of +\code{"horizontal"} or \code{"vertical"}.} +} +\value{ +A \verb{} object. +} +\description{ +This guide displays the sizes of points as a series of circles. It is +typically paired with \code{\link[ggplot2:geom_point]{geom_point()}} with +\code{\link[ggplot2:draw_key]{draw_key_point()}} glyphs. +} +\details{ +Please note that the default size scales scale to area, not radius, so +equidistant breaks will appear at irregularly spaced positions due to +labelling the diameter of a circle. + +This graph was designed with standard round \link[ggplot2:scale_shape]{shapes} +in mind, i.e. shapes 1, 16, 19 and 21. For that reason, \code{shape = 1} is the +default \code{override.aes} argument. Other shapes will probably be drawn but the +quality of their alignment and label placement may be unsatisfactory. +} +\examples{ +# A standard plot +p <- ggplot(mtcars, aes(disp, mpg)) + + geom_point(aes(size = hp), alpha = 0.3) + +# By default, the sizes aren't large enough to make this guide clear +p + scale_size_area(guide = "circles") + +# Update with a more approrpriate scale +p <- p + + scale_size_area( + max_size = 30, + limits = c(0, NA), + breaks = c(0, 25, 100, 250) + ) +p + guides(size = "circles") + +# Horizontal orientation +p + guides(size = guide_circles( + vjust = 0.5, hjust = 0, text_position = "bottom" +)) + +# Alternative text placement +p + guides(size = guide_circles( + text_position = "ontop", + clip_text = TRUE +)) + +# More styling options +p + guides(size = guide_circles(override.aes = aes(colour = "red")))+ + theme( + # Key background + legend.key = element_rect(colour = "black", fill = 'white'), + # Padding around central shapes + legendry.legend.key.margin = margin(1, 1, 1, 1, "cm"), + legend.ticks = element_line(colour = "blue"), + legend.text.position = "left" + ) +} +\seealso{ +Other standalone guides: +\code{\link{guide_axis_base}()}, +\code{\link{guide_axis_dendro}()}, +\code{\link{guide_axis_nested}()}, +\code{\link{guide_colbar}()}, +\code{\link{guide_colring}()}, +\code{\link{guide_colsteps}()}, +\code{\link{guide_legend_base}()}, +\code{\link{guide_legend_cross}()}, +\code{\link{guide_legend_group}()} +} +\concept{standalone guides} diff --git a/man/guide_colbar.Rd b/man/guide_colbar.Rd index 5ab923d..a6c7d3e 100644 --- a/man/guide_colbar.Rd +++ b/man/guide_colbar.Rd @@ -158,6 +158,7 @@ Other standalone guides: \code{\link{guide_axis_base}()}, \code{\link{guide_axis_dendro}()}, \code{\link{guide_axis_nested}()}, +\code{\link{guide_circles}()}, \code{\link{guide_colring}()}, \code{\link{guide_colsteps}()}, \code{\link{guide_legend_base}()}, diff --git a/man/guide_colring.Rd b/man/guide_colring.Rd index c9ec3bc..cb99b20 100644 --- a/man/guide_colring.Rd +++ b/man/guide_colring.Rd @@ -110,6 +110,7 @@ Other standalone guides: \code{\link{guide_axis_base}()}, \code{\link{guide_axis_dendro}()}, \code{\link{guide_axis_nested}()}, +\code{\link{guide_circles}()}, \code{\link{guide_colbar}()}, \code{\link{guide_colsteps}()}, \code{\link{guide_legend_base}()}, diff --git a/man/guide_colsteps.Rd b/man/guide_colsteps.Rd index 318df2e..5d1faf4 100644 --- a/man/guide_colsteps.Rd +++ b/man/guide_colsteps.Rd @@ -155,6 +155,7 @@ Other standalone guides: \code{\link{guide_axis_base}()}, \code{\link{guide_axis_dendro}()}, \code{\link{guide_axis_nested}()}, +\code{\link{guide_circles}()}, \code{\link{guide_colbar}()}, \code{\link{guide_colring}()}, \code{\link{guide_legend_base}()}, diff --git a/man/guide_legend_base.Rd b/man/guide_legend_base.Rd index fc567a9..da99704 100644 --- a/man/guide_legend_base.Rd +++ b/man/guide_legend_base.Rd @@ -108,6 +108,7 @@ Other standalone guides: \code{\link{guide_axis_base}()}, \code{\link{guide_axis_dendro}()}, \code{\link{guide_axis_nested}()}, +\code{\link{guide_circles}()}, \code{\link{guide_colbar}()}, \code{\link{guide_colring}()}, \code{\link{guide_colsteps}()}, diff --git a/man/guide_legend_cross.Rd b/man/guide_legend_cross.Rd index 10a5012..f4fff66 100644 --- a/man/guide_legend_cross.Rd +++ b/man/guide_legend_cross.Rd @@ -109,6 +109,7 @@ Other standalone guides: \code{\link{guide_axis_base}()}, \code{\link{guide_axis_dendro}()}, \code{\link{guide_axis_nested}()}, +\code{\link{guide_circles}()}, \code{\link{guide_colbar}()}, \code{\link{guide_colring}()}, \code{\link{guide_colsteps}()}, diff --git a/man/guide_legend_group.Rd b/man/guide_legend_group.Rd index 8070886..d2e516a 100644 --- a/man/guide_legend_group.Rd +++ b/man/guide_legend_group.Rd @@ -99,6 +99,7 @@ Other standalone guides: \code{\link{guide_axis_base}()}, \code{\link{guide_axis_dendro}()}, \code{\link{guide_axis_nested}()}, +\code{\link{guide_circles}()}, \code{\link{guide_colbar}()}, \code{\link{guide_colring}()}, \code{\link{guide_colsteps}()}, diff --git a/man/legendry_extensions.Rd b/man/legendry_extensions.Rd index 75fa7fd..8755497 100644 --- a/man/legendry_extensions.Rd +++ b/man/legendry_extensions.Rd @@ -1,12 +1,12 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/compose-.R, R/compose-crux.R, % R/compose-ontop.R, R/compose-sandwich.R, R/compose-stack.R, -% R/gizmo-barcap.R, R/gizmo-density.R, R/gizmo-grob.R, R/gizmo-histogram.R, -% R/gizmo-stepcap.R, R/guide-legend-base.R, R/guide-legend-group.R, -% R/guide_colring.R, R/legendry-package.R, R/primitive-box.R, -% R/primitive-bracket.R, R/primitive-fence.R, R/primitive-labels.R, -% R/primitive-line.R, R/primitive-spacer.R, R/primitive-ticks.R, -% R/primitive-title.R +% R/gizmo-barcap.R, R/gizmo-circles.R, R/gizmo-density.R, R/gizmo-grob.R, +% R/gizmo-histogram.R, R/gizmo-stepcap.R, R/guide-legend-base.R, +% R/guide-legend-group.R, R/guide_colring.R, R/legendry-package.R, +% R/primitive-box.R, R/primitive-bracket.R, R/primitive-fence.R, +% R/primitive-labels.R, R/primitive-line.R, R/primitive-spacer.R, +% R/primitive-ticks.R, R/primitive-title.R \docType{data} \name{Compose} \alias{Compose} @@ -15,6 +15,7 @@ \alias{ComposeSandwich} \alias{ComposeStack} \alias{GizmoBarcap} +\alias{GuideCircles} \alias{GizmoDensity} \alias{GizmoGrob} \alias{GizmoHistogram} diff --git a/man/theme_guide.Rd b/man/theme_guide.Rd index 951094b..35453d2 100644 --- a/man/theme_guide.Rd +++ b/man/theme_guide.Rd @@ -27,6 +27,7 @@ theme_guide( key.spacing = NULL, key.spacing.x = NULL, key.spacing.y = NULL, + key.margin = NULL, frame = NULL, byrow = NULL, background = NULL, @@ -95,6 +96,9 @@ respectively.} setting the \code{legend.key.spacing}, \code{legend.key.spacing.x} and \code{legend.key.spacing.y} elements respectively.} +\item{key.margin}{A \code{\link[ggplot2:element]{}} setting the margin around +legend glyphs.} + \item{frame}{An \code{\link[ggplot2:element]{}} setting the \code{legend.frame} element.} diff --git a/tests/testthat/_snaps/gizmo-circles/guide-circles-text-locations.svg b/tests/testthat/_snaps/gizmo-circles/guide-circles-text-locations.svg new file mode 100644 index 0000000..fa5321d --- /dev/null +++ b/tests/testthat/_snaps/gizmo-circles/guide-circles-text-locations.svg @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +x + + + + + + + + + + + + + + + + + + + + + +0 +3 +12 +25 + + + + + + + + + + + + + + + + + + +x + + + + + + + + + + + + + + + + + + + + + +0 +3 +12 +25 + + + + + + + + + + + + + + + + + + +x + + + + + + + + + + + + + + + + + + + + + +0 +3 +12 +25 + + + + + + + + + + + + + + + + + + +x + + + + + + + + + + + + + + + + + + + + + +0 +3 +12 +25 + + + + + + diff --git a/tests/testthat/_snaps/gizmo-circles/guide-circles-text-placement.svg b/tests/testthat/_snaps/gizmo-circles/guide-circles-text-placement.svg new file mode 100644 index 0000000..88ea97c --- /dev/null +++ b/tests/testthat/_snaps/gizmo-circles/guide-circles-text-placement.svg @@ -0,0 +1,346 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +x + + + + + + + + + + + + + +0 +3 +12 +25 + + + + + + + + + + + + + + + + + + +x + + + + + + + + + + + + + +0 +3 +12 +25 + + + + + + + + + + + + + + + + + + +x + + + + + + + + + + + + + +0 +3 +12 +25 + + + + + + + + + + + + + + + + + + +x + + + + + + + + + + + + + +0 +3 +12 +25 + + + + + + + + + + + + + + + + + + +x + + + + + + + + + + + + + +0 +3 +12 +25 + + + + + + + + + + + + + + + + + + +x + + + + + + + + + + + + + +0 +3 +12 +25 + + + + + + + + + + + + + + + + + + +x + + + + + + + + + + + + + +0 +3 +12 +25 + + + + + + + + + + + + + + + + + + +x + + + + + + + + + + + + + +0 +3 +12 +25 + + + + + + + + + + + + + + + + + + +x + + + + + + + + + + + + + +0 +3 +12 +25 + + + + + + diff --git a/tests/testthat/test-gizmo-circles.R b/tests/testthat/test-gizmo-circles.R new file mode 100644 index 0000000..a40f56d --- /dev/null +++ b/tests/testthat/test-gizmo-circles.R @@ -0,0 +1,66 @@ +test_that("label placement is ok regardless of hjust/vjust", { + + p <- ggplot(data.frame(x = c(4, 12, 25))) + + geom_point(aes(x, x, size = x)) + + scale_size_area( + limits = c(0, 25), + breaks = c(0, 3, 12, 25), + max_size = 20, + guide = guide_circles() + ) + + build <- ggplot_build(p) + guide <- build$plot$guides$get_guide("size") + params <- build$plot$guides$get_params("size") + params[c("position", "direction")] <- list("right", "vertical") + + grid <- vec_expand_grid(hjust = c(0, 0.5, 1), vjust = c(0, 0.5, 1)) + + grobs <- lapply(vec_seq_along(grid), function(i) { + tmp <- params + tmp[c("hjust", "vjust")] <- as.list(grid[i, ]) + guide$draw( + theme_get() + theme(legend.text.position = "ontop"), + params = tmp + ) + }) + + gt <- gtable(unit(rep(1, 3), "null"), unit(rep(1, 3), "null")) + gt <- gtable_add_grob( + gt, grobs, + t = grid$vjust * 2 + 1, + l = grid$hjust * 2 + 1 + ) + + vdiffr::expect_doppelganger( + "guide_circles text placement", + gt + ) + + grid <- data.frame( + text = c("top", "right", "bottom", "left"), + hjust = c(1, 0.5, 0, 0.5), + vjust = c(0.5, 0, 0.5, 1) + ) + + grobs <- lapply(vec_seq_along(grid), function(i) { + tmp <- params + tmp[c("text_position", "hjust", "vjust")] <- as.list(grid[i, ]) + guide$draw( + theme_get(), + params = tmp + ) + }) + + gt <- gtable(unit(rep(1, 2), "null"), unit(rep(1, 2), "null")) + gt <- gtable_add_grob( + gt, grobs, + t = c(1, 2, 1, 2), + l = c(1, 1, 2, 2) + ) + + vdiffr::expect_doppelganger( + "guide_circles text locations", + gt + ) +})