Skip to content

Commit

Permalink
Add geom_marquee, and element_marquee along with various bug fixes in…
Browse files Browse the repository at this point in the history
… marquee_grob found during development of the ggplot2 features
  • Loading branch information
thomasp85 committed Apr 29, 2024
1 parent cc48513 commit 3eec3aa
Show file tree
Hide file tree
Showing 6 changed files with 553 additions and 19 deletions.
14 changes: 8 additions & 6 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,27 @@ S3method(format,marquee_rem)
S3method(format,marquee_skip_inherit)
S3method(format,marquee_style)
S3method(format,marquee_style_set)
S3method(heightDetails,textboxgrob)
S3method(makeContent,marquee)
S3method(heightDetails,marquee_grob)
S3method(makeContent,marquee_grob)
S3method(makeContent,marquee_precalculated)
S3method(makeContent,svg_grob)
S3method(makeContext,marquee)
S3method(makeContext,marquee_precalculated)
S3method(makeContext,marquee_grob)
S3method(makeContext,marquee_precalculated_grob)
S3method(print,marquee_box)
S3method(print,marquee_em)
S3method(print,marquee_relative)
S3method(print,marquee_rem)
S3method(print,marquee_skip_inherit)
S3method(print,marquee_style)
S3method(print,marquee_style_set)
S3method(str,marquee_style)
S3method(widthDetails,textboxgrob)
S3method(widthDetails,marquee_grob)
export(base_style)
export(box)
export(classic_style)
export(element_grob.element_marquee)
export(element_marquee)
export(em)
export(geom_marquee)
export(marquee_bullets)
export(marquee_grob)
export(marquee_parse)
Expand Down
123 changes: 123 additions & 0 deletions R/element_marquee.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#' ggplot2 theme element supporting marquee syntax
#'
#' This theme element is a drop-in replacement for `ggplot2::element_text()`. It
#' works by integrating the various style settings of the element into the base
#' style of the provided style set. If a margin is given, it is set on the body
#' tag with [skip_inherit()]. The default width is `NA` meaning that it will
#' span as long as the given text is, doing no line wrapping. You can set it to
#' any unit to make it fit within a specific width. However, this may not work
#' as expected with rotated text (you may get lucky). Note that you may see
#' small shifts in the visuals when going from `element_text()` to
#' `element_marquee()` as size reporting may differ between the two elements.
#'
#' @param family The font family of the base style
#' @param colour The font colour of the base style
#' @param size The font size of the base style
#' @param lineheight The lineheight of the base style
#' @param margin The margin for the body tag
#' @param style A style set to base the rendering on
#' @param width The maximum width of the text. See the description for some
#' caveats for this
#' @inheritParams ggplot2::element_text
#'
#' @export
#'
#' @examples
#' if (is_installed("ggplot2")) {
#' library(ggplot2)
#' p <- ggplot(mtcars) +
#' geom_point(aes(mpg, disp)) +
#' labs(title = "A {.red *marquee*} title\n* Look at this bullet list\n\n* great, huh?") +
#' theme_gray(base_size = 6) +
#' theme(title = element_marquee())
#'
#' plot(p)
#'
#' ggplot(mtcars) +
#' geom_histogram(aes(x = mpg)) +
#' labs(title = "I put a plot in your title so you can plot while you title\n\n![](p)\n\nWhat more could you _possibly_ want?") +
#' theme(title = element_marquee())
#' }
#'
element_marquee <- function(family = NULL, colour = NULL, size = NULL, hjust = NULL,
vjust = NULL, angle = NULL, lineheight = NULL,
color = NULL, margin = NULL, style = NULL, width = NULL,
inherit.blank = FALSE, ...) {
if (!is.null(color))
colour <- color
n <- max(length(family), 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, colour = colour, size = size, hjust = hjust,
vjust = vjust, angle = angle, lineheight = lineheight,
margin = margin, style = style, width = width,
inherit.blank = inherit.blank),
class = c("element_marquee", "element_text", "element"))
}
#' @export
element_grob.element_marquee <- function(element, label = "", x = NULL, y = NULL, family = NULL,
colour = NULL, size = NULL, hjust = NULL, vjust = NULL,
angle = NULL, lineheight = NULL, margin = NULL, margin_x = FALSE,
margin_y = FALSE, style = NULL, width = NULL, ...) {
if (is.null(label)) return(ggplot2::zeroGrob())
style <- style %||% element$style %||% classic_style()
style <- modify_style(style, "base",
family = family %||% element$family %||% style$base$family,
color = colour %||% element$colour %||% style$base$color,
size = size %||% element$size %||% style$base$size,
lineheight = lineheight %||% element$lineheight %||% style$base$lineheight
)
margin <- margin %||% element$margin
if (!is.null(margin)) {
pad <- skip_inherit(box(
if (margin_y) margin[1] else 0,
if (margin_x) margin[2] else 0,
if (margin_y) margin[3] else 0,
if (margin_x) margin[4] else 0
))
style <- modify_style(style, "body", padding = pad)
}
just <- rotate_just(angle %||% element$angle, hjust %||% element$hjust, vjust %||% element$vjust)
n <- max(length(x), length(y), 1)
x <- x %||% rep(just$hjust, n)
y <- y %||% rep(just$vjust, n)
width <- width %||% element$width %||% NA
angle <- angle %||% element$angle %||% 0
marquee_grob(label, style, x = x, y = y, width = width,
hjust = hjust %||% element$hjust, vjust = vjust %||% element$vjust, angle = angle)
}

