How to apply a clipping mask to geom in a ggplot? - r

I am trying to apply a clipping mask to a geom from a ggplot to mask part of the data, but keep the axis, the grid, other geoms and the legend visible. I do not want to create a specific plot, and therefore I am not looking for a work-around with polygons masking some parts of the plot.
This is the kind of design I would like to emulate (the mask, not necessarily theme, I now how to do that):
(source)
See also this example
One could argue that I could filter the data that is not contained in the polygon that defines the mask. But, while it works for point, and can work for polygon/line-like objects, it does for rasters (the border would not exactly follow non-vertical or non-horizontal lines).
So I tried the following:
library(ggplot2)
library(gridSVG)
library(grImport)
# Create a plot
p <- ggplot(diamonds[1:300,], aes(carat, price)) + geom_point(aes(colour = cut))
# And a clipping mask
pg <- polygonGrob(c(.7, 0, 0, 1, 1),
c(0, .7, 1, 1, 0))
cp <- clipPath(pg)
I was able to use the packages gridSVG to define a clipping mask, but I have difficulties applying it on a ggplot object, even after extracting the grob (see resource here) with ggplotGrob(). I was not able to apply the clipping mask to the grob:
g <- ggplotGrob(p) # store the plot as a grob
registerClipPath("mask", cp)
g_clipped <- clipPath(g)
gridsvg(name = "test_c2.svg")
grid.draw(clipPathGrob(g_clipped, cp)$grob)
dev.off()
My intuition was that the g_clipped should be plotted, but I couldn't grid.draw() it, since its a clipPath object. And the grid.draw()
line written here show the plot not masked. I think I don't quite grasp how the clipPath objects function.
The function grobify() sounds like it could help for an alternative appraoch without gridSVG, see details here, but I do not understand the quite minimalistic documentation.
Since I cannot even apply the clipping mask to the whole plot, I am far for my objective.
If you can help me understand how to apply the clipping mask from gridSVGor have an alternative solution to apply a clipping mask to specific geoms, please let me know.

