Related
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().
A slightly hacky way of doing this is to overplot a small black point on a larger white point:
ggplot(pressure, aes(temperature, pressure)) +
geom_line() +
geom_point(size=5, colour="white") +
geom_point(size=2) +
theme_classic() +
theme(panel.background = element_rect(colour = "black"))
In addition, following Control point border thickness in ggplot, in version 2.0.0 of ggplot2 it's possible to use the stroke argument of geom_point to control the border thickness, so the two geom_points 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.
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 like 21 lets you make the inner circle black and the stroke the same color as the background.
library(ggplot2)
bgnd <- theme_get()$panel.background$fill
ggplot(pressure, aes(x = temperature, y = pressure)) +
geom_line() +
geom_point(shape = 21, fill = "black", size = 2, stroke = 1, color = bgnd)
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.
library(lemon)
ggplot(pressure, aes(x = temperature, y = pressure)) +
geom_pointline()
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 want geom_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:
GeomPointPath <- ggproto(
"GeomPointPath", GeomPoint,
draw_panel = function(self, data, panel_params, coord, na.rm = FALSE)
{
# bgcol <- sys.frame(4)$theme$panel.background$fill
# if (is.null(bgcol)) {
# bgcol <- theme_get()$panel.background$fill
# }
# EDIT: More robust bgcol finding -----------
# Find theme, approach as in https://github.com/tidyverse/ggplot2/issues/3116
theme <- NULL
for(i in 1:20) {
env <- parent.frame(i)
if("theme" %in% names(env)) {
theme <- env$theme
break
}
}
if (is.null(theme)) {
theme <- theme_get()
}
# Lookup likely background fills
bgcol <- theme$panel.background$fill
if (is.null(bgcol)) {
bgcol <- theme$plot.background$fill
}
if (is.null(bgcol)) {
bgcol <- theme$rect$fill
}
if (is.null(bgcol)) {
# Default to white if no fill can be found
bgcol <- "white"
}
# END EDIT ------------------
if (is.character(data$shape)) {
data$shape <- ggplot2:::translate_shape_string(data$shape)
}
coords <- coord$transform(data, panel_params)
# Draw background points
bgpoints <- grid::pointsGrob(
coords$x, coords$y, pch = coords$shape,
gp = grid::gpar(
col = alpha(bgcol, NA),
fill = alpha(bgcol, NA),
fontsize = (coords$size * .pt + coords$stroke * .stroke/2) * coords$mult,
lwd = coords$stroke * .stroke/2
)
)
# Draw actual points
mypoints <- grid::pointsGrob(
coords$x, coords$y, pch = coords$shape,
gp = grid::gpar(
col = alpha(coords$colour, coords$alpha),
fill = alpha(coords$fill, coords$alpha),
fontsize = coords$size * .pt + coords$stroke * .stroke/2,
lwd = coords$stroke * .stroke/2
)
)
# Draw line
myline <- grid::polylineGrob(
coords$x, coords$y,
id = match(coords$group, unique(coords$group)),
default.units = "native",
gp = grid::gpar(
col = alpha(coords$colour, coords$alpha),
fill = alpha(coords$colour, coords$alpha),
lwd = (coords$linesize * .pt),
lty = coords$linetype,
lineend = "butt",
linejoin = "round", linemitre = 10
)
)
# Place graphical objects in a tree
ggplot2:::ggname(
"geom_pointpath",
grid::grobTree(myline, bgpoints, mypoints)
)
},
# Set some defaults, assures that aesthetic mappings can be made
default_aes = aes(
shape = 19, colour = "black", size = 1.5, fill = NA, alpha = NA, stroke = 0.5,
linesize = 0.5, linetype = 1, mult = 3,
)
)
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 4th sys.frame() is the environment of the ggplot2:::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:
geom_pointpath <- function(mapping = NULL, data = NULL, stat = "identity",
position = "identity", ..., na.rm = FALSE, show.legend = NA,
inherit.aes = TRUE)
{
layer(data = data, mapping = mapping, stat = stat, geom = GeomPointPath,
position = position, show.legend = show.legend, inherit.aes = inherit.aes,
params = list(na.rm = na.rm, ...))
}
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.
theme_set(theme_gray())
g <- ggplot(pressure, aes(temperature, pressure)) +
geom_pointpath() +
theme(panel.background = element_rect(fill = "dodgerblue"))
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 in geom_pointpath(). It inherits the other aesthetics from GeomPoint.
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 of facet_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.
GeomPointPath <- ggproto(
"GeomPointPath", GeomPoint,
draw_panel = function(data, panel_params, coord, na.rm = FALSE){
# Default geom point behaviour
if (is.character(data$shape)) {
data$shape <- translate_shape_string(data$shape)
}
coords <- coord$transform(data, panel_params)
my_points <- pointsGrob(
coords$x,
coords$y,
pch = coords$shape,
gp = gpar(col = alpha(coords$colour, coords$alpha),
fill = alpha(coords$fill, coords$alpha),
fontsize = coords$size * .pt + coords$stroke * .stroke/2,
lwd = coords$stroke * .stroke/2))
# New behaviour
## Convert x and y to units
x <- unit(coords$x, "npc")
y <- unit(coords$y, "npc")
## Make custom grob class
my_path <- grob(
x = x,
y = y,
mult = (coords$size * .pt + coords$stroke * .stroke/2) * coords$mult,
name = "pointpath",
gp = grid::gpar(
col = alpha(coords$colour, coords$alpha),
fill = alpha(coords$colour, coords$alpha),
lwd = (coords$linesize * .pt),
lty = coords$linetype,
lineend = "butt",
linejoin = "round", linemitre = 10
),
vp = NULL,
### Now this is the important bit:
cl = 'pointpath'
)
## Combine grobs
ggplot2:::ggname(
"geom_pointpath",
grid::grobTree(my_path, my_points)
)
},
# Adding some defaults for lines and mult
default_aes = aes(
shape = 19, colour = "black", size = 1.5, fill = NA, alpha = NA, stroke = 0.5,
linesize = 0.5, linetype = 1, mult = 0.5,
)
)
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:
# Make hook for drawing
makeContent.pointpath <- function(x){
# Convert npcs to absolute units
x_new <- convertX(x$x, "mm", TRUE)
y_new <- convertY(x$y, "mm", TRUE)
# Do trigonometry stuff
hyp <- sqrt(diff(x_new)^2 + diff(y_new)^2)
sin_plot <- diff(y_new) / hyp
cos_plot <- diff(x_new) / hyp
diff_x0_seg <- head(x$mult, -1) * cos_plot
diff_x1_seg <- (hyp - head(x$mult, -1)) * cos_plot
diff_y0_seg <- head(x$mult, -1) * sin_plot
diff_y1_seg <- (hyp - head(x$mult, -1)) * sin_plot
x0 = head(x_new, -1) + diff_x0_seg
x1 = head(x_new, -1) + diff_x1_seg
y0 = head(y_new, -1) + diff_y0_seg
y1 = head(y_new, -1) + diff_y1_seg
keep <- unclass(x0) < unclass(x1)
# Remove old xy coordinates
x$x <- NULL
x$y <- NULL
# Supply new xy coordinates
x$x0 <- unit(x0, "mm")[keep]
x$x1 <- unit(x1, "mm")[keep]
x$y0 <- unit(y0, "mm")[keep]
x$y1 <- unit(y1, "mm")[keep]
# Set to segments class
class(x)[1] <- 'segments'
x
}
Now all we need is a layer wrapper like before, which does nothing special:
geom_pointpath <- function(mapping = NULL, data = NULL, stat = "identity",
position = "identity", ..., na.rm = FALSE, show.legend = NA,
inherit.aes = TRUE)
{
layer(data = data, mapping = mapping, stat = stat, geom = GeomPointPath,
position = position, show.legend = show.legend, inherit.aes = inherit.aes,
params = list(na.rm = na.rm, ...))
}
The demonstration:
g <- ggplot(pressure, aes(temperature, pressure)) +
# Ribbon for showing no point-over-point background artefacts
geom_ribbon(aes(ymin = pressure - 50, ymax = pressure + 50), alpha = 0.2) +
geom_pointpath()
And this should be stable for any resized aspect ratio. You can supply aes(mult = ...) or just mult = ... 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.
This is now possible with the CRAN package {ggh4x}. Funny fact, the geom for this package saw the light of the day on this SO post :) Thanks teunbrand!
library(ggh4x)
#> Loading required package: ggplot2
ggplot(pressure, aes(temperature, pressure)) +
geom_pointpath()
Created on 2021-11-13 by the reprex package (v2.0.1)
I need to arrange several plots for a figure. I am creating individual plots using base and grid graphics. In order to arrange them in a single figure I have been using grid.echo(), grid.grab() to convert to grobs and then arrangeGrob() and grid.arrange() to build the final figure. A few weeks ago my tentative figure was working fine but now when I rerun the code it produces a figure with whitespace in the margins of the plots.
I add a minimal example that shows the problem that I am facing...
##minimal example
library(grid)
library(gridExtra)
library(gridGraphics)
##test plot
plot_n1<-plot(1:10,1:10, asp=1)
##convert test plot to grob
grid.echo()
test_p<-grid.grab()
##simulate several plots arranged in a more complex layout
multi<-arrangeGrob(test_p, test_p, test_p, test_p, ncol=1, heights=c(1/4,1/4,1/4,1/4))
##create graph
png(filename="minimal_multiplot.png", res=300, width=20, height=20, units="cm")
grid.arrange(test_p, multi, ncol=2, widths=c(2/3,1/3))
dev.off()
What am I doing wrong?
There does indeed appear to be a problem when converting a graphics plot into a grid plot, then using grid.grab() to grab and then draw the plot into a smaller regions (i.e., using your method). For instance, using viewports to define a slightly smaller region (coloured grey in the image below), axis material is missing.
# Packages
library(grid)
library(gridGraphics)
plot(1:10,1:10)
grid.echo()
test_p = grid.grab()
grid.newpage()
pushViewport(viewport(x = 0, width = .85, just = "left"))
grid.rect(gp = gpar(col = NA, fill = "grey90"))
grid.draw(test_p)
upViewport()
grid.rect(gp = gpar(col = "grey90", size = .1, fill = NA))
But Paul Murrell (author of the gridGraphics package) offers an alternative (see the examples at ?gridGraphics::grid.echo, and pp. 156-157 of The gridGraphics package in The R Journal v7/1). One can define a function that draws the plot, then that function becomes the argument of grid.echo() at the time of drawing the plot within the viewport. newpage = FALSE stops grid from opening a new page. Note that none of the axis material is chopped off.
pf = function() {
plot(1:10,1:10)
}
grid.newpage()
pushViewport(viewport(x = 0, width = .85, just = "left"))
grid.rect(gp = gpar(col =NA, fill = "grey90"))
grid.echo(pf, newpage=FALSE)
upViewport()
grid.rect(gp = gpar(col = "grey90", size = .1, fill = NA))
So to get your desired plot, I would do something like this - but still using viewports.
pf = function() {
par(mar=c(7.2, 7.2, 1, 1), mex = .3, tcl = .15, mgp = c(3, .15, 0))
plot(1:10, 1:10, cex.axis = .75, cex.lab = .75)
}
grid.newpage()
pushViewport(viewport(layout = grid.layout(3, 2,
widths = unit(c(2, 1), "null"),
heights = unit(c(1, 1, 1), "null"))))
pushViewport(viewport(layout.pos.col = 1, layout.pos.row = 1:3))
grid.echo(pf, newpage = FALSE)
upViewport()
for(i in 1:3) {
pushViewport(viewport(layout.pos.col = 2, layout.pos.row = i))
grid.echo(pf, newpage = FALSE)
upViewport()
}
upViewport()
grid.rect(gp = gpar(col = "grey90", size = .1, fill = NA))
I'm trying to use the function grid.gradientFill from the gridSVG package, but unfortunately I'm not able to see a gradient in my SVG output.
I'm not sure if my code is correct or my Browser does not work (Chrome: 35.0.1916.153 m), can you please give some advise?
Here is my R code:
library(grid)
library(gridSVG)
lg <- linearGradient(col = c("black", "white", "black"))
x <- c(0.2,0.2,0.35,0.5,0.65,0.8,0.8,0.65,0.5,0.35)
y <- c(0.5,0.6,0.61,0.7,0.81,0.8,0.7,0.71,0.6,0.51)
s <- c(0,0,-1,0,-1,0,0,-1,0,-1)
grid.newpage()
vp <- viewport(width=0.75, height=0.75)
pushViewport(vp)
grid.rect(gp=gpar(col="blue"))
pushViewport(viewport(layout.pos.col=1, layout.pos.row=1))
grid.rect(x = unit(0.5, "npc"), y = unit(0.5, "npc"),
width = unit(1, "npc"), height = unit(1, "npc"),
just = "centre",
default.units = "npc",
gp=gpar(col="green", fill = "blue"), draw = TRUE, name = "tom")
grid.xspline(x = x, y = y,shape=s, open=FALSE, gp=gpar(col=NA, fill="darkred"), name="spline")
grid.gradientFill("spline", lg)
grid.gradientFill("tom", lg)
grid.export("c:/#temp/somekindofgradient.SVG")
I'm very interested in giving the spline a gradient ...
Any hint is appreciated :-)
So, finally I found the solution :-)
If you want to use
grid.gradientFill(object, ...)
The object, in my question the grid.xspline(...) object called "spline" does not have to have a fill parameter, meaning ...
Replacing
grid.xspline(x = x, y = y,shape=s, open=FALSE, gp=gpar(col=NA, fill="darkred"), name="spline")
with
grid.xspline(x = x, y = y,shape=s, open=FALSE, gp=gpar(col=NA), name="spline")
And there is some beautiful gradient :-)
I am using both the lattice and ggplots package to create scatterplot in R.
when using lattice package, I can contrl the whole graph size (including plot area and title/footnote) with below codes:
xyplot(
test2 ~ test1,
data = in.data,
groups = SEX,
#title
main = textGrob(c("This is the title", "\nThis is the sub-title"),
x = unit(c(left.margin,left.margin), "inches"),
just = "left",
gp = gpar(fontsize = c(title.font, title.font), lineheight=1.5)),
#footnote
sub = textGrob(paste(footnote1,Sys.time(),sep="\n"),
x = unit(c(left.margin), "inches"),
y = unit(c(bottom.margin), "inches"),
just = c("left", "bottom"),
gp = gpar(fontsize = footer.font, lineheight=0.9))
# Figure margins
lattice.options = list(layout.widths = leftright.margin,
layout.heights = topbottome.margin)
)
You can see that the lattice.options can control the whole figure size including title and footnote.
while with ggplot2, I create a plot use qplot(...) then control plot margin with:
theme(plot.margin=unit(x=c(top.margin,right.margin,bottom.margin,left.margin),
units="inches"))
then add title and footnote with arrangeGrob.
g <- arrangeGrob(myplot, main=textGrob(c(title1, title2),
x = unit(c(left.margin,left.margin), "inches"),
y = unit(c(-top.margin,-top.margin), "inches"),
just = "left",
gp = gpar(fontsize = c(title.font, title.font),
fontface=c("plain","plain"),
lineheight = 1.5)),
sub = textGrob(paste(footnote1,Sys.time(),sep="\n"),
x = unit(c(left.margin), "inches"),
y = unit(c(bottom.margin), "inches"),
just = c("left", "bottom"),
gp = gpar(fontsize = footer.font,lineheight=0.9)))
g
In this way, the plot.margin only control the plot area, not control title and footnote. So how should I do to control the both plot area and title area in ggplot2 (just like lattice package)?
If the answer is not, in this case I will prefer lattice pakcage.
I'm looking for a way to control the line thickness of text plotted in R without having the dimensions of the characters change. Here's an example (not using R):
The middle word has a thickness of twice the top, yet the dimensions are the same (so no scaling happened). The bottom word is actually two words: a red word overlain on a heavy white word, to create color separation (especially useful for annotating a busy plot).
Here's a set of commands I threw together to try and replicate the figure above:
png("font.png",width=1.02, height=1.02, units="in", res=150)
par(ps=10, font=1, bg="light gray", col="black", mai=rep(0.02,4), pin=c(1,1))
plot.new()
box()
text(0.5,0.85,"FONT",cex=1)
text(0.5,0.6,"FONT",cex=2)
text(0.5,0.3,"FONT",cex=2,col="white")
text(0.5,0.3,"FONT",cex=1,col="red")
text(0.5,0.1,"FONT",cex=1, font=2, col="white")
text(0.5,0.1,"FONT",cex=1, font=1, col="red")
dev.off()
giving:
So the effect is the same as changing the font-face to bold, but the size difference is not big enough to be noticeable when overlain. The par help page doesn't appear to have a specific setting for this. Anyone have any ideas?
Note changing size in ggplot2 doesn't produce the effect I want either, last time I checked.
You could try adding multiple versions of the text slightly shifted in a circular pattern,
library(grid)
stextGrob <- function (label, r=0.02, x = unit(0.5, "npc"), y = unit(0.5, "npc"),
just = "centre", hjust = NULL, vjust = NULL, rot = 0, check.overlap = FALSE,
default.units = "npc", name = NULL, gp = gpar(), vp = NULL){
let <- textGrob("a", gp=gp, vp=vp)
wlet <- grobWidth(let)
hlet <- grobHeight(let)
tg <- textGrob(label=label, x=x, y=y, gp=gpar(col="red"),
just = just, hjust = hjust, vjust = vjust, rot = rot,
check.overlap = check.overlap,
default.units = default.units)
tgl <- c(lapply(seq(0, 2*pi, length=36), function(theta){
textGrob(label=label,x=x+cos(theta)*r*wlet,
y=y+sin(theta)*r*hlet, gp=gpar(col="white"),
just = just, hjust = hjust, vjust = vjust, rot = rot,
check.overlap = check.overlap,
default.units = default.units)
}), list(tg))
g <- gTree(children=do.call(gList, tgl), vp=vp, name=name, gp=gp)
}
grid.stext <- function(...){
g <- stextGrob(...)
grid.draw(g)
invisible(g)
}
grid.newpage()
grid.rect(gp=gpar(fill="grey"))
grid.stext("Yeah", gp=gpar(cex=4))
There's a version using base graphics lurking in the archives of R-help, from which this is inspired.
Another option using a temporary postscript file, converted to a shape by grImport,
library(grImport)
cat("%!PS
/Times-Roman findfont
100 scalefont
setfont
newpath
0 0 moveto
(hello) show", file="hello.ps")
PostScriptTrace("hello.ps", "hello.xml")
hello <- readPicture("hello.xml")
grid.rect(gp=gpar(fill="grey"))
grid.picture(hello,use.gc = FALSE, gp=gpar(fill="red", lwd=8, col="white"))
I imagine something similar could be done with a temporary raster graphic file, blurred by some image processing algorithm and displayed as raster below the text.
You could try:
text(...,"FONT", vfont = c('serif','bold'))
Although I'm not sure how you'd do the third version of FONT.