Draw legend with geom_sf when no aesthetic is specified - r

When drawing maps using ggplot/geom_sf, any layer mapped to an aesthetic is represented in the legend. I am also aware that using show.legend = ... can be used to force legend representation and to manipulate the symbology used (point, line, polygon). However, when plotting simple objects without the need to further map them to an aesthetic, i.e. using colour/fill to show aditional information, no legend entry is given. This also seems to be the case when using show.legend = TRUE. I found this question on GitHub: https://github.com/tidyverse/ggplot2/issues/3636 . I am not quite sure, whether the person asking was undestood correctly, but there didn't seem to be a good answer. So, using his code:
library(ggplot2)
library(sf)
nc <- st_read(system.file("shape/nc.shp", package = "sf"))
ggplot(nc) + geom_sf(show.legend = TRUE)
fails to produce a legend. I would like the legend to simply show a line in the same colour as shown in the map and give a name for that layer. I can add a legend, using this workaround:
ggplot() +
geom_sf(aes(fill = "US State borders"), nc, show.legend = "line")
However, this now changes the colour of the plotted layer. Ok, so let's try to specify a colour:
ggplot() +
geom_sf(aes(fill = "US State borders"), nc, fill = "grey", show.legend = "line")
Whoops, lost the legend again, probably because I have now specified fill twice. Is this really not possible using ggplot/geom_sf?

If I understand the question correctly, it should work if you specify the fill color in scale_fill_manual():
ggplot() +
geom_sf(aes(fill = "US State borders"), nc, show.legend = "line") +
scale_fill_manual(values = 'grey') +
labs(fill = '') # removing legend title

Related

Dynamically align plots (custom ggplot2 legend for spatial maps) in R

I have a number of maps that I'm generating in R using the sf library and I would like to have a nice looking legend. Unfortunately, it seems that the standard legend for geom_sf() are these ugly looking boxes. The only SO post I could find on adjusting shapes in ggplot2 legends is here.
The here is to use guides(colour = guide_legend(override.aes = list(shape = 16))); however, this only seems to work for geom_point() and not for geom_sf().
Unless someone can suggest an alternative method for changing the shape of legend objects I will need to design a custom legend in Inkscape and align this along with various maps.
Here's a snippet of the code to show what I've tried already:
legend <- image_read_svg('https://svgshare.com/i/FDV.svg')
p1 <- ggplot() +
geom_sf(data=otherroads, size = .45, aes(color=SUFTYPABRV)) +
geom_sf(data=allroads, size = .55, aes(color=SUFTYPABRV)) +
scale_color_manual(values = plotcolors, guide = "legend") + theme_map() +
labs(title = "Sydney")
ggdraw() +
draw_plot(p1) +
draw_image(legend, width = 0.4, hjust = -0.75, vjust = 0.43)
Good legend location example
The output looks good in this case; however, this won't work for me because it requires manual tweaking for every plot.
What I would like is for the location of this legend to be dynamically placed according to the ggplot object, which depends upon the city I'm plotting.
Bad legend location example
The code (and data) in it's entirety can be cloned from github: https://github.com/moldach/map-help.git
This answer is not to adress the legend placement, but changing the legend icons. From your examples, I gather your data produces a legend that looks like this by default:
# example from the geom_sf help page
nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)
# throwing in some random categorical data
nc$catvar <- sample(LETTERS[1:5], nrow(nc), replace = TRUE)
ggplot(nc) +
geom_sf(aes(colour = catvar))
Not too long ago, ggplot added the ability to set what legend shape you wanted by implicitly (via the ellipsis) adding the key_glyph argument to all layers, such as geoms and stats.
ggplot(nc) +
geom_sf(aes(colour = catvar), key_glyph = "timeseries")
You could use this, to set the glyph to points and then use the override.aes trick to get the shapes that you want.
ggplot(nc) +
geom_sf(aes(colour = catvar), key_glyph = "point") +
guides(colour = guide_legend(override.aes = list(size = 3, shape = c(15:19))))
It should work with all the regular legend placement tools that are already in ggplot. You might have to specify locations specific for the plot, but at least you won't have to export your legend to svg files first before adding them.

Insert geom_sf layer underneath existing geom_sf layers