The following is a grid solution, but very much a work-around. It shows how to apply a non-rectangular clipping region to a ggplot, so that one set of points in your plot is clipped. You weren't too far wrong in your attempt. A couple of points to note:
You need to grid.force() the ggplotGrob object so the grid can see the grobs.
Do not define the ggplot grob as a clipping path - the clipping path is the polygon.
The clipping path is applied to the points grob within the plot panel of the ggplot. This means that other objects in the plot panel, the panel background and grid lines, do not get clipped. Only the data points are clipped.
I've added a blue line to the plot to show that the line too does not need to be clipped; but can be clipped if desired.
There are also commented lines of code that, when uncommented, will draw the clipping region, and move the grid lines and points to the front (that is, in front of the darker grey clipping region).
library(ggplot2)
library(gridSVG)
library(grid)
# Open the graphics device
gridsvg(name = "test.svg")
# Create a plot
p <- ggplot(diamonds[1:300, ], aes(carat, price)) +
geom_point(aes(colour = cut)) +
geom_line(data = data.frame(x = c(.3, .9), y = c(500, 2500)), aes(x,y), col = "skyblue", size = 2)
g <- ggplotGrob(p) # Store the plot as a grob
g = grid.force(g) # So that grid sees all grobs
grid.draw(g) # Draw the plot
# Define the clipping path
pg <- polygonGrob(c(.7, 0, 0, 1, 1),
c(0, .7, 1, 1, 0))
# The clipping path can be nearly any shape you desire.
# Try this for a circular region
# pg = circleGrob(x = .5, y = .6, r = .5)
cp <- clipPath(pg)
# Add the clipping path to the points grob.
# That is, only the points inside the polygon will be visible,
# but the background and grid lines will not be clipped.
# Nor will the blue line be clipped.
# grid.ls(g) # names of the grobs
seekViewport(grep("panel.[0-9]", grid.ls(g)$name, value = TRUE))
grid.clipPath("points", cp, grep = TRUE)
# To clip the blue line, uncomment the next line
# grid.clipPath("GRID.polyline", cp, grep = TRUE)
# To show the clipping region,
# uncomment the next two lines.
# showcp = editGrob(pg, gp = gpar(fill = rgb(0, 0, 0, 0.05), col = "transparent"))
# grid.draw(showcp)
# And to move the grid lines, remaining data points, and blue line in front of the clipping region,
# uncomment the next five lines
# panel = grid.get("panel", grep = TRUE) # Get the panel, and remove the background grob
# panel = removeGrob(panel, "background", grep = TRUE)
# grid.remove("points", grep = TRUE) # Remove points and grid lines from the rendered plot
# grid.remove("line", grep = TRUE, global = TRUE)
# grid.draw(panel) # Draw the edited panel - on top of the clipping region
# Turn off the graphics device
dev.off()
# Find text.svg in your working directory
Edit Defining the clipping region using the coordinate system in which the data points were drawn.
library(ggplot2)
library(gridSVG)
library(grid)
# Open the graphics device
gridsvg(name = "test.svg")
# Create a plot
p <- ggplot(diamonds[1:300, ], aes(carat, price)) +
geom_point(aes(colour = cut)) +
geom_line(data = data.frame(x = c(.3, .9), y = c(500, 2500)), aes(x,y), col = "skyblue", size = 2)
g <- ggplotGrob(p) # Store the plot as a grob
g = grid.force(g) # So that grid sees all grobs
grid.draw(g) # Draw the plot
# Get axis limits (including any expansion)
axis.limits = summarise_layout(ggplot_build(p))[1, c('xmin', 'xmax', 'ymin', 'ymax')]
# Find the 'panel' viewport,
# then push to a new viewport,
# one that exactly overlaps the 'panel' viewport,
# but with limits on the x and y scales that are the same
# as the limits for the original ggplot.
seekViewport(grep("panel.[0-9]", grid.ls(g)$name, value = TRUE))
pushViewport(dataViewport(xscale = axis.limits[1, 1:2],
yscale = axis.limits[1, 3:4]))
# Define the clipping path
pg <- polygonGrob(x = c(.6, 0.3, .3, .8, 1.2),
y = c(500, 1500, 2900, 2900, 1500),
default.units="native")
cp <- clipPath(pg)
# Add the clipping path to the points grob.
# That is, only the points inside the polygon will be visible,
# but the background and grid lines will not be clipped.
# Nor will the blue line be clipped.
# grid.ls(g) # names of the grobs
grid.clipPath("points", cp, grep = TRUE)
# To clip the blue line, uncomment the next line
grid.clipPath("GRID.polyline", cp, grep = TRUE)
# To show the clipping region.
showcp = editGrob(pg, gp = gpar(fill = rgb(0, 0, 0, 0.05), col = "transparent"))
grid.draw(showcp)
# And to move the grid lines and remaining data points in front of the clipping region.
panel = grid.get("panel", grep = TRUE) # Get the panel, and remove the background grob
panel = removeGrob(panel, "background", grep = TRUE)
grid.remove("points", grep = TRUE) # Remove points and grid lines from the rendered plot
grid.remove("line", grep = TRUE, global = TRUE)
grid.draw(panel) # Draw the edited panel - on top of the clipping region
# Turn off the graphics device
dev.off()
# Find text.svg in your working directory

Since you are starting out with a ggplot object, it may be simpler to create the mask itself as a geom layer, rather than convert everything to grob and work in the grid system there.
The geom_polypath() function from the ggpolypath package can be used here. Unlike the standard geom_polygon in ggplot2, it is able to handle polygons with holes (see vignette):
# sample data frame for clipping. The first four x & y coordinates are for the outer ends;
# the next four are for the hole in the polygon.
clipping.df <- data.frame(x = c(0, 1.5, 1.5, 0, 0.2, 1, 0.7, 0.3),
y = c(0, 0, 3000, 3000, 250, 2000, 2800, 1500),
hole = rep(c(FALSE, TRUE), each = 4),
group = rep(c("1", "2"), each = 4))
library(ggpolypath)
p +
geom_polypath(data = clipping.df,
aes(x = x, y = y, group = group),
colour = NA, fill = "black", alpha = 0.5,
inherit.aes = FALSE) +
scale_x_continuous(expand = c(0, 0)) + # don't show edges beyond the extent
scale_y_continuous(expand = c(0, 0)) # of the polygon

Related

Is it possible to draw the axis line first, before the data?

