Skip to content

Adding names to a margin vector #6786

@dmuenz

Description

@dmuenz

I'd like to suggest that the margin function return a named vector, i.e., with names c("t", "r", "b", "l").

m <- ggplot2::margin(t = 1, r = 2, b = 3, l = 4)
m[3] # returns bottom margin
m["b"] # doesn't work -- returns NA

My motivation is that I'm doing some grob calculations that use, for example, just the bottom margin, and being able to write m["b"] would be more self-documenting than m[3].

To test this out, I modified the margin <- S7::new_class(...) definition by adding the line names(u) <- c("t", "r", "b", "l") right before the S7::new_object(u) call in the constructor function. This works, though there's a performance penalty. On my computer, calling the new margin constructor is about 2 μs slower (median time of 35.6 μs versus 34 μs for the original function). Accessing an individual element of the margin object is about 0.5 μs slower (median time of 2.8 μs versus 2.3 μs), and this 0.5 μs penalty holds whether we're accessing an element by name or by numeric index. My code for these benchmark tests is below the fold. I don't know whether these seemingly small penalties would add up to something significant when building/drawing a full ggplot object.

Thanks for your consideration.

Code and benchmarks
library(ggplot2)

# revised copy of the margin constructor, with named elements
margin_named <- S7::new_class(
  "margin_named",
  parent = S7::new_S3_class(c("simpleUnit", "unit", "unit_v2")),
  constructor = function(t = 0, r = 0, b = 0, l = 0, unit = "pt", ...) {
    ggplot2:::warn_dots_empty()
    lens <- c(length(t), length(r), length(b), length(l))
    if (any(lens != 1)) {
      incorrect <- c("t", "r", "b", "l")[lens != 1]
      s <- if (length(incorrect) > 1) "s" else ""
      cli::cli_warn(c(
        "In {.fn margin}, the argument{s} {.and {.arg {incorrect}}} should \\
        have length 1, not length {.and {lens[lens != 1]}}.",
        i = "Argument{s} get(s) truncated to length 1."
      ))
      t <- t[1]
      r <- r[1]
      b <- b[1]
      l <- l[1]
    }
    u <- unit(c(t, r, b, l), unit)
    names(u) <- c("t", "r", "b", "l") # name the unit vector
    S7::new_object(u)
  }
)

# compare constructing a margin object
bench::mark(orig = margin(t = 1, r = 2, b = 3, l = 4),
            new = margin_named(t = 1, r = 2, b = 3, l = 4),
            check = FALSE)

# make two margin objects, one unnamed and one named
m <- margin(t = 1, r = 2, b = 3, l = 4)
m_named <- margin_named(t = 1, r = 2, b = 3, l = 4)

# with the named margin object, we can access elements by name or number
m_named[3]
m_named["b"]

# compare accessing an element
bench::mark(m[3], m_named[3], m_named["b"], check = FALSE)

# show that naming a margin vector doesn't appear to mess up other parts of the
# ggplot build & draw process
p <- mpg |>
  ggplot(aes(cty, hwy)) +
  geom_point()

p + theme(margins = margin(1, 2, 3, 4, unit = "cm"))
p + theme(margins = margin_named(1, 2, 3, 4, unit = "cm"))

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions