27

In answering a recent visualization question I really needed braces to show a span on an axis, and I can't figure out how to do it in ggplot2. Here's the plot:

example plot

Instead of a tick mark, I'd really like the y axis label "Second letter of two-letter names" to have a brace extending from 1 to 10 (the vertical span of the red and blue second letters). But I'm not sure how to make that happen. The x axis could benefit from similar treatment.

Code is available in the linked CrossValidated question (and unnecessarily complicated for this example, so I won't show it). Instead, here's a minimal example:

library(ggplot2)
x <- c(runif(10),runif(10)+2)
y <- c(runif(10),runif(10)+2)
qplot(x=x,y=y) +
  scale_x_continuous("",breaks=c(.5,2.5),labels=c("Low types","High types") )

minimal example

In this case, a brace from (0,1) for low types and from (2,3) for the high types would be ideal instead of tick marks.

I'd rather not use geom_rect because:

  • The tick marks will remain
  • I'd prefer braces
  • It will be inside the plot instead of outside it

How would I accomplish this? The perfect answer would have:

  • A nice, smooth, thin curly brace
  • Drawn outside the plotting area
  • Specified via a high-level argument (ideally a range-type object passed to the breaks option in scale_x_continuous)
Community
  • 1
  • 1
Ari B. Friedman
  • 71,271
  • 35
  • 175
  • 235
  • Untested, but your question seemed familiar: http://stackoverflow.com/questions/6178763/how-to-add-braces-to-a-graph/6179288#6179288 – joran Aug 09 '11 at 19:36
  • @joran: Hoping for a ggplot2-specific solution though. I guess since your (very cool) answer is grid-based not lattice-based it should be able to be adopted. But I still don't know how to turn off the tick marks, plot things outside the plotting area, etc. – Ari B. Friedman Aug 09 '11 at 19:40
  • Any solution will almost certainly involve mucking about with grid. That other code wasn't mine (I'm not that good with grid) I just thought it might make for a good starting point for you, or others. – joran Aug 09 '11 at 19:48
  • It's handy, that's for sure. I guess my concern (in addition to the ones stated previously) is that it's still kludgy. ggplot2 is so beautiful about specifying data separately from presentation, and then I'd have to go draw almost with a pencil to get things the way I want them using the fancy curves you've provided. Seems like the right way to do it conceptually speaking would be as to pass a span object as one element of a list you pass to breaks in scale_x_continuous(). – Ari B. Friedman Aug 09 '11 at 20:13

6 Answers6

20

Another solution using a function that draws a curly bracket.

Thanks Gur!

curly <- function(N = 100, Tilt = 1, Long = 2, scale = 0.1, xcent = 0.5,
                  ycent = 0.5, theta = 0, col = 1, lwd = 1, grid = FALSE){

# N determines how many points in each curve
# Tilt is the ratio between the axis in the ellipse 
#  defining the curliness of each curve
# Long is the length of the straight line in the curly brackets 
#  in units of the projection of the curly brackets in this dimension
# 2*scale is the absolute size of the projection of the curly brackets 
#  in the y dimension (when theta=0)
# xcent is the location center of the x axis of the curly brackets
# ycent is the location center of the y axis of the curly brackets
# theta is the angle (in radians) of the curly brackets orientation
# col and lwd are passed to points/grid.lines

           ymin <- scale / Tilt
           y2 <- ymin * Long
           i <- seq(0, pi/2, length.out = N)

           x <- c(ymin * Tilt * (sin(i)-1),
                  seq(0,0, length.out = 2),
                  ymin * (Tilt * (1 - sin(rev(i)))),
                  ymin * (Tilt * (1 - sin(i))),
                  seq(0,0, length.out = 2),
                  ymin * Tilt * (sin(rev(i)) - 1))

           y <- c(-cos(i) * ymin,
                  c(0,y2),
                  y2 + (cos(rev(i))) * ymin,
                  y2 + (2 - cos(i)) * ymin,
                  c(y2 + 2 * ymin, 2 * y2 + 2 * ymin),
                  2 * y2 + 2 * ymin + cos(rev(i)) * ymin)

           x <- x + xcent
           y <- y + ycent - ymin - y2

           x1 <- cos(theta) * (x - xcent) - sin(theta) * (y - ycent) + xcent
           y1 <- cos(theta) * (y - ycent) + sin(theta) * (x - xcent) + ycent

           ##For grid library:
           if(grid){
              grid.lines(unit(x1,"npc"), unit(y1,"npc"),gp=gpar(col=col,lwd=lwd))
           }

           ##Uncomment for base graphics
           else{
              par(xpd=TRUE)
              points(x1,y1,type='l',col=col,lwd=lwd)
              par(xpd=FALSE)
           }

}


library(ggplot2)
x <- c(runif(10),runif(10)+2)
y <- c(runif(10),runif(10)+2)
qplot(x=x,y=y) +
  scale_x_continuous("",breaks=c(.5,2.5),labels=c("Low types","High types") )

curly(N=100,Tilt=0.4,Long=0.3,scale=0.025,xcent=0.2525,
      ycent=par()$usr[3]+0.1,theta=-pi/2,col="red",lwd=2,grid=TRUE)
curly(N=100,Tilt=0.4,Long=0.3,scale=0.025,xcent=0.8,
      ycent=par()$usr[3]+0.1,theta=-pi/2,col="red",lwd=2,grid=TRUE)

result plot

joran
  • 169,992
  • 32
  • 429
  • 468
user845354
  • 201
  • 1
  • 3
  • 1
    For the non-mathematics such as myself - If you convert angle to radians internally with `theta <- angle*pi/180`, you can use `angle` as a function argument - I find this easier. – tjebo Feb 24 '21 at 11:12
17

Update: Be sure to see this related Stackoverflow Q&A if you need to save the plot with ggsave() and have the brackets persist in the saved image.


The OP requested the bracket be off the plot. This solution uses axis.ticks.length in combination with axis.ticks = element_blank() to allow the brace to be outside the plotting area. This answer builds upon those of @Pankil and @user697473: we will use pBrackets R package -- and include pictures!

library(ggplot2)
library(grid)
library(pBrackets) 
x <- c(runif(10),runif(10)+2)
y <- c(runif(10),runif(10)+2)
the_plot <- qplot(x=x,y=y) +
  scale_x_continuous("",breaks=c(.5,2.5),labels=c("Low types","High types") ) +
  theme(axis.ticks = element_blank(),
        axis.ticks.length = unit(.85, "cm"))


#Run grid.locator a few times to get coordinates for the outer
#most points of the bracket, making sure the 
#bottom_y coordinate is just at the bottom of the gray area.
# to exit grid.locator hit esc; after setting coordinates
# in grid.bracket comment out grid.locator() line
the_plot
grid.locator(unit="native") 
bottom_y <- 284
grid.brackets(220, bottom_y,   80, bottom_y, lwd=2, col="red")
grid.brackets(600, bottom_y,  440, bottom_y, lwd=2, col="red")

enter image description here

A quick note on @Pankil's answer:

## Bracket coordinates depend on the size of the plot
## for instance,
## Pankil's suggested bracket coordinates do not work
## with the following sizing:
the_plot
grid.brackets(240, 440, 50, 440, lwd=2, col="red")
grid.brackets(570, 440, 381, 440, lwd=2, col="red")
## 440 seems to be off the graph...

enter image description here

And a couple more to showcase functionality of pBrackets:

#note, if you reverse the x1 and x2, the bracket flips:
the_plot
grid.brackets( 80, bottom_y, 220, bottom_y, lwd=2, col="red")
grid.brackets(440, bottom_y, 600, bottom_y, lwd=2, col="red")

enter image description here

## go vertical:
the_plot
grid.brackets(235, 200, 235, 300, lwd=2, col="red")
grid.brackets(445, 125, 445,  25, lwd=2, col="red")

enter image description here

swihart
  • 2,648
  • 2
  • 18
  • 42
  • Yes but how can you save this? ggsave won't add the brackets – SamanthaDS Feb 25 '16 at 16:20
  • Nice answer. But it would benefit from an explanation on how to export the plot to a PDF. I've managed to save it to PNG via RStudio's Export button, but failed with the usual solutions like ``ggsave(...)``, ``pdf(...)...dev.off()`` Maybe some function from the ``grid`` package is designed for that, like ``gridArrange()``, but I couldn't quickly find a solution. – PatrickT Mar 28 '18 at 16:33
  • Oh wait: https://stackoverflow.com/questions/35633239/add-curly-braces-to-ggplot2-and-then-use-ggsave – PatrickT Mar 28 '18 at 16:37
  • PatrickT -- I've added the SamanthaDS post that you linked to the answer. Thank you both. – swihart Mar 28 '18 at 19:45
  • `grid.brackets()` do not seem to start and stop where the `x1` `y1` `x2` `y2` parameters tell them to! (they look very good, just can't get them to where they're supposed to be) – stevec May 09 '20 at 20:49