on_load({
on_package_load("ggplot2", registerS3method("element_grob", "element_marquee", element_grob.element_marquee, asNamespace("ggplot2")))
})

# Adaption of ggplot2:::rotate_just() to work with additional just keywords
rotate_just <- function (angle, hjust, vjust) {
angle <- (angle %||% 0)%%360
if (is.character(hjust)) {
hjust <- switch(hjust, "left" = , "left-ink" = 0, "center" = , "center-ink" = 0.5, "right" = , "right-ink" = 1)
}
if (is.character(vjust)) {
vjust <- switch(vjust, "bottom" = , "bottom-ink" = ,"last-line" = 0, "center" = , "center-ink" = 0.5, "top" = , "top-ink" = , "first-line" = 1)
}
size <- vctrs::vec_size_common(angle, hjust, vjust)
angle <- vctrs::vec_recycle(angle, size)
hjust <- vctrs::vec_recycle(hjust, size)
vjust <- vctrs::vec_recycle(vjust, size)
case <- findInterval(angle, c(0, 90, 180, 270, 360))
hnew <- hjust
vnew <- vjust
is_case <- which(case == 2)
hnew[is_case] <- 1 - vjust[is_case]
vnew[is_case] <- hjust[is_case]
is_case <- which(case == 3)
hnew[is_case] <- 1 - hjust[is_case]
vnew[is_case] <- 1 - vjust[is_case]
is_case <- which(case == 4)
hnew[is_case] <- vjust[is_case]
vnew[is_case] <- 1 - hjust[is_case]
list(hjust = hnew, vjust = vnew)
}
180 changes: 180 additions & 0 deletions R/geom_marquee.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#' Draw text formatted with marquee
#'
#' The geom is an extension of `geom_text()` and `geom_label()` that allows you
#' to draw richly formatted text in marquee-markdown format in your plot. For
#' plain text it is a near-drop-in replacement for the above geoms except some
#' sizing might be very slightly different. However, using this geom you are
#' able to access the much more powerful font settings available in marquee, so
#' even then it might make sense to opt for this geom.
#'
#' @inheritParams ggplot2::geom_text
#'
#' @details
#' Styling of the text is based on a style set with the exception that the
#' standard aesthetics such as family, size, colour, fill, etc. are recognized
#' and applied to the base tag style. The default style set ([classic_style])
#' can be changed using the style aesthetic which can take a vector of style
#' sets so that each text can rely on it's own style if needed. As with
#' [element_marquee()], the `fill` aesthetic is treated differently and not
#' applied to the base tag, but to the body tag as a [skip_inherit()] style so
#' as to not propagate the fill.
#'
#' Contrary to the standard text and label geoms, `geom_marquee()` takes a
#' `width` aesthetic that can be used to turn on soft wrapping of text. The
#' default value (`NA`) lets the text run as long as it want's (honoring hard
#' breaks), but setting this to something else will instruct marquee to use at
#' most that amount of space. You can use grid units to set it to an absolute
#' amount.
#'
#' @export
#'
#' @examples
#' # Standard use
#' p <- ggplot(mtcars, aes(wt, mpg))
#' p + geom_marquee(aes(label = rownames(mtcars)))
#'
#' # Make use of more powerful font features (note, result may depend on fonts
#' # installed on the system)
#' p + geom_marquee(
#' aes(label = rownames(mtcars)),
#' style = classic_style(weight = "thin", width = "condensed")
#' )
#'
#' # Turn on line wrapping
#' p + geom_marquee(aes(label = rownames(mtcars)), width = unit(2, "cm"))
#'
#' # Style like label
#' label_style <- modify_style(
#' classic_style(),
#' "body",
#' padding = skip_inherit(box(4)),
#' border = "black",
#' border_size = skip_inherit(box(1)),
#' border_radius = 3
#' )
#' p + geom_marquee(aes(label = rownames(mtcars), fill = gear), style = label_style)
#'
#' # Use markdown to style the text
#' red_bold_names <- sub("(\\w+)", "{.red **\\1**}", rownames(mtcars))
#' p + geom_marquee(aes(label = red_bold_names))
#'
geom_marquee <- function(mapping = NULL, data = NULL, stat = "identity",
position = "identity", ..., size.unit = "mm",
na.rm = FALSE, show.legend = NA, inherit.aes = TRUE) {
check_installed("ggplot2")

ggplot2::layer(
data = data, mapping = mapping, stat = stat, geom = GeomMarquee$geom,
position = position, show.legend = show.legend, inherit.aes = inherit.aes,
params = list2(size.unit = size.unit, na.rm = na.rm, ...)
)
}

