Alpha aesthetic shows arrow's skeleton instead of plain shape - how to prevent it?
Asked Answered
N

2

16

I'm aiming at building a bar plot with arrows at the end of bars. I went for geom_segment with arrow defined. I want to map one column onto transparency, but the alpha aesthetic doesn't seem to work fine with arrow object. Here's the code snippet:

tibble(y = c(10, 20, 30), n = c(300, 100, 200), transparency = c(10, 2, 4)) %>% 
  ggplot() + geom_segment(aes(x = 0, xend = n, y = y, yend = y, alpha = transparency), 
                          colour = 'red', size = 10, arrow = arrow(length = unit(1.5, 'cm'), type = 'closed')) +
  scale_y_continuous(limits = c(5, 35))

enter image description here

It can be easily observed that arrow object doesn't look well with lower values of alpha, showing its skeleton instead of plain, transparent shape. Is there a way to prevent it?

Nadbus answered 28/2, 2020 at 6:58 Comment(3)
Interesting observation - I can only think of some workaround like drawing a separate segment with smaller width, e.g. like this:tibble(y = c(10, 20, 30), n = c(300, 100, 200), transparency = c(10, 2, 4)) %>% ggplot() + geom_segment(aes(x = 0, xend = n-10, y = y, yend = y, alpha = transparency), colour = 'red', size = 10) + geom_segment(aes(x = n-0.1, xend = n, y = y, yend = y, alpha = transparency), colour = 'red', size = 1, arrow = arrow(length = unit(1.5, 'cm'), type = 'closed')) + scale_y_continuous(limits = c(5, 35))Pretorius
this is interesting indeed. I guess this is not avoidable without calculating the exact area for the overlapping "skeletons", and to set the alpha programmatically for each area (this will be a terrible hack). If you really really want transparent arrows, another approach would be to draw 1) the segment and 2) adjacent to it a trianlge. (this also seems like quite a hack to me) .Temblor
You would definitely be right that it would be nice to have a flat transparency for arrows. I believe this is not caused by any behaviour on ggplot's end but seems to be related to how the 'grid' package draws arrows (on which ggplot2 depends).Voronezh
B
24

We can create a new geom, geom_arrowbar, that we can use like any other geom, so in your case it would give the desired plot by just doing:

tibble(y = c(10, 20, 30), n = c(300, 100, 200), transparency = c(10, 2, 4)) %>%
  ggplot() +
  geom_arrowbar(aes(x = n, y = y, alpha = transparency), fill = "red") +
  scale_y_continuous(limits = c(5, 35)) +
  scale_x_continuous(limits = c(0, 350))

enter image description here

And it contains 3 parameters, column_width, head_width and head_length that allow you to change the shape of the arrow if you don't like the defaults. We can also specify the fill colour and other aesthetics as needed:

tibble(y = c(10, 20, 30), n = c(300, 100, 200), transparency = c(10, 2, 4)) %>%
  ggplot() +
  geom_arrowbar(aes(x = n, y = y, alpha = transparency, fill = as.factor(n)),
                column_width = 1.8, head_width = 1.8, colour = "black") +
  scale_y_continuous(limits = c(5, 35)) +
  scale_x_continuous(limits = c(0, 350))

enter image description here

The only snag being that we have to write it first!

Following the examples in the extending ggplot2 vignette, we can define our geom_arrowbar in the same way that other geoms are defined, except we want to be able to pass in our 3 parameters that control the shape of the arrow. These are added to the params list of the resultant layer object, which will be used to create our arrows layer:

library(tidyverse)

geom_arrowbar <- function(mapping = NULL, data = NULL, stat = "identity",
                          position = "identity", na.rm = FALSE, show.legend = NA,
                          inherit.aes = TRUE, head_width = 1, column_width = 1,
                          head_length = 1, ...) 
{
  layer(geom = GeomArrowBar, mapping = mapping, data = data, stat = stat,
        position = position, show.legend = show.legend, inherit.aes = inherit.aes,
        params = list(na.rm = na.rm, head_width = head_width,
                      column_width = column_width, head_length = head_length, ...))
}