I have a basic map of India with states and borders, some labels, and a number of other specifications stored as a gg object. I'd like to generate a number of maps with a district layer, which will bear data from different variables.
To prevent the district maps overwriting state and country borders, it must be before all the previous code, which I'd like to avoid repeating.
I thought I could do this by calling on $layers for the gg object as per this answer. However, it throws an error. Reprex is below:
library(ggplot2)
library(sf)
library(raster)
# Download district and state data (should be less than 10 Mb in total)
distSF <- st_as_sf(getData("GADM",country="IND",level=2))
stateSF <- st_as_sf(getData("GADM",country="IND",level=1))
# Add border
countryborder <- st_union(stateSF)
# Basic plot
basicIndia <- ggplot() +
geom_sf(data = stateSF, color = "white", fill = NA) +
geom_sf(data = countryborder, color = "blue", fill = NA) +
theme_dark()
basicIndia
# Data-bearing plot
districts <- ggplot() +
geom_sf(data = distSF, fill = "gold")
basicIndia$layers <- c(geom_sf(data = distSF, fill = "gold"), basicIndia$layers)
basicIndia
#> Error in y$layer_data(plot$data): attempt to apply non-function
Intended outcome
Any help would be much appreciated!
I'm still not sure if I'm missing a detail of what you're looking for, but ggplot2 draws layers in the order you provide them. So something like
ggplot(data) +
geom_col() +
geom_point(...) +
geom_line(...)
will draw columns, then points on top of those, then lines on top of the previous layers.
Same goes for sf plots, which makes it easy to make a plot like this of multiple geographic levels.
(I'm using rmapshaper::ms_simplify on the sf objects just to simplify them and speed things up for plotting.)
library(dplyr)
library(ggplot2)
library(sf)
library(raster)
distSF <- st_as_sf(getData("GADM",country="IND",level=2)) %>% rmapshaper::ms_simplify()
...
Then you can plot by adding up the layers in the order you need them displayed. Keep in mind that if you needed to do other calculations with any of these sfs, you could do that in advance or inside your geom_sf.
ggplot() +
geom_sf(data = distSF, fill = "gold", size = 0.1) +
geom_sf(data = stateSF, color = "white", fill = NA) +
geom_sf(data = countryborder, color = "blue", fill = NA)
Regarding trying to add one plot to another: ggplot2 works in layers, so you create a single base ggplot object, then add geometries on top of it. So you could make, for example, two valid plots:
state_plot <- ggplot(stateSF) +
geom_sf(color = "white", fill = NA)
country_plot <- ggplot(countryborder) +
geom_sf(color = "blue", fill = NA)
But you can't add them, because you would have 2 base ggplot objects. This should be the error you mentioned:
state_plot +
country_plot
#> Error: Don't know how to add country_plot to a plot
Instead, if you need to make a plot, then add something else on top of it, make the base ggplot, then add geometry layers, such as a geom_sf with a different set of data.
state_plot +
geom_sf(data = countryborder, fill = NA, color = "blue")
Created on 2018-10-29 by the reprex package (v0.2.1)
If you look at geom_sf(data=distSF) you'll see that it is a list made up of two elements - you want the first one which contains the layer information, so geom_sf(data = distSF, fill = "gold")[[1]] should work.
districts <- ggplot() +
geom_sf(data = distSF, fill = "gold")
basicIndia$layers <- c(geom_sf(data = distSF, fill = "gold")[[1]], basicIndia$layers)

Add a box for the NA values to the ggplot legend for a continuous map

I have got a map with a legend gradient and I would like to add a box for the NA values. My question is really similar to this one and this one. Also I have read this topic, but I can't find a "nice" solution somewhere or maybe there isn't any?
Here is an reproducible example:
library(ggplot2)
map <- map_data("world")
map$value <- setNames(sample(-50:50, length(unique(map$region)), TRUE),
unique(map$region))[map$region]
map[map$region == "Russia", "value"] <- NA
ggplot() +
geom_polygon(data = map,
aes(long, lat, group = group, fill = value)) +
scale_fill_gradient2(low = "brown3", mid = "cornsilk1", high = "turquoise4",
limits = c(-50, 50),
na.value = "black")
So I would like to add a black box for the NA value for Russia. I know, I can replace the NA's by a number, so it will appear in the gradient and I think, I can write a workaround like the following, but all this workarounds do not seem like a pretty solution for me and also I would like to avoid "senseless" warnings:
ggplot() +
geom_polygon(data = map,
aes(long, lat, group = group, fill = value)) +
scale_fill_gradient2(low = "brown3", mid = "cornsilk1", high = "turquoise4",
limits = c(-50, 50),
na.value = "black") +
geom_point(aes(x = -100, y = -50, size = "NA"), shape = NA, colour = "black") +
guides(size = guide_legend("NA", override.aes = list(shape = 15, size = 10)))
Warning messages:
1: Using size for a discrete variable is not advised.
2: Removed 1 rows containing missing values (geom_point).
One approach is to split your value variable into a discrete scale. I have done this using cut(). You can then use a discrete color scale where "NA" is one of the distinct colors labels. I have used scale_fill_brewer(), but there are other ways to do this.
map$discrete_value = cut(map$value, breaks=seq(from=-50, to=50, length.out=8))
p = ggplot() +
geom_polygon(data=map, aes(long, lat, group=group, fill=discrete_value)) +
scale_fill_brewer(palette="RdYlBu", na.value="black") +
coord_quickmap()
ggsave("map.png", plot=p, width=10, height=5, dpi=150)
Another solution
Because the original poster said they need to retain the color gradient scale and the colorbar-style legend, I am posting another possible solution. It has 3 components:
We need to trick ggplot into drawing a separate color scale by using aes() to map something to color. I mapped a column of empty strings using aes(colour="").
To ensure that we do not draw a colored boundary around each polygon, I specified a manual color scale with a single possible value, NA.
Finally, guides() along with override.aes is used to ensure the new color legend is drawn as the correct color.
p2 = ggplot() +
geom_polygon(data=map, aes(long, lat, group=group, fill=value, colour="")) +
scale_fill_gradient2(low="brown3", mid="cornsilk1", high="turquoise4",
limits=c(-50, 50), na.value="black") +
scale_colour_manual(values=NA) +
guides(colour=guide_legend("No data", override.aes=list(colour="black")))
ggsave("map2.png", plot=p2, width=10, height=5, dpi=150)
It's possible, but I did it years ago. You can't use guides. You have to set individually the continuous scale for the values as well as the discrete scale for the NAs. This is what the error is telling you and this is how ggplot2 works. Did you try using both scale_continuous and scale_discrete since your set up is rather awkward, instead of simply using guides which is basically used for simple plot designs?

Edit boundaries in a map with ggplot2

I have created a map with ggplot2. So far I am happy with my plot but I was wondering whether it is possible to edit the boundaries that separate each of the regions within the map. I know that adding geom_path() you can explicitly incorporate them but I have not figured out how to edit the level of thickness. Is anyone familiar with how to do this? Many thanks in advance.
The code I have used is the following:
library(ggplot2)
library(RColorBrewer)
ex_sector = ggplot(data_mapping, aes(long, lat, group=group, fill = entry_cat)) +
scale_fill_brewer(type = "seq", palette = "Greens") + geom_polygon() + geom_path(colour = "black")
ggsave("test_fill.png", ex_sector, scale = 0.5)
An the output is:

Adding shaded target region to ggplot2 barchart

I have two data frames: one I am using to create the bars in a barchart and a second that I am using to create a shaded "target region" behind the bars using geom_rect.
Here is example data:
test.data <- data.frame(crop=c("A","B","C"), mean=c(6,4,12))
target.data <- data.frame(crop=c("ONE","TWO"), mean=c(31,12), min=c(24,9), max=c(36,14))
I start with the means of test.data for the bars and means of target.data for the line in the target region:
library(ggplot2)
a <- ggplot(test.data, aes(y=mean, x=crop)) + geom_hline(aes(yintercept = mean, color = crop), target.data) + geom_bar(stat="identity")
a
So far so good, but then when I try to add a shaded region to display the min-max range of target.data, there is an issue. The shaded region appears just fine, but somehow, the crops from target.data are getting added to the x-axis. I'm not sure why this is happening.
b <- a + geom_rect(aes(xmin=-Inf, xmax=Inf, ymin=min, ymax=max, fill = crop), data = target.data, alpha = 0.5)
b
How can I add the geom_rect shapes without adding those extra names to the x-axis of the bar-chart?
This is a solution to your question, but I'd like to better understand you problem because we might be able to make a more interpretable plot. All you have to do is add aes(x = NULL) to your geom_rect() call. I took the liberty to change the variable 'crop' in add.data to 'brop' to minimize any confusion.
test.data <- data.frame(crop=c("A","B","C"), mean=c(6,4,12))
add.data <- data.frame(brop=c("ONE","TWO"), mean=c(31,12), min=c(24,9), max=c(36,14))
ggplot(test.data, aes(y=mean, x=crop)) +
geom_hline(data = add.data, aes(yintercept = mean, color = brop)) +
geom_bar(stat="identity") +
geom_rect(data = add.data, aes(xmin=-Inf, xmax=Inf, x = NULL, ymin=min, ymax=max, fill = brop),
alpha = 0.5, show.legend = F)
In ggplot calls all of the aesthetics or aes() are inherited from the intial call:
ggplot(data, aes(x=foo, y=bar)).
That means that regardless of what layers I add on geom_rect(), geom_hline(), etc. ggplot is looking for 'foo' to assign to x and 'bar' to assign to y, unless you specifically tell it otherwise. So like aeosmith pointed out you can clear all inherited aethesitcs for a layer with inherit.aes = FALSE, or you can knock out single variables at a time by reassigning them as NULL.

Resources