GeomMarquee <- new_environment(list(geom = NULL))

make_marquee_geom <- function() {
GeomMarquee$geom <- ggplot2::ggproto(
"GeomMarquee", ggplot2::Geom,

required_aes = c("x", "y", "label"),

default_aes = ggplot2::aes(
colour = "black", fill = NA, size = 3.88, angle = 0,
hjust = 0.5, vjust = 0.5, alpha = NA, family = "", lineheight = 1.2,
style = classic_style(), width = NA
),

draw_panel = function(data, panel_params, coord, na.rm = FALSE, size.unit = "mm") {
lab <- data$label

styles <- data$style
if (!is_style_set(styles)) {
stop_input_type(styles, "a marquee_style_set object", arg = "style")
}
check_character(data$family, arg = "family")
size <- data$size * resolve_text_unit(size.unit) * 72.72 / 72
check_numeric(size, arg = "size")
check_numeric(data$lineheight)
colour <- ggplot2::alpha(data$colour, data$alpha)
check_character(colour, arg = "colour")
if (!is.character(data$fill) &&
!(is.logical(data$fill) && all(is.na(data$fill))) &&
!all(vapply(data$fill, function(x) is.character(x) || inherits(x, "GridPattern"), logical(1)))) {
stop_input_type(data$fill, "a character vector or a list of strings and patters", arg = "fill")
}
for (i in seq_along(styles)) {
styles[[i]]$base$family <- data$family[[i]]
styles[[i]]$base$size <- size[[i]]
styles[[i]]$base$lineheight <- data$lineheight[[i]]
styles[[i]]$base$color <- colour[[i]]
if (!"body" %in% names(styles[[i]])) {
styles[[i]]$body <- style()
}
styles[[i]]$body$background <- skip_inherit(data$fill[[i]])
}

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)

marquee_grob(
text = lab, style = styles,
x = data$x, y = data$y, width = data$width,
hjust = data$hjust, vjust = data$vjust,
angle = data$angle, name = "geom_marquee"
)
},

draw_key = ggplot2::draw_key_label
)
}

on_load(on_package_load("ggplot2", {
make_marquee_geom()
}))

combine_styles <- function(style, family, size, lineheight, color, background) {browser()
style <- modify_style(style, "base", family = family, size = size, color = color, lineheight = lineheight)
modify_style(style, "body", background = skip_inherit(background))
}

resolve_text_unit <- function(unit) {
unit <- arg_match0(unit, c("mm", "pt", "cm", "in", "pc", "Pt"))
switch(
unit,
"mm" = 2.845276,
"cm" = 28.45276,
"in" = 72.27,
"pc" = 12,
"Pt" = 72 / 72.27,
1
)
}

compute_just <- function (just, a = 0.5, b = a, angle = 0) {
if (!is.character(just)) {
return(just)
}
if (any(grepl("outward|inward", just))) {
angle <- angle%%360
angle <- ifelse(angle > 180, angle - 360, angle)
angle <- ifelse(angle < -180, angle + 360, angle)
rotated_forward <- grepl("outward|inward", just) & (angle > 45 & angle < 135)
rotated_backwards <- grepl("outward|inward", just) & (angle < -45 & angle > -135)
ab <- ifelse(rotated_forward | rotated_backwards, b, a)
just_swap <- rotated_backwards | abs(angle) > 135
inward <- (just == "inward" & !just_swap | just == "outward" & just_swap)
just[inward] <- c("left", "center", "right")[just_dir(ab[inward])]
outward <- (just == "outward" & !just_swap) | (just == "inward" & just_swap)
just[outward] <- c("right", "center", "left")[just_dir(ab[outward])]
}
just
}

just_dir <- function (x, tol = 0.001) {
out <- rep(2L, length(x))
out[x < 0.5 - tol] <- 1L
out[x > 0.5 + tol] <- 3L
out
}
Loading

0 comments on commit 3eec3aa

Please sign in to comment.