This is a follow up to my previous question where I was looking for a solution to get the axis drawn first, then the data. The answer works for that specific question and example, but it opened a more general question how to change the plotting order of the underlying grobs. First the axis, then the data.
Very much in the way that the panel grid grob can be drawn on top or not.
Panel grid and axis grobs are apparently generated differently - axes more as guide objects rather than "simple" grobs. (Axes are drawn with ggplot2:::draw_axis(), whereas the panel grid is built as part of the ggplot2:::Layout object).
I guess this is why axes are drawn on top, and I wondered if the drawing order can be changed.
# An example to play with
library(ggplot2)
df <- data.frame(var = "", val = 0)
ggplot(df) +
geom_point(aes(val, var), color = "red", size = 10) +
scale_x_continuous(
expand = c(0, 0),
limits = c(0,1)
) +
coord_cartesian(clip = "off") +
theme_classic()
A ggplot can be represented by its gtable. The position of the grobs are given by the layout element, and "the z-column is used to define the drawing order of the grobs".
The z value for the panel, which contains the points grob, can then be increased so that it is drawn last.
So if p is your plot then
g <- ggplotGrob(p) ;
g$layout[g$layout$name == "panel", "z"] <- max(g$layout$z) + 1L
grid::grid.draw(g)
However, as noted in the comment this changes how the axis look, which perhaps, is due to the panel being drawn over some of the axis.
But in new exciting news from dww
if we add theme(panel.background = element_rect(fill = NA)) to the plot, the axes are no longer partially obscured. This both proves that this is the cause of the thinner axis lines, and also provides a reasonable workaround, provided you don't need a colored panel background.
Since you are looking for a more "on the draw level" solution, then the place to start is to ask "how is the ggplot drawn in the first place?". The answer can be found in the print method for ggplot objects:
ggplot2:::print.ggplot
#> function (x, newpage = is.null(vp), vp = NULL, ...)
#> {
#> set_last_plot(x)
#> if (newpage)
#> grid.newpage()
#> grDevices::recordGraphics(requireNamespace("ggplot2",
#> quietly = TRUE), list(), getNamespace("ggplot2"))
#> data <- ggplot_build(x)
#> gtable <- ggplot_gtable(data)
#> if (is.null(vp)) {
#> grid.draw(gtable)
#> }
#> else {
#> if (is.character(vp))
#> seekViewport(vp)
#> else pushViewport(vp)
#> grid.draw(gtable)
#> upViewport()
#> }
#> invisible(x)
#> }
where you can see that a ggplot is actually drawn by calling ggplot_build on the ggplot object, then ggplot_gtable on the output of ggplot_build.
The difficulty is that the panel, with its background, gridlines and data is created as a distinct grob tree. This is then nested as a single entity inside the final grob table produced by ggplot_build. The axis lines are drawn "on top" of that panel. If you draw these lines first, part of their thickness will be over-drawn with the panel. As mentioned in user20650's answer, this is not a problem if you don't need your plot to have a background color.
To my knowledge, there is no native way to include the axis lines as part of the panel unless you add them yourself as grobs.
The following little suite of functions allows you to take a plot object, remove the axis lines from it and add axis lines into the panel:
get_axis_grobs <- function(p_table)
{
axes <- grep("axis", p_table$layout$name)
axes[sapply(p_table$grobs[axes], function(x) class(x)[1] == "absoluteGrob")]
}
remove_lines_from_axis <- function(axis_grob)
{
axis_grob$children[[grep("polyline", names(axis_grob$children))]] <- zeroGrob()
axis_grob
}
remove_all_axis_lines <- function(p_table)
{
axes <- get_axis_grobs(p_table)
for(i in axes) p_table$grobs[[i]] <- remove_lines_from_axis(p_table$grobs[[i]])
p_table
}
get_panel_grob <- function(p_table)
{
p_table$grobs[[grep("panel", p_table$layout$name)]]
}
add_axis_lines_to_panel <- function(panel)
{
old_order <- panel$childrenOrder
panel <- grid::addGrob(panel, grid::linesGrob(x = unit(c(0, 0), "npc")))
panel <- grid::addGrob(panel, grid::linesGrob(y = unit(c(0, 0), "npc")))
panel$childrenOrder <- c(old_order[1],
setdiff(panel$childrenOrder, old_order),
old_order[2:length(old_order)])
panel
}
These can all be co-ordinated into a single function now to make the whole process much easier:
underplot_axes <- function(p)
{
p_built <- ggplot_build(p)
p_table <- ggplot_gtable(p_built)
p_table <- remove_all_axis_lines(p_table)
p_table$grobs[[grep("panel", p_table$layout$name)]] <-
add_axis_lines_to_panel(get_panel_grob(p_table))
grid::grid.newpage()
grid::grid.draw(p_table)
invisible(p_table)
}
And now you can just call underplot_axes on a ggplot object. I have modified your example a little to create a gray background panel, so that we can see more clearly what's going on:
library(ggplot2)
df <- data.frame(var = "", val = 0)
p <- ggplot(df) +
geom_point(aes(val, var), color = "red", size = 10) +
scale_x_continuous(
expand = c(0, 0),
limits = c(0,1)
) +
coord_cartesian(clip = "off") +
theme_classic() +
theme(panel.background = element_rect(fill = "gray90"))
p
underplot_axes(p)
Created on 2021-05-07 by the reprex package (v0.3.0)
Now, you may consider this "creating fake axes", but I would consider it more as "moving" the axis lines from one place in the grob tree to another. It's a shame that the option doesn't seem to be built into ggplot, but I can also see that it would take a pretty major overhaul of how a ggplot is constructed to allow that option.
Here's a hack that doesn't require going "under the hood", but rather uses patchwork to add another layer on top that is just the geom layer.
a <- [your plot above]
library(patchwork)
a + inset_element(a + them_void(), left = 0, bottom = 0, right = 1, top = 1)

