75

I'd like to place a black border around points on a scatterplot that are filled based on data, using ggplot2. Also, I would like to avoid having a legend entry for the black border since it will be on each point. Basically I'm looking for this plot, but with a black border around each point.

df <- data.frame(id=runif(12), x=1:12, y=runif(12))
ggplot(df, aes(x=x, y=y))+geom_point(aes(colour=id), size=12)

the plot I want, except it doesn't have borders around the points

As a bonus, I'd like to not have a legend entry for the black border. My best try is:

df <- data.frame(id=runif(12), x=1:12, y=runif(12))
ggplot(df, aes(x=x, y=y))+geom_point(aes(fill=id, colour="black"), size=12)

Which gives:

not at all the plot I want

I don't understand why that doesn't give me what I want, and worse (for my education in ggplot2) I don't understand why it doesn't seem to map fill color to anything! Any help?

Perhaps if I can get the outline and fill mapping right I can use a hack like the one in hte last set of figures here to turn off the legend.

Paul Hiemstra
  • 59,984
  • 12
  • 142
  • 149
Drew Steen
  • 16,045
  • 12
  • 62
  • 90

5 Answers5

131

It's a bit obscure, but you have to use pch>20 (I think 21:25 are the relevant shapes): fill controls the interior colo(u)ring and colour controls the line around the edge.

(g0 <- ggplot(df, aes(x=x, y=y))+geom_point(aes(fill=id), 
       colour="black",pch=21, size=5))

update: with recent ggplot2 versions (e.g. 2.0.0, don't know how far back it goes) the default guide is a colourbar. Need g0 + guides(fill="legend") to get a legend with points as in the plot shown here. The default breaks have changed, too: to exactly replicate this plot you need g0 + scale_fill_continuous(guide="legend",breaks=seq(0.2,0.8,by=0.1)) ...

enter image description here

Related but not identical: how to create a plot with customized points in R? . The accepted answer to that question uses the layering technique shown in @joran's answer, but (IMO) the answer by @jbaums, which uses the pch=21 technique, is superior. (I think shape=21 is an alternative, and perhaps even preferred, to pch=21.)

PS you should put colour outside the mapping (aes bit) if you want to set it absolutely and not according to the value of some variable ...

Community
  • 1
  • 1
Ben Bolker
  • 211,554
  • 25
  • 370
  • 453
34

The first question's a gimme:

ggplot(df, aes(x=x, y=y)) + 
    geom_point(aes(colour=id), size=12) + 
    geom_point(shape = 1,size = 12,colour = "black")

And, oh, you don't want an extra legend. I think that does it then:

enter image description here

joran
  • 169,992
  • 32
  • 429
  • 468
  • Interestingly a guide doesn't show up for the second geom_point - I don't really understand why that is, but since it is what I wanted anyway I'll worry about it another day – Drew Steen May 03 '12 at 18:49
  • 1
    Oh, I see why - because it is not inside an aes() call – Drew Steen May 03 '12 at 19:02
  • @DrewSteen - looks like you got it, I asked a similar question a while back since it took me a while to figure that out too. I'm not sure if/what would be made out of date by the introduction of ggplot 0.9: http://stackoverflow.com/questions/4221168/ggplot-legend-issue-w-geom-point-and-geom-text – Chase May 03 '12 at 19:52
  • 1
    This is nice, but fails if you're using jitter. – Roman Luštrik Oct 01 '12 at 21:36
  • @RomanLuštrik You are right. Is there a solution? I'd like to do do something similar using geom_jitter. – Seanosapien Oct 19 '16 at 16:29
  • 2
    @Seanosapien sure, just "jitter" the points manually (transform the values in `df`) using either `rnorm` or `jitter`. You may want to store these values in a new variable in case you need raw values downstream. – Roman Luštrik Oct 20 '16 at 08:52
6

I had the same issue, but I needed a solution that allows for jitter, too. For this you do need to use a pch that is a filled shape with a border and a grid.edit function from gridExtra package. Using your example:

df <- data.frame(id=runif(12), x=1:12, y=runif(12))
ggplot(df, aes(x=x, y=y, fill=id))+geom_point(pch=21, colour="Black", size=12)

library(gridExtra)
grid.edit("geom_point.points", grep = TRUE, gp = gpar(lwd = 3))
alexo4
  • 351
  • 3
  • 8
6

I had the same question, but perhaps since I was using geom_map with latitudes and longitudes, the other answers as of January 2020 didn't work for me.

Restating the question, where the following does not have a black outline around the points:

df <- data.frame(id=runif(12), x=1:12, y=runif(12))
ggplot(df, aes(x=x, y=y))+geom_point(aes(colour=id), size=12) 

If I declared both the color and fill in the aesthetic and then used shape 21, problem solved.

ggplot(df, aes(x=x, y=y)) + 
  geom_point(aes(colour=id, fill=id), 
  shape = 21,size = 12,colour = "black")

Points Outlined in Black

windyvation
  • 497
  • 3
  • 13
  • Hi @windyvation what if my data is plotted so the size depends on a variable, how can I add a black outline to the circles? My plot would be like this `ggplot(data, aes(x = Region, y = func, label=NA)) + geom_point(aes(size = scaled_val, colour = value), shape = 21, colour = "black") + geom_text(hjust = 1, size = 2) + theme_bw()+ scale_color_gradient(low = "lightblue", high = "darkblue")` – Ecg Apr 22 '20 at 17:22
  • 1
    @Ecg modifying the code above, the following works for variable size: library(tidyverse) size_x <- 1:12 df <- data.frame(id=runif(12), x=1:12, y=runif(12), size = size_x) ggplot(df, aes(x=x, y=y)) + geom_point(aes(colour=id, fill=id), shape = 21,size = size_x,colour = "black") – windyvation Apr 23 '20 at 23:09
0

If you want more control (for example, borders on points with various shapes and transparencies), use the fill aesthetic with shapes 21:25

ggplot(aes(x = Sepal.Length, y = Petal.Width, fill = Species, shape = Species), data = iris) + # notice: fill
    geom_point(size = 4, alpha = 0.5) + # transparent point
    geom_point(size = 4, fill = NA, colour = "black") + # black border
    scale_shape_manual(values = c(21:23)) + # enable fill aesthetic
    theme_classic()
Jeff Bezos
  • 1,929
  • 13
  • 23