12

Here is kludgy solution in ggplot that constructs a line drawing that vaguely resembles a curly bracket.

Construct a function that takes as input the position and dimensions of a curly bracket. What this function does is to specify the co-ordinates of an outline drawing of a bracket and uses some math scaling to get it to the size and position desired. You can use this principle and modify the co-ordinates to give you any desired shape. In principle you can use the same concept and add curves, ellipses, etc.

bracket <- function(x, width, y, height){
  data.frame(
      x=(c(0,1,4,5,6,9,10)/10-0.5)*(width) + x,
      y=c(0,1,1,2,1,1,0)/2*(height) + y
  )
}

Pass that to ggplot and specifically geom_line

qplot(x=x,y=y) +
    scale_x_continuous("",breaks=c(.5,2.5), labels=c("Low types","High types")) +
    geom_line(data=bracket(0.5,1,0,-0.2)) +
    geom_line(data=bracket(2.5,2,0,-0.2))

enter image description here

Andrie
  • 176,377
  • 47
  • 447
  • 496
  • 1
    Nice. You could combine with the answer here to move the braces closer to the axis: http://stackoverflow.com/questions/7001710/how-can-i-force-ggplots-geom-tile-to-fill-every-facet/7001873#7001873 – Ari B. Friedman Aug 09 '11 at 21:04
9

as @user697473 suggested pBrackets is the elegant solution.

It works best with the default plotting commands but to make it work with GGplot2 use the pBracket::grid.brackets. I'm including the code to make it easy for try it out.

Starting with your code..

library(ggplot2)
x <- c(runif(10),runif(10)+2)
y <- c(runif(10),runif(10)+2)
qplot(x=x,y=y) +
  scale_x_continuous("",breaks=c(.5,2.5),labels=c("Low types","High types") ) +
  theme(axis.ticks = element_blank())

the last line removes the ticks you didn't want.
Now the pBrackets

library(pBrackets)  # this will also load grid package
grid.locator(unit="native") 

now using your cursor identify the point on the graph where the brackets start and end. You will get the corresponding coordinates in the 'native' unit. now feed them in the command below

grid.brackets(240, 440, 50, 440, lwd=2, col="red")
grid.brackets(570, 440, 381, 440, lwd=2, col="red")

You can add the brackets anywhere on the graph or even add text using grid.text.

enter image description here

Hope this helps! Thank you pBrackets!

Pankil!

PatrickT
  • 10,037
  • 9
  • 76
  • 111
Pankil Shah
  • 1,090
  • 12
  • 9
4

The new pbrackets package may help: http://cran.r-project.org/web/packages/pBrackets/index.html.

user697473
  • 2,165
  • 1
  • 20
  • 47
2

#== EDIT: ggbrace replaced the curlyBraces package

This answer is really late, but for braces inside the plotting area I made a small package ggbrace that lets you have curly braces with the coordinates you specify (easy to use):

basically:

devtools::install_github("NicolasH2/ggbrace")
library(ggbrace)
library(ggplot2)

you can customize x and y coordinates and where the bracket points to. In this specific case:

x <- c(runif(10),runif(10)+2)
y <- c(runif(10),runif(10)+2)

qplot(x=x,y=y) +
  scale_x_continuous("",breaks=c(.5,2.5),labels=c("Low types","High types") ) + 
  geom_brace(aes(x=c(0,1), y=c(0,-.3)), inherit.data=F, rotate=180) + 
  geom_brace(aes(x=c(2,3), y=c(0,-.3)), inherit.data=F, rotate=180)

enter image description here

EDIT: If you want the braces outside of the plotting area, you can set some ggplot parameters. It is described in a little more detail on the ggbrace github page.

qplot(x=x,y=y) +
  geom_brace(aes(x=c(0,1), y=c(-.4,-.7), label = "Low types"), inherit.data=F, rotate=180, labelsize=5) + 
  geom_brace(aes(x=c(2,3), y=c(-.4,-.7), label = "High types"), inherit.data=F, rotate=180, labelsize=5) +
  coord_cartesian(y=range(y), clip = "off") +
  theme(plot.margin = unit(c(0.05, 0.05, 0.2, 0.05), units="npc"))

enter image description here

NicolasH2
  • 774
  • 5
  • 20