Avoid drawing lines inside of shapes?

386 views
Skip to first unread message

Winston Chang

unread,
May 3, 2009, 7:22:05 PM5/3/09
to ggplot2
Dear ggplot2 experts,
When making a graph with both lines and points, is there a way to have the lines not be drawn inside the points, or stop short of touching the points? I have a plot with hollow points and a heavy line, and it is a bit difficult to see the shape. Also, with the heavy line, it is also a bit hard to distinguish between hollow and filled shapes without making the shapes very large.

Here's an example showing the hollow/filled issue.

df <- structure(list(subject = structure(c(1L, 1L, 1L, 1L, 1L, 2L,
2L, 2L, 2L, 2L), .Label = c("A", "B"), class = "factor"), xval = c(1,
2, 3, 4, 5, 1, 2, 3, 4, 5), yval = c(1.2, 2, 2.5, 2.2, 2, 2,
3, 2, 1.5, 1)), .Names = c("subject", "xval", "yval"), row.names = c(NA,
10L), class = "data.frame")

qplot(xval, yval, data=df, geom=c("point"), shape=subject, size=I(3)) +
    scale_shape_manual(value = c(1,19)) +
    geom_line(size=2)



point_line-2.png



hadley wickham

unread,
May 3, 2009, 7:30:58 PM5/3/09
to Winston Chang, ggplot2
You could just draw big white points in the background:

ggplot(df, aes(xval, yval, group = subject)) +
geom_line(size = 2) +
geom_point(colour = "white", size = 5, legend = T) +
geom_point(aes(shape = subject), size = 3) +
scale_shape_manual(value = c(1,19))

This looks pretty good with the default shapes too:

ggplot(df, aes(xval, yval, shape = subject)) +
geom_line(size = 2) +
geom_point(colour = "white", size = 5, legend = T) +
geom_point(size = 3)

Hadley
--
http://had.co.nz/

Winston Chang

unread,
May 3, 2009, 8:56:43 PM5/3/09
to hadley wickham, ggplot2
Of course! It seems totally obvious now that you've said it.
Thanks,
-Winston

Winston Chang

unread,
Jun 2, 2009, 3:29:23 PM6/2/09
to hadley wickham, ggplot2
Revisiting the issue of having lines not go inside hollow points... In an ideal world, there are three issues that I'd like to do:
1. Control the thickness of the point outlines
2. Have simple, understandable code (the underplotting method here is a bit hacky)
3. Have the hollowed-out part be transparent, so that you can see gridlines through it

============
I'm guessing that 3 probably won't be addressed, because it requires a bit of interaction between the point and line geoms. It's also less important, at least to me. One possible solution would be to add an option to the line geom to have it avoid the target coordinates by some radius.

============
Regarding 2, I just realized that using shapes 21-25 lets you easily have an outline with a white fill. It won't allow you to make the line stop short of the points, but that is less important to me. Here's an example:
df <- read.table(header=T, con <- textConnection('
  subject xval yval
       A    1  2.0
       A    2  2.5
       B    1  3.0
       B    2  2.0
'))
close(con)


ggplot(df, aes(xval, yval, group = subject)) +
 geom_line(size = 2) +
 geom_point(aes(shape = subject), fill="white", size = 4) +
 scale_shape_manual(value = c(21,22))

One limitation is that the outlines on large points is quite thin.

============
Finally, for 1, I couldn't figure out a simple way to change the thickness of the point outlines. I tried plotting a solid point with size 4, then overplotting with a white point of size 2, but it doesn't center properly with solid point types 15-18.


ggplot(df, aes(xval, yval, group = subject)) +
 geom_line(size = 2) +
 geom_point(aes(shape = subject), size = 5) +
 geom_point(aes(shape = subject), colour="white", size = 2) +
 scale_shape_manual(value = c(15,17))

Update (I just figured this out as I wrote it): I think the centering problem for me was because shapes 15-18 aren't properly antialiased on my system (Ubuntu Linux, R 2.8.1). Using shapes 21-25 results in proper centering, but you need to specify the fill to be the same as colour; otherwise they'll render as outlines only.


ggplot(df, aes(xval, yval, group = subject)) +
 geom_line(size = 2) +
 geom_point(aes(shape = subject), fill="black", colour="black", size = 5) +
 geom_point(aes(shape = subject), fill="white", colour="white", size = 2) +
 scale_shape_manual(value = c(22,24))


So on the whole, this seems like an OK solution for issue 1, but it does introduce a bit more complexity and inelegance to the code. If there were a way to directly control line thickness (as can be done with the standard R plotting routines), that would be better. Thoughts?

I hope the attached pictures show up in the proper order...

-Winston



On Sun, May 3, 2009 at 6:30 PM, hadley wickham <h.wi...@gmail.com> wrote:
pch21_22.png
overplot15_17.png
overplot22_24.png

baptiste auguie

unread,
Jun 2, 2009, 5:05:32 PM6/2/09
to Winston Chang, ggplot2
if you feel like experimenting, you could try the polygon geom of the
ggextra package. I've just experimented the addition of an "lex"
argument (plot attached),

http://code.google.com/p/ggextra/source/checkout for the svn code
http://ggextra.googlecode.com/files/ggextra_0.5.tar.gz for the Mac binary
# fingers crossed, the package will build overnight
# install.packages("ggextra", repos="http://R-Forge.R-project.org")


d <- read.table(header=T, con <- textConnection('
subject xval yval
A 1 2.0
A 2 2.5
B 1 3.0
B 2 2.0
'))
close(con)


p <-
ggplot(d, aes(xval, yval, group = subject)) +
geom_line(size = 2)
library(ggextra)
p1 <-
p + geom_ngon(aes(sides = subject), size=5, fill="white", lex=1)
p2 <-
p + geom_ngon(aes(sides = subject), size=5, fill="white", lex=5)

arrange(p, p1, p2, ncol=3)


It would probably be as easy to add a lex parameter to the current
geoms of ggplot2, but Hadley may have found arguments against doing so
at the time, i dunno. (well, proliferation of parameters comes to
mind).

Let me know what you think of this,

baptiste
--
_____________________________

Baptiste Auguié

School of Physics
University of Exeter
Stocker Road,
Exeter, Devon,
EX4 4QL, UK

http://newton.ex.ac.uk/research/emag
______________________________
Picture 2.png

Winston Chang

unread,
Jun 2, 2009, 5:39:04 PM6/2/09
to baptiste auguie, ggplot2
Baptiste -
This looks promising... thanks for making it and pointing it out!

For me, the lex argument doesn't seem to make the line thicker -- I guess that's because the new build isn't up yet? (If you add instructions for building/installing from SVN, that would be helpful -- I figured out once how to build+install an R package from source, but I've since forgotten.)

A couple suggestions:
- It would be nice to have sharp corners instead of rounded ones (although I understand that coding the proper mitering is non-trivial)
- Circles would also be nice, although this may conflict with the idea of having n-gons.

As for using it as a general-purpose replacement for geom_point, I think it might not be ideal for typical line+point graphs. This is because, for most such graphs, the points just need to be easily distinguishable; and so circles, squares, triangles, crosses, and so on are fine. With n-sided polygons, they're not obviously different (for me) once you get up to 5 or 6 sides.

-Winston

baptiste auguie

unread,
Jun 3, 2009, 4:34:30 AM6/3/09
to Winston Chang, ggplot2
On Tue, Jun 2, 2009 at 11:39 PM, Winston Chang <win...@stdout.org> wrote:
> Baptiste -
> This looks promising... thanks for making it and pointing it out!
>

I'm wondering whether it wouldn't be fairly doable to create a new
geom to mimic the type='b' of base graphics. As far as I understand
from main/plot.c it's only a matter of chopping up the segments to
make an exclusion zone around the points. Any takers?

> For me, the lex argument doesn't seem to make the line thicker -- I guess
> that's because the new build isn't up yet? (If you add instructions for
> building/installing from SVN, that would be helpful -- I figured out once
> how to build+install an R package from source, but I've since forgotten.)

install.packages("ggextra",
repos="http://R-Forge.R-project.org",type="source") seems to work this
morning, but the Mac binary doesn't (yet?). I'll never understand
this!

If you're on linux / Mac svn is fairly trivial: install it, then open
up a terminal window, type:

svn checkout http://ggextra.googlecode.com/svn/trunk/ ggextra-read-only

and you should obtain the full directory. You can install it by
running (command line again),

R CMD INSTALL .

There are some further instructions on http://r-forge.r-project.org/ .

>
> A couple suggestions:
> - It would be nice to have sharp corners instead of rounded ones (although I
> understand that coding the proper mitering is non-trivial)

done -- it seems to be the argument linejoin='mitre' in gpar(). At
least it works for polygons, I'm not sure about other symbols.

> - Circles would also be nice, although this may conflict with the idea of
> having n-gons.

ngon offers an approximation of a circle for sides > 8 (50 sides), but
it's clearly not ideal.

>
> As for using it as a general-purpose replacement for geom_point, I think it
> might not be ideal for typical line+point graphs.

i never envisaged to use geom_ngon routinely, and this is for a
variety of reasons. One of them being, as you point out, the limited
number of visually distinguishable shapes. Another is the efficiency
(although I'm quite pleasantly surprised so far). But most important
is the experimental character of this geom: I have run at most 10
different test cases, while the ggplot2 geoms need to work in all
kinds of situations, most of which I haven't even thought of.

This is because, for most
> such graphs, the points just need to be easily distinguishable; and so
> circles, squares, triangles, crosses, and so on are fine.

Although I agree on this particular point, I'm not so keen on the base
symbols: they appear to me as quite random in terms of order,
duplication, size, parameters (fill / no fill). (see a recent post on
the size of point symbols).

With n-sided
> polygons, they're not obviously different (for me) once you get up to 5 or 6
> sides.

Agreed, I set the limit to 8, plus 50 for a fake circle (ideally that
number would depend on the size...)

As for your request of setting the linewidth, I've tested a minor
change to geom_point() that I renamed geom_point2. The only change is
the addition of the gpar argument lex. Seems to work for me. (this
one, you'll need the svn version as I just uploaded it).


Cheers,

baptiste

baptiste auguie

unread,
Jun 3, 2009, 6:06:28 AM6/3/09
to ggplot2
On Wed, Jun 3, 2009 at 10:34 AM, baptiste auguie
<bapt...@googlemail.com> wrote:


> I'm wondering whether it wouldn't be fairly doable to create a new
> geom to mimic the type='b' of base graphics. As far as I understand
> from main/plot.c it's only a matter of chopping up the segments to
> make an exclusion zone around the points.


Here's a start:

barbedGrob <- function(x = stats::runif(10), y = stats::runif(10),
size=1, shape=1,
colour="red", fill="blue", alpha=0.8,
linetype=1, lex=1){

start <- 1
end <- length(x)

grob.lines <- segmentsGrob(
x[-end], y[-end], x[-start], y[-start],
default.units="native",
gp = gpar(
col = alpha(colour, alpha),
lex = lex, lty = linetype, lineend = "butt"
)
)

grob.points <- pointsGrob(x, y, pch=shape, size=size*unit(1, "char"),
gp = gpar(
col = alpha(colour, alpha),
fill = alpha(fill, alpha),
lex = lex, linejoin = "mitre"
)
)

gTree(children = gList(grob.lines,grob.points))

}

g <- barbedGrob(shape=21, size=3, fill="blue", lex=2)
pushViewport(vp=viewport(width=1, height=1))
grid.draw(g)


Now, the fun part will be to chop the segments...

baptiste auguie

unread,
Jun 3, 2009, 3:25:18 PM6/3/09
to ggplot2
As for chopping, I've come up with the following hack,

x <- c(-0.2, 0.8) # only dealing with one segment so far
y <- c(0.2, 0.5) # (Any takers to adapt the code for any number of points?)

pushViewport(vp=viewport(width=1, height=1,
xscale = c(-1, 1), yscale = c(-1, 1)))
grid.points(x = x,
y = y,
pch = 21, size = unit(1, "char"),
default.units = "native", name = NULL,
gp = gpar(col="red", fill=alpha("green", 0.5)), draw = TRUE)


dx <- diff(x)
dy <- diff(y)

length <- sqrt(dx^2 + dy^2)
exclusion <- convertX(unit(1.5, "char"), "npc", TRUE) # in real world,
needs to be a vector for each point can have a different size
scaling <- exclusion / length

# left and right don't mean much but, hey, one needs directions
x.left <- scaling * dx + x[1]
y.left <- scaling * dy + y[1]

x.right <- x[2] - scaling * dx
y.right <- y[2] - scaling * dy

grid.segments(x0 = x.left, y0 = y.left,
x1 = x.right, y1=y.right,
default.units = "native", name = NULL,
gp = gpar(col="blue", lex=4), draw = TRUE)


Let me know what you think,

baptiste

--

Winston Chang

unread,
Jun 3, 2009, 6:03:56 PM6/3/09
to baptiste auguie, ggplot2

As for your request of setting the linewidth, I've tested a minor
change to geom_point() that I renamed geom_point2. The only change is
the addition of the gpar argument lex. Seems to work for me. (this
one, you'll need the svn version as I just uploaded it).

Thanks for adding this! 'lex' works well. Comments:
- Legends don't draw with the new linewidth.
- It appears to use different units than 'size' does for line thickness and point diameter. For example, a line with size=2 looks about the same as a point with lex=5. In my opinion, it would be good to use the same units, even if that means that the default for lex will be a small non-integer. If they're in the same units, then it will be simpler to make graphs with consistent line and point outline widths.
- Perhaps lex isn't the best name? The other aesthetic names in ggplot2 use normal language.

library(ggextra)
df <- data.frame(x=1:5, y=rnorm(5))

ggplot(df, aes(x=x, y=y)) +
    geom_line(size=2) +
    geom_point2(shape=21, fill="white", size=5, lex=5)
 

lextest.png


A final suggestion for Hadley - it would be great if this (and some of Baptiste's other additions) could be added to ggplot2 proper at some point.

-Winston
lextest.png

Winston Chang

unread,
Jun 3, 2009, 6:09:28 PM6/3/09
to baptiste auguie, ggplot2
Oops, that was the wrong code for the image I posted. It should be this:

library(ggextra)
df <- data.frame(x=c(1:4, 4.05, 5),
                  y=c(0, 1.2, 1.7, .8, .85, 2),
                  z=c("A","A","A","A","B","B"))

ggplot(df, aes(x=x, y=y)) +
    geom_line(aes(colour=z),size=2) +
    geom_point2(aes(colour=z), shape=21, fill="white", size=5, lex=5)


lextest.png


lextest.png

baptiste auguie

unread,
Jun 4, 2009, 7:29:52 AM6/4/09
to ggplot2
I've come up with the following draft for a new geom. What do you think?
Useful? Improvements / optimisation? Should it use polylines rather than
segments? ...

Best,

baptiste


barbedGrob <- function(x = stats::runif(10),
y = stats::runif(10),

size=abs(rnorm(10, mean=1)), shape=21,


colour="red", fill="blue", alpha=0.8,

linetype=1, lex=2){

dx <- diff(x)
dy <- diff(y)

new.x <- rep(x, each=2)[-c(1, 2*length(x))]
new.y <- rep(y, each=2)[-c(1, 2*length(y))]
new.size <- rep(size, each=2)[-c(1, 2*length(size))]

length <- sqrt(dx^2 + dy^2)

exclusion <- 0.5*convertX(unit(new.size, "char"), "npc", TRUE)

scaling <- exclusion / rep(length, each=2)

start <- seq(1, by=2, length(new.x))
end <- seq(2, by=2, length(new.x))

x.start <- scaling[start] * dx[(start+1)/2] + new.x[start]
y.start <- scaling[start] * dy[(start+1)/2] + new.y[start]

x.end <- new.x[end] - scaling[end] * dx[end/2]
y.end <- new.y[end] - scaling[end] * dy[end/2]

grob.lines <- segmentsGrob(
x0 = x.start, y0 = y.start,
x1 = x.end, y1=y.end,


default.units="native",
gp = gpar(
col = alpha(colour, alpha),
lex = lex, lty = linetype, lineend = "butt"
)
)

grob.points <- pointsGrob(x, y, pch=shape, size=unit(size, "char"),


gp = gpar(
col = alpha(colour, alpha),
fill = alpha(fill, alpha),
lex = lex, linejoin = "mitre"
)
)

gTree(children = gList(grob.lines,grob.points))

}


barbedGrob() -> g
pushViewport(vp=viewport(width=1, height=1))
grid.draw(g)


bapt4510.vcf

baptiste auguie

unread,
Jun 4, 2009, 11:24:24 AM6/4/09
to ggplot2

On Thu, Jun 4, 2009 at 12:03 AM, Winston Chang <win...@stdout.org> wrote:

> Thanks for adding this! 'lex' works well. Comments:
> - Legends don't draw with the new linewidth.

I noticed this too, but I couldn't pinpoint the origin of this issue.
I'm afraid that's as far as my little experiments have taught me about
ggplot2.

> - It appears to use different units than 'size' does for line
thickness and
> point diameter. For example, a line with size=2 looks about the same as a
> point with lex=5. In my opinion, it would be good to use the same units,
> even if that means that the default for lex will be a small
non-integer. If
> they're in the same units, then it will be simpler to make graphs with
> consistent line and point outline widths.

agreed -- but units are a somewhat iffy business for me. I think I've
got it sorted now but I'll have to check.

> - Perhaps lex isn't the best name? The other aesthetic names in
ggplot2 use
> normal language.
>

I agree, but I have a problem with the name 'size' when used for the
line thickness of some geoms. I'm still not sure adding a scale for this
parameter made much sense in the first place.

>
> A final suggestion for Hadley - it would be great if this (and some of
> Baptiste's other additions) could be added to ggplot2 proper at some
point.
>

well, I won't reply for Hadley but IMHO any such experimental feature
should probably be considered a bit like community packages vs. base R:
any proposed 'improvement' should first meet strong criteria before
being added to the main package,

- high standards (aesthetics, conformity with other pieces of code,
legibility, examples, doc, efficiency, ... ), and reliability
- thorough testing (hence a necessary delay before adding new features)
- clear usefulness / improvement

None of the current code of ggextra meets any of these points when
checked against the standards of ggplot2. That situation will hopefully
improve with time (and contributors, should more people join in).

What's more, ggplot2 seems quite a big piece of code already (to me
anyway), so it's probably best not to add too many features too soon
(again, imho). That's part of the idea behind my placing this
'experimental add-ons' package on r-forge: make it as easy to test new
ideas as loading a small package.

It's very clear that Hadley listens to everybody's suggestions on this
list, so 'm sure he'll pick up any ideas worth exploring further.

Cheers,

baptiste

hadley wickham

unread,
Jun 8, 2009, 11:24:01 AM6/8/09
to baptiste auguie, ggplot2
On Thu, Jun 4, 2009 at 6:29 AM, baptiste auguie<bapt...@googlemail.com> wrote:
> I've come up with the following draft for a new geom. What do you think?
> Useful? Improvements / optimisation? Should it use polylines rather than
> segments? ...

I think segments are fine. But it currently resembles more of a
ggplot2 function than a grid function:

* you should probably use pch, cex, etc, and use gpar
* you should be able to specify units for x and y (and have a
default.units parameter)

Hadley

--
http://had.co.nz/

hadley wickham

unread,
Jun 8, 2009, 11:28:48 AM6/8/09
to baptiste auguie, ggplot2
>  > - It appears to use different units than 'size' does for line
> thickness and
>  > point diameter. For example, a line with size=2 looks about the same as a
>  > point with lex=5. In my opinion, it would be good to use the same units,
>  > even if that means that the default for lex will be a small
> non-integer. If
>  > they're in the same units, then it will be simpler to make graphs with
>  > consistent line and point outline widths.
>
> agreed -- but units are a somewhat iffy business for me. I think I've
> got it sorted now but I'll have to check.

The default R units are in points - multiply your unit in millimeters
by ggplot2:::.pt to convert to points.

>  > - Perhaps lex isn't the best name? The other aesthetic names in
> ggplot2 use
>  > normal language.
>  >
>
> I agree, but I have a problem with the name 'size' when used for the
> line thickness of some geoms. I'm still not sure adding a scale for this
> parameter made much sense in the first place.

Size is currently used for line thickness of any thing with a fill,
and the size of points. One possibility would be to switch to using
linewidth for all line widths. Another would be to have size2 (for
secondary size) which for points would control the line width. Scales
do start to get complicated though, as you'll need to have a different
scale for the line thickness of the point and the size of the point.
Personally, I still prefer the technique of drawing two points in
different colours.

>  > A final suggestion for Hadley - it would be great if this (and some of
>  > Baptiste's other additions) could be added to ggplot2 proper at some
> point.
>  >
>
> well, I won't reply for Hadley but IMHO any such experimental feature
> should probably be considered a bit like community packages vs. base R:
> any proposed 'improvement' should first meet strong criteria before
> being added to the main package,

I agree :) I definitely do pick up smaller features and include them,
but larger experimental features like this need more thought.

> What's more, ggplot2 seems quite a big piece of code already (to me
> anyway), so it's probably best not to add too many features too soon
> (again, imho). That's part of the idea behind my placing this
> 'experimental add-ons' package on r-forge: make it as easy to test new
> ideas as loading a small package.

This seems like the right model to me too.

Hadley


--
http://had.co.nz/

baptiste auguie

unread,
Jun 9, 2009, 4:49:01 AM6/9/09
to ggplot2
good points, I've put a new version on the r-wiki,

http://econum.umh.ac.be/rwiki/doku.php?id=tips:graphics-grid:linesandpointsgrob

I'm not clear why some gpar() such as pch and size are outside the gp
argument of pointsGrob(), while others are inside. Let me know if you
see some inconsistency in my new version.

Also, as far as a ggplot geom is concerned, it might be that the points
should remain a separate geom (another layer). That way, one can control
the appearance of lines and points separately (further, one could use
different geoms such as error bars, stars, ellipses, ...). I've added a
switch only.lines=TRUE for this purpose, which draws only the portions
of segments.

Best,

baptiste

baptiste auguie

unread,
Jun 9, 2009, 5:13:42 AM6/9/09
to ggplot2
hadley wickham wrote:
>> > - It appears to use different units than 'size' does for line
>> thickness and
>> > point diameter. For example, a line with size=2 looks about the same as a
>> > point with lex=5. In my opinion, it would be good to use the same units,
>> > even if that means that the default for lex will be a small
>> non-integer. If
>> > they're in the same units, then it will be simpler to make graphs with
>> > consistent line and point outline widths.
>>
>> agreed -- but units are a somewhat iffy business for me. I think I've
>> got it sorted now but I'll have to check.
>>
>
> The default R units are in points - multiply your unit in millimeters
> by ggplot2:::.pt to convert to points.
>
>
Yep, that's what i figured.

>> > - Perhaps lex isn't the best name? The other aesthetic names in
>> ggplot2 use
>> > normal language.
>> >
>>
>> I agree, but I have a problem with the name 'size' when used for the
>> line thickness of some geoms. I'm still not sure adding a scale for this
>> parameter made much sense in the first place.
>>
>
> Size is currently used for line thickness of any thing with a fill,
> and the size of points. One possibility would be to switch to using
> linewidth for all line widths. Another would be to have size2 (for
> secondary size) which for points would control the line width. Scales
> do start to get complicated though, as you'll need to have a different
> scale for the line thickness of the point and the size of the point.
>

Personally, I'd go for the first option --- linewidth for all line
widths. But that's not something I could do in this add-ons package as
it's a global change throughout ggplot2's code.


> Personally, I still prefer the technique of drawing two points in
> different colours.
>

I think a real type = 'b' geom has its value, especially when the data
is quite dense: you don't want the large white points to mask
surrounding lines or other layers unnecessarily. (not that I've ever
used type= 'b' myself, but I can see situations where it might be good).
Perhaps the new argument only.lines=TRUE could provide a better compromise.

Best,

baptiste

--

baptiste Auguié

http://www.bauguie.com/bapt/

Reply all
Reply to author
Forward
0 new messages