Left-aligning ggplot when saved while using a fixed aspect ratio

I'm building a custom ggplot theme to standardize the look & feel of graphs I produce. The goal is more complex than this minimal example, so I'm looking for a general solution. I have a few key goals:
I want all graphs to export at the same size (3000 pixels wide, 1500 pixels high).
I want to control the aspect ratio of the plot panel itself.
I want to use textGrobs to include figure numbers.
I want the image to be left-aligned
The challenge I'm facing is that when combining these two constraints, the image that gets saved centers the ggplot graph within the window, which makes sense as a default, but looks bad in this case.
I'm hoping there's a general solution to left-align the ggplot panel when I export. Ideally, this will also work similarly for faceted graphs.
It seems that something should be possible using one of or some combination of the gridExtra, gtable, cowplot, and egg packages, but after experimenting for a few hours I'm at a bit of a loss. Does anybody know how I can accomplish this? My code is included below.
This is the image that gets produced. As you can see, the caption is left-aligned at the bottom, but the ggplot itself is horizontally centered. I want the ggplot graph left-aligned as well.
Graph output: https://i.stack.imgur.com/5EM2c.png
library(ggplot2)
# Generate dummy data
x <- paste0("var", seq(1,10))
y <- LETTERS[1:10]
data <- expand.grid(X=x, Y=y)
data$Z <- runif(100, -2, 2)
# Generate heatmap with fixed aspect ratio
p1 <- ggplot(data, aes(X, Y, fill= Z)) +
geom_tile() +
labs(title = 'A Heatmap Graph') +
theme(aspect.ratio = 1)
# A text grob for the footer
figure_number_grob <- grid::textGrob('Figure 10',
x = 0.004,
hjust = 0,
gp = grid::gpar(fontsize = 10,
col = '#01A184'))
plot_grid <- ggpubr::ggarrange(p1,
figure_number_grob,
ncol = 1,
nrow = 2,
heights = c(1,
0.05))
# save it
png(filename = '~/test.png', width = 3000, height = 1500, res = 300, type = 'cairo')
print(plot_grid)
dev.off()
I was able to find a solution to this that works for my needs, though it does feel a bit hacky.
Here's the core idea:
Generate the plot without a fixed aspect ratio.
Split the legend from the plot as its own component
Use GridExtra's arrangeGrob to combine the plot, a spacer, the legend, and another spacer horizontally
Set the width of the plot to some fraction of npc (normal parent coordinates), in this case 0.5. This means that the plot will take up 50% of the horizontal space of the output file.
Note that this is not exactly the same as setting a fixed aspect ratio for the plot. If you know the size of the output file, it's close to the same thing, but the size of axis text & axis titles will affect the output aspect ratio for the panel itself, so while it gets you close, it's not ideal if you need a truly fixed aspect ratio
Set the width of the spacers to the remaining portion of the npc (in this case, 0.5 again), minus the width of the legend to horizontally center the legend in the remaining space.
Here's my code:
library(ggplot2)
# Generate dummy data
x <- paste0("var", seq(1,10))
y <- LETTERS[1:10]
data <- expand.grid(X=x, Y=y)
data$Z <- runif(100, -2, 2)
# Generate heatmap WITHOUT fixed aspect ratio. I address this below
p1 <- ggplot(data, aes(X, Y, fill= Z)) +
geom_tile() +
labs(title = 'A Heatmap Graph')
# Extract the legend from our plot
legend = gtable::gtable_filter(ggplotGrob(p1), "guide-box")
plot_output <- gridExtra::arrangeGrob(
p1 + theme(legend.position="none"), # Remove legend from base plot
grid::rectGrob(gp=grid::gpar(col=NA)), # Add a spacer
legend, # Add the legend back
grid::rectGrob(gp=grid::gpar(col=NA)), # Add a spacer
nrow=1, # Format plots in 1 row
widths=grid::unit.c(unit(0.5, "npc"), # Plot takes up half of width
(unit(0.5, "npc") - legend$width) * 0.5, # Spacer width
legend$width, # Legend width
(unit(0.5, "npc") - legend$width) * 0.5)) # Spacer width
# A text grob for the footer
figure_number_grob <- grid::textGrob('Figure 10',
x = 0.004,
hjust = 0,
gp = grid::gpar(fontsize = 10,
col = '#01A184'))
plot_grid <- ggpubr::ggarrange(plot_output,
figure_number_grob,
ncol = 1,
nrow = 2,
heights = c(1,
0.05))
# save it
png(filename = '~/test.png', width = 3000, height = 1500, res = 300, type = 'cairo')
print(plot_grid)
dev.off()
And here's the output image: https://i.stack.imgur.com/rgzFy.png