Now "all" that remains is to define what a GeomArrowBar is. This is effectively a ggproto class definition. The most important part of it is the draw_panel member function, which takes each line of our dataframe and converts it into arrow shapes. After some basic maths to work out from the x and y co-ordinates as well as our various shape parameters what the shape of the arrow should be, it produces one grid::polygonGrob for each line of our data and stores it in a gTree. This forms the graphical component of the layer.

GeomArrowBar <- ggproto("GeomArrowBar", Geom,
  required_aes = c("x", "y"),
  default_aes = aes(colour = NA, fill = "grey20", size = 0.5, linetype = 1, alpha = 1),
  extra_params = c("na.rm", "head_width", "column_width", "head_length"),
  draw_key = draw_key_polygon,
  draw_panel = function(data, panel_params, coord, head_width = 1,
                        column_width = 1, head_length = 1) {
    hwidth <- head_width / 5
    wid <- column_width / 10
    len <- head_length / 10
    data2 <- data
    data2$x[1] <- data2$y[1] <- 0
    zero <- coord$transform(data2, panel_params)$x[1]
    coords <- coord$transform(data, panel_params)
    make_arrow_y <- function(y, wid, hwidth) {
      c(y - wid/2, y - wid/2, y - hwidth/2, y, y + hwidth/2, y + wid/2, y + wid/2)
    }
    make_arrow_x <- function(x, len){
      if(x < zero) len <- -len
      return(c(zero, x - len, x - len , x, x - len, x - len, zero))
    }
    my_tree <- grid::gTree()
    for(i in seq(nrow(coords))){
      my_tree <- grid::addGrob(my_tree, grid::polygonGrob(
        make_arrow_x(coords$x[i], len),
        make_arrow_y(coords$y[i], wid, hwidth),
        default.units = "native",
        gp = grid::gpar(
          col = coords$colour[i],
          fill = scales::alpha(coords$fill[i], coords$alpha[i]),
          lwd = coords$size[i] * .pt,
          lty = coords$linetype[i]))) }
    my_tree}
)

This implementation is far from perfect. It is missing some important functionality, such as sensible default axis limits and the ability to coord_flip, and it will produce unaesthetic results if the arrow heads are longer than the whole column (though you might not want to use such a plot in that situation anyway). It will, however, sensibly have the arrow pointing to the left if you have a negative value. A better implementation might also add an option for empty arrow heads.

In short, it would need a lot of tweaks to iron out these (and other) bugs and make it production-ready, but it's good enough to produce some nice charts without too much effort in the meantime.

Created on 2020-03-08 by the reprex package (v0.3.0)

Bothy answered 8/3, 2020 at 12:7 Comment(3)
This is almost perfect. I am trying to make vertically pointing arrows. (from the x-Axis to the (x,y) point, I tried to work around it using coord_flip() which didn't work. Probaply I only have to switch x and y at somepoint in your code put by trying to do so I completely messed it up. Could you help me out?Shawndashawnee
@Shawndashawnee comments aren't the best place to get help. Maybe you could post a new question with your data? Alternatively the solution below might help?Bothy
unfortunately ` Warning in install.packages : package ?ggenes? is not available (for R version 3.4.4) ` Asking a new question felt redundant to this one.Shawndashawnee
L
11

You could use geom_gene_arrow from library(gggenes)

data.frame(y=c(10, 20, 30), n=c(300, 100, 200), transparency=c(10, 2, 4)) %>% 
  ggplot() + 
  geom_gene_arrow(aes(xmin = 0, xmax = n, y = y, alpha = transparency), 
                  arrowhead_height = unit(6, "mm"), fill='red') +
  scale_y_continuous(limits = c(5, 35))

enter image description here

Libna answered 12/3, 2020 at 13:20 Comment(1)
This must be the wheel that I've just reinvented! ;)Bothy

© 2022 - 2024 — McMap. All rights reserved.