Base plot()
functionality allows one to set type='b'
and get a combined line and point plot in which the points are offset from the line segments
plot(pressure, type = 'b', pch = 19)
I can easily create a ggplot with lines and points as follows.
ggplot(pressure, aes(temperature, pressure)) +
geom_line() +
geom_point()
The lines, however, run right up to the points. I can envision a way that I might hack together something like type='b'
functionality using other geoms (e.g. geom_segment()
?), but I am wondering if there is a more direct way to accomplish this with geom_line()
and geom_point()
.
One option which is less hacky than manually matching the stroke color with the panel background is to get the panel background beforehand, either from
theme_get
for the default theme, or with a specific theme that you'll be using. Using a stroked shape like21
lets you make the inner circle black and the stroke the same color as the background.A couple SO questions (here's one) deal with the math behind shortening segments between points. It's simple but tedious geometry. But in the time since this question was first posted, the
lemon
package has come out, which has a geom to do this. It's got arguments for how to calculate the shortening, which probably require just some simple tweaking.I'm sorry for answering twice, but this seems sufficiently different to merit a different answer.
I've given this question some more thought and I'll concede that a geometric approach is indeed the better approach over the point-over-point approach. However, the geometric approach comes with its own set of problems, namely that any attempt at pre-computing coordinates before draw-time is going to give you some skew in one way or another (see a follow up question from @Tjebo).
It is next to impossible to know the aspect ratio or exact sizes of the plot a priori, except by setting an aspect ratio manually or using the
space
argument offacet_grid()
. Because this is impossible, any precomputed set of coordinates is going to be inadequate if the plot is resized.I've shamelessly stolen some good ideas from other people, so thanks to @Tjebo and @moody_mudskipper for the maths and credit to ggplot guru thomasp85 and the ggforce package for the calculating at drawtime inspiration.
On with it; first we'll define our ggproto as before, now making a custom grob class for our path. An important detail is that we convert our xy coordinates to formal units.
Through the magic of object oriented programming, we can now write a new method for our new grob class. While that may be uninteresting in and of itself, it gets particularly interesting if we write this method for
makeContent
, which is called every time a grob is drawn. So, let's write a method that invokes the mathematical operations on the exact coordinates the graphics device is going to use:Now all we need is a layer wrapper like before, which does nothing special:
The demonstration:
And this should be stable for any resized aspect ratio. You can supply
aes(mult = ...)
or justmult = ...
to control the size of the gaps between segments. By default it is proportional to the point sizes, so varying the point size while keeping the gap contant is a challenge. Segments that are shorter than two times the gap are deleted.A slightly hacky way of doing this is to overplot a small black point on a larger white point:
In addition, following Control point border thickness in ggplot, in version 2.0.0 of
ggplot2
it's possible to use thestroke
argument ofgeom_point
to control the border thickness, so the twogeom_point
s can be replaced by just (e.g.)geom_point(size=2, shape=21, fill="black", colour="white", stroke=3)
, eliminating the need to overlay the points.Ok I have an implementation of a geom, that does not rely on hardcoding and should not have wierd offsets. It's essentialy a
geom_point()
implementation, that draws a path* between points, draws a larger background point with colours set to the panel background and then the normal points.*note that path's behaviour is not to connect points along the x-axis, but along row-order in the
data.frame
that is given to ggplot. You can sort your data beforehand if you wantgeom_line()
behaviour.The main problem for me was to get the inner workings of the geom drawing code to retrieve the theme of the current plot to extract the background colour of the panel. Due to this, I'm very unsure how stable this would be (and would welcome any tips), but at least it works.
EDIT: should be more stable now
Let's get to the, admittedly lengthy,
ggproto
object code:Observant people may have noticed the line
bgcol <- sys.frame(4)$theme$panel.background$fill
. I could not find another way to access the current plot's theme, without having to adjust at least several other functions to pass the theme as an argument. In my version of ggplot (3.1.0), the 4thsys.frame()
is the environment of theggplot2:::ggplot_gtable.ggplot_built
call wherein the geom drawing code is evaluated. It's quite easy to imagine that this function can be updated in the future -which can change the scoping- hence the stability warning. As a backup, it defaults to the global theme settings when it can't find the current theme.EDIT: should now be more stable
Onwards to the layer wrapper which is pretty much self-explanatory:
Adding it to a ggplot should be a familiar thing. Just setting the theme to the default
theme_gray()
to test that it indeed takes the current plot's theme.Of course, this method will obscure grid lines with the background points, but that is the tradeoff I was willing to make to prevent wonkyness due to line path shortening. Line sizes, line types, and the relative size of the background points can be set with
aes(linesize = ..., linetype = ..., mult = ...)
or per the...
argument ingeom_pointpath()
. It inherits the other aesthetics fromGeomPoint
.