How to reduce the space between the plot and the border for geographic maps?

I am trying to plot a bathymetry map of the the northeast US using the marmap library. The following code loads the correct extent but when I plot the map I have blank space between the border and the map either at the top/bottom or left/right of the map. This also occurs when exporting the plots. If I drag the plot viewer screen size the plot adjusts and I can remove almost all of the empty space but I will be running this script in a loop so its not practical to solve this problem this way. Because of the loop I also can't hard code any dimensions into the plot because it will change for each new extent. How can I set the border of the plot to match the extent of the bathymetry?
library(marmap)
library(maps)
atl<- getNOAA.bathy(-80.93645,-41.61417,30.2 ,60.905 ,resolution=4)
blues <- colorRampPalette(c("darkblue", "cyan"))
greys <- colorRampPalette(c(grey(0.4),grey(0.99)))
plot(atl, image = TRUE, land = TRUE, n=0,
bpal = list(c(0, max(atl), greys(100)),
c(min(atl), 0, blues(100))))
map(database= "state", col="black", fill=FALSE, add=TRUE)
text(x=state.center$x, y=state.center$y, state.abb, cex=0.5)
This behavior is caused by the asp argument of plot.bathy(). By default, it is fixed as asp = 1 to ensure that the scales on both axes are the same (one degree of longitude equals one degree of latitude). An unwelcome consequence of this default, is the white bands appearing either on the left/right sides of the graph, or on the top/bottom sides depending on the dimensions of your bathymetric map and the plotting device.
So I suppose you have 2 options:
If you don't mind having a slightly distorted perspective, you can set asp = NA in your call to plot.bathy()
If you want to have the correct aspect ratio but need to use the default size for your plotting region, then you have to download a bathymetric region that covers the whole plotting region of your active device. For instance, you could call plot.bathy() once to create a "default" plot, then, use par("usr") to determine the limits of the bathymetry needed to fill the entire plotting area. You would then download a second bathymetry with the appropriate ranges in longitude and latitude. Which is maybe not desirable.
Here is what the code would look like for the second option:
atl <- getNOAA.bathy(-80.93645, -41.61417, 30.2, 60.905, resolution = 4)
blues <- colorRampPalette(c("darkblue", "cyan"))
greys <- colorRampPalette(c(grey(0.4), grey(0.99)))
plot(atl, image = TRUE, land = TRUE, n = 0,
bpal = list(c(0, max(atl), greys(100)),
c(min(atl), 0, blues(100))))
coord <- par("usr")
atl2 <- getNOAA.bathy(coord[1], coord[2], coord[3], coord[4], res = 4)
plot(atl2, image = TRUE, land = TRUE, lwd = 0.2,
bpal = list(c(0, max(atl2), greys(100)),
c(min(atl2), 0, blues(100))))
map(database = "state", col = "black", fill = FALSE, add = TRUE)
text(x = state.center$x, y = state.center$y, state.abb, cex = 0.5)
I suppose the solution proposed by Roman Luštrik works too, but it has the inconvenience of leaving the white bands visible on both sides of the plot.
As an aside, if you have a lot of bathymetric regions to plot, you should maybe consider using the keep = TRUE argument of getNOAA.bathy() to avoid querying the NOAA servers each time you need to re-execute your code (and it is much faster to load local data than remote ones). And you could also download once and for all the global 4Go ETOPO1 and use subset.bathy() to, well, subset the bathymetry you need for each plot.
Here is a proposal using a workaround. The idea is to convert the bathy object into raster object and then make the plot using levelplot from rasterVisthat correctly fits the plotting area to the raster extent. Note that using raster allows having a defined pixel size and, therefore, a correct width/height ratio that you don't seem to have with marmap::plot method.
library(raster)
library(rasterVis)
r <- marmap::as.raster(atl)
state <- map('state', plot = FALSE)
state <- data.frame(lon = state$x, lat = state$y)
state.lab <- data.frame(lon = state.center$x, lat = state.center$y,
label = state.abb)
# you can remove the color legend by adding colorkey = FALSE in levelplot()
levelplot(r,
at = c(seq(min(atl), 0, length.out = 100),
seq(0, max(atl), length.out = 100)[-1]),
col.regions = c(blues(100), greys(100)),
margin = FALSE) +
xyplot(lat ~ lon, state, type = 'l',
col = 'black') +
xyplot(lat ~ lon, data = state.lab,
panel = function(y, x, ...) {
ltext(x = x, y = y, labels = state.lab$label, cex = 0.75)
})

How to change the legend box width when plotting in R

I use the following script to generate a legend in R. But the legend box is too small... how do I increase the box width?
legend("topleft", lty = 1, legend = c("Sub_metering_1","Sub_metering_2","Sub_metering_3"),col = c("black","red","blue"))
You are probably resizing your graph after you plot it and the legend. If that is the case, and you want to keep the box, one option would be to plot the graph, resize it, and then generate the legend. Perhaps a better option would be to size the window to the desired width to start with:
# on Windows, you can use the `windows` function. elsewhere, try quartz or X11
windows(height = 7, width = 3.5)
plot(hp ~ mpg, data = mtcars)
leg <- legend("topleft", lty = 1,
legend = c("Sub_metering_1","Sub_metering_2","Sub_metering_3"),
col = c("black","red","blue"),
#plot = FALSE,
#bty = "n")
)
You can also define exactly where you want the box to fall by providing a pair of x and y coordinates to the legend function. Those values would represent the upper left and bottom right corners of the box. The legend function will actually generate the coordinates for the upper-left hand corner of the box along with the width and height. By default it returns them invisibly, but you can assign them to an object, and If you use the plot = FALSE, option to legend you can capture those coordinates and modify them as you wish without actually plotting the legend.
windows(height = 7, width = 3.5)
plot(hp ~ mpg, data = mtcars)
legend(x = c(9.46, 31), y = c(346.32, 298),
legend = c("Sub_metering_1","Sub_metering_2","Sub_metering_3"),
col = c("black","red","blue"),
lty = 1)
The legend function will actually generate the coordinates for the upper-left hand corner of the box (that's where I got 9.46 and 346.62) along with the width and height of the box. By default it returns them invisibly, but you can assign them to an object, and if you use the plot = FALSE, option to legend you can capture those coordinates and modify them as you wish without actually plotting the legend.
plot(hp ~ mpg, data = mtcars)
leg <- legend("topleft", lty = 1,
legend = c("Sub_metering_1","Sub_metering_2","Sub_metering_3"),
col = c("black","red","blue"),
plot = FALSE)
# adjust as desired
leftx <- leg$rect$left
rightx <- (leg$rect$left + leg$rect$w) * 1.2
topy <- leg$rect$top
bottomy <- (leg$rect$top - leg$rect$h) * 1
# use the new coordinates to define custom
legend(x = c(leftx, rightx), y = c(topy, bottomy), lty = 1,
legend = c("Sub_metering_1","Sub_metering_2","Sub_metering_3"),
col = c("black","red","blue"))
Part of the legend width is determined by the longest width of the labels you use, which is calculated via strwidth. Below an easy example how to halve or double the size by using legend(..., text.width = ...).
plot(1)
text = c("Sub_metering_1","Sub_metering_2","Sub_metering_3")
legend("topleft"
,lty = 1
,legend = text
,col = c("black","red","blue")
)
strwidth(text)
# [1] 0.1734099 0.1734099 0.1734099
# half the length
legend("bottomleft"
,lty = 1
,legend = text
,text.width = strwidth(text)[1]/2
,col = c("black","red","blue")
)
# double the length
legend("center"
,lty = 1
,legend = text
,text.width = strwidth(text)[1]*2
,col = c("black","red","blue")
)

Absolute positioning of rasterGrobs in gtable cells

I have been attempting to specify absolute positions for rasterGrobs in gtable cells without success. I would like to be able to have the extents of an image align to values on the y axis. The script aligns drill-core images alongside multi-sensor data plotted in ggplot2 facets. For example, a particular radiograph core image needs to have its top at 192 mm, and bottom at 1482 mm, but I want the scale to go from 0 to 1523 mm. Please see the included link for an example of what I am doing, but for simplicity I have only posted an MWE here. Is it possible to specify an absolute position for a rasterGrob inside a gtable cell?
sample of intended output
In terms of the MWE below, my only solution thus far has been to move Rlogo.png around using relative positions set when using rasterGrob(). Using "native" coordinates does not appear to be what I need either. Similarly, I can't make sense of the position parameters called in gtable_add_grob().
library(png)
library(ggplot2)
library(gtable)
# read Image
img <- readPNG(system.file("img", "Rlogo.png", package = "png"))
# convert to rastergrob
g <- rasterGrob(img, y = unit(0.5, "npc"), x = unit(0.5, "npc"))
# create plot
tp <- qplot(1:5, 1:5, geom="blank") + scale_y_reverse()
# convert plot to gtable
tt <- ggplot_gtable(ggplot_build(tp))
# add column to gtable to hold image
tt <- gtable_add_cols(tt, tt$width[[.5*4]], 3)
# add grob to cell 3, 4
tt <- gtable_add_grob(tt,g,3,4)
# render
grid.draw(tt)
Did a lot of searching before coming up with this solution of using rasterGrob to add images to panels in a ggplot. Perhaps though there is a more elegant solution someone can suggest?
The grob can set its position within a cell, as illustrated below
library(gridExtra)
library(grid)
library(gtable)
# quick shortcut to create a 2x2 gtable filled with 4 rectGrobs
tg <- arrangeGrob(grobs=replicate(4, rectGrob(), FALSE))
# red rect of fixed size with default position (0.5, 0.5) npc
rg1 <- rasterGrob("red", width=unit(1,"cm"), height=unit(1,"cm"))
# blue rect with specific x position (0) npc, left-justified
rg2 <- rasterGrob("blue", width=unit(1,"cm"), height=unit(1,"cm"),
x = 0, hjust=0)
# green rect at x = 1cm left-justified, y=-0.5cm from middle, top-justified
rg3 <- rasterGrob("green", width=unit(1,"cm"), height=unit(1,"cm"),
x = unit(1,"cm"), y=unit(0.5, "npc") - unit(0.5, "cm"),
hjust=0, vjust=1)
# place those on top
tg <- gtable_add_grob(tg, rg1, 1, 2, z = Inf, name = "default")
tg <- gtable_add_grob(tg, rg2, 1, 2, z = Inf, name = "left")
tg <- gtable_add_grob(tg, rg3, 1, 2, z = Inf, name = "custom")
grid.newpage()
grid.draw(tg)

Resources