Skip to content

Feature request: New geom_dash() #6430

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jbengler opened this issue Apr 15, 2025 · 11 comments
Closed

Feature request: New geom_dash() #6430

jbengler opened this issue Apr 15, 2025 · 11 comments
Labels
feature a feature request or enhancement layers 📈

Comments

@jbengler
Copy link

Hi Teun!

Measures of central tendency—like the mean or median—are often visualized using a horizontal dash. To my knowledge, this is not easily done in current ggplot2. Some workarounds exist, such as using geom_errorbar() in creative ways. However, this approach stacks several lines on top of each other. It also requires a good understanding of how to tweak stat_summary(). Overall, current options feel unnecessarily clunky to me.

https://stackoverflow.com/questions/59018229/adding-a-dashed-line-to-mark-the-mean-in-ggplot

Therefore, I drafted a new geom_dash(), which you can see in action below. In case this would be of interest, I could provide a PR to be used as a starting point.

Image Image Image Image
@clauswilke
Copy link
Member

I have wanted something like this for a long time. (Even wrote my own, but then flaked out and never put it anywhere where it could be reused.) I think ggforce would be a better place for it though.

@jbengler
Copy link
Author

I thought this was such a basic and commonly used geom when plotting continuous vs. categorical data that it might even be built into ggplot2 itself.

@teunbrand
Copy link
Collaborator

Thanks for the suggestion Broder! I agree that this is useful and definitely should live somewhere. After a cursory rummage through existing extensions I couldn't quickly find something similar. I'm a bit torn between 'it should live in ggplot2' and 'it should live in an extension package'. On the one hand, yes this is useful in a pretty broad way. On the other hand, a policy that ggplot2 has is that it tries to take on as little as possible additional geoms and stats (other than meta-functionality). I'll discuss with Thomas after Easter.

@jbengler
Copy link
Author

Thanks for considering!

@clauswilke
Copy link
Member

Related:
thomasp85/ggforce#198
https://wilkelab.org/ungeviz/articles/misc-geoms-stats.html#horizontal-and-vertical-plines

I think you need both horizontal and vertical lines.

@teunbrand
Copy link
Collaborator

using geom_errorbar() in creative ways. However, this approach stacks several lines on top of each other

I have no problems just removing duplicate and empty lines from geom_errorbar() so that it can be put into service of drawing dashes though.

@clauswilke
Copy link
Member

To me the issue with geom_errorbar() is that it's not a drop-in replacement for geom_point(), as it expects minimum and maximum values. I would like to have a direct equivalent for geom_point() that draws horizontal or vertical lines instead of points.

@jbengler
Copy link
Author

jbengler commented Apr 15, 2025

I have no problems just removing duplicate and empty lines from geom_errorbar() so that it can be put into service of drawing dashes though.

I think this describes what I did. I took geom_errobar() as a template and removed all unnecessary elements.

geom_dash <- function(mapping = NULL, data = NULL,
                          stat = "identity", position = "identity",
                          ...,
                          na.rm = FALSE,
                          orientation = NA,
                          show.legend = NA,
                          inherit.aes = TRUE) {
  layer(
    data = data,
    mapping = mapping,
    stat = stat,
    geom = GeomDash,
    position = position,
    show.legend = show.legend,
    inherit.aes = inherit.aes,
    params = rlang::list2(
      na.rm = na.rm,
      orientation = orientation,
      ...
    )
  )
}

#' @rdname ggplot2-ggproto
#' @format NULL
#' @usage NULL
#' @export
GeomDash <- ggproto("GeomDash", Geom,

                        default_aes = aes(
                          colour = from_theme(colour %||% ink),
                          linewidth = from_theme(linewidth),
                          linetype = from_theme(linetype),
                          width = 0.9,
                          alpha = NA
                        ),

                        draw_key = draw_key_path,

                        required_aes = c("x|y"),

                        setup_params = function(data, params) {
                          params$flipped_aes <- has_flipped_aes(data, params, range_is_orthogonal = TRUE)
                          # if flipped_aes == TRUE then y, xmin, xmax is present
                          if (!(params$flipped_aes || all(c("x") %in% c(names(data), names(params))))) {
                            cli::cli_abort("Either, {.field x} {.emph or} {.field y} must be supplied.")
                          }
                          params
                        },

                        extra_params = c("na.rm", "orientation"),

                        setup_data = function(self, data, params) {
                          data$flipped_aes <- params$flipped_aes
                          data <- flip_data(data, params$flipped_aes)
                          data <- ggplot2:::compute_data_size(
                            data, params$width,
                            default = self$default_aes$width,
                            zero = FALSE, discrete = TRUE
                          )
                          data <- transform(data,
                                            xmin = x - width / 2, xmax = x + width / 2, width = NULL
                          )
                          flip_data(data, params$flipped_aes)
                        },

                        # Note: `width` is vestigial
                        draw_panel = function(self, data, panel_params, coord, lineend = "butt",
                                              width = NULL, flipped_aes = FALSE) {
                          data <- ggplot2:::fix_linewidth(data, snake_class(self))
                          data <- ggplot2:::flip_data(data, flipped_aes)
                          x <- vctrs::vec_interleave(data$xmin, data$xmax)
                          y <- vctrs::vec_interleave(data$y, data$y)
                          data <- ggplot2:::data_frame0(
                            x = x,
                            y = y,
                            colour = rep(data$colour, each = 2),
                            alpha = rep(data$alpha, each = 2),
                            linewidth = rep(data$linewidth, each = 2),
                            linetype = rep(data$linetype, each = 2),
                            group = rep(seq_len(nrow(data)), each = 2),
                            .size = nrow(data) * 2
                          )
                          data <- ggplot2:::flip_data(data, flipped_aes)
                          GeomPath$draw_panel(data, panel_params, coord, lineend = lineend)
                        },

                        rename_size = TRUE
)

@jbengler
Copy link
Author

Thus, it now only requires x or y.

@teunbrand teunbrand added feature a feature request or enhancement layers 📈 labels Apr 16, 2025
@teunbrand
Copy link
Collaborator

I've discussed this with Thomas and the consensus was that we feel this would be a good fit for an extension package. You're absolutely free to build one yourself, but if you feel that is too much work, ggforce would also work (if Thomas ever gets around reviewing PRs 😁)

@teunbrand teunbrand closed this as not planned Won't fix, can't repro, duplicate, stale Apr 25, 2025
@jbengler
Copy link
Author

I still believe it would be great to see this integrated into ggplot2 someday—perhaps in an alternative future, or simply ten years down the line 😅

Thank you as well for suggesting ggforce as a potential home. I have recently been exploring the ggplot2 extension mechanisms more deeply, so I suppose time will tell whether I end up with a random bunch of geoms—or something I would actually feel comfortable calling a proper package. That said, the reach of such a standalone package would likely be quite limited, so there is definitely a strong case for ggforce. I will take some time to figure this out.

In any case, thank you again for considering it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature a feature request or enhancement layers 📈
Projects
None yet
Development

No branches or pull requests

3 participants