st_centroid renders all labels on the same point - r

I'm trying to display labels on GIS polygon features in R using the st_centroid function in the sf library. Unfortunately, while the head() function seems to show that each polygon has different x and y coordinates associated with it, all labels get rendered overlapping at a single point on the map (which is apparently the centroid of one particular polygon). What am I doing wrong here?
Current code setup:
library("ggplot2")
library("sf")
sf::sf_use_s2(FALSE) #makes centroids not break
world <- st_read("C:/prgrm/gis/source/10m_land_and_islands.shp")
prov <- st_read("C:/prgrm/gis/edited ncm/ncm_provinces.shp")
prov <- cbind(prov, st_coordinates(st_centroid(prov))) #attaches centroids to 'prov' dataset
head(prov)
ggplot(data = world) +
geom_sf() +
geom_sf(data=prov, aes(fill="blue")) +
geom_text(data=prov, aes(X,Y, label=provname_r), size=5) +
coord_sf(xlim=c(-2000000,1000000),ylim=c(-1500000, 3000000), crs=st_crs(3310))

You may be better off with specifying the centroid placement via fun.geometry argument of the geom_sf_text() call / by the way the default is sf::st_point_on_surface() - which is a good default as it makes sure that the label is not placed inside a hole, should the polygon have one.
Consider this example, using the well known & much loved nc.shp shapefile that ships with {sf}.
library(sf)
library(ggplot2)
# in place of your world dataset
shape <- st_read(system.file("shape/nc.shp", package="sf")) # included with sf package
# in place of your prov dataset
ashe <- shape[1, ]
ggplot(data = shape) +
geom_sf() +
geom_sf(data = ashe, fill = "blue") +
geom_sf_text(data = ashe,
aes(label = NAME),
color = "red",
fun.geometry = st_centroid)

Related

Inconsistent behaviour of ggplot when adding points to sf maps

Using ggplot I want to add points to a sf map. Take this code:
ggplot(data = shapefile) +
geom_sf()+
geom_point(x = -70.67,y =-33.45, size = 10, colour = "red")
This code works fine for one of my shapefiles but not for another, and I'm not sure why. Here's the code's output with my first shapefile:
And here's the code's output with the second shapefile:
Potential reasons for why the second call is not recognizing the coordinates? The only difference I'm seeing between the two plots is that in the first, longitude and latitude are annotated numerically, and in the second, after their north/south and east/west orientation.
The inconsistent behavior is due to different projections for each shapefile. You typically need to supply the point locations that match the units of the projection you are using. You could either convert the points to your shapefile's projection, or, if you don't care if the data are in a geographic coordinate system, you can convert to 4326 which is lat/long on WGS84 datum.
Method 1: Maintaining your shapefile's projection. This method will convert your point(s) to a spatial sf data type, so you can just plot with geom_sf.
pts <- st_point(c(-70.67, -33.45)) %>% st_sfc() %>% st_as_sf(crs=st_crs(4326))
ggplot(data = shapefile) +
geom_sf() +
geom_sf(data = pts, size = 10, colour = "red")
Method 2: Converting the shapefile to EPSG 4326.
ggplot(data = shapefile %>% st_transform(st_crs(4326))) +
geom_sf() +
geom_point(x = -70.67,y =-33.45, size = 10, colour = "red")

Shapefile not filling properly with geom_polygon, inconsistent with QGIS/ArcMap

I've imported a shapefile of the world's oceans from Natural Earth to R via readOGR. When I try to render it in ggplot, it fills in land over N. & S. America. The behaviour is inconsistent with QGIS & ArcMap, both of which render & fill the shapefile just fine. Any ideas?
download.file("https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/physical/ne_50m_ocean.zip" , destfile="./ne_50m_ocean.zip")
system("unzip ./ne_50m_ocean.zip")
wrld <- readOGR(dsn=getwd(),layer="ne_50m_ocean")
wrld <- tidy(wrld)
ggplot() + geom_polygon(data = wrld, aes(x = long, y = lat, group = group), colour = "black", fill = "blue")
screenshot of RStudio render
screenshot of QGIS render
I was able to resolve this using read_sf() instead of readOGR(), then tweaking the ggplot code to accommodate. Also worked for the next bit of my workflow which involved rasterizing the sf object using fasterize() for a mask ocean layer (simplified demo code included in case useful to others):
#get data
download.file("https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/physical/ne_50m_ocean.zip" , destfile="./ne_50m_ocean.zip")
system("unzip ./ne_50m_ocean.zip")
wrld <- read_sf(dsn=getwd(),layer="ne_50m_ocean")
#plot sf object
ggplot() + geom_sf(data=wrld, colour = "black", fill = "blue")
#rasterize sf object
r <- raster(ncol=720, nrow=360)
extent(r) <- extent(wrld)
rp <- fasterize(wrld, r)
ocean <- as.data.frame(rasterToPoints(rp))
#plot raster object
ggplot() + geom_tile(data=ocean,aes(x=x,y=y),fill="white")

how map certain USDA hardiness zones in R

Has anyone been able to create maps of a selection of USDA hardiness zones in R, maybe with ggplot2 and sf packages? I'd specifically like to create a map with only zones 9b and higher in color .
I think some of the data to create the map is found here Prism Climate Group, but I am inexperienced and at a loss to know what to do with GIS data (file extensions SGML,XML,DBF, PRJ, SHP,SHX).
To elaborate a little bit on the answer by #niloc:
The USA looks more natural when shown in the Albers conical projection (Canada border slightly curved - like in the original image).
This can be achieved by using coord_sf(crs = 5070) in your {ggplot2} call.
The gist of the answer (downloading, unzipping & plotting via ggplot2::geom_sf()) remains unchanged).
library(sf)
library(tidyverse)
library(USAboundaries)
# Download and unzip file
temp_shapefile <- tempfile()
download.file('http://prism.oregonstate.edu/projects/public/phm/phm_us_shp.zip', temp_shapefile)
unzip(temp_shapefile)
# Read full shapefile
shp_hardness <- read_sf('phm_us_shp.shp')
# Subset to zones 9b and higher
shp_hardness_subset <- shp_hardness %>%
filter(str_detect(ZONE, '9b|10a|10b|11a|11b'))
# state boundaries for context
usa <- us_boundaries(type="state", resolution = "low") %>%
filter(!state_abbr %in% c("PR", "AK", "HI")) # lower 48 only
# Plot it
ggplot() +
geom_sf(data = shp_hardness_subset, aes(fill = ZONE)) +
geom_sf(data = usa, color = 'black', fill = NA) +
coord_sf(crs = 5070) +
theme_void() # remove lat/long grid lines
There is a lot going on in that map with all of the insets, the legend with F and C, states displayed over the CONUS. Would be better to narrow down your question.
But here is a start. The shapefile is composed of many files (XML, DBF, etc) but you only need to point read_sf() at the .shp file. Subsetting with an sf object can be done just like with a data.frame.
library(sf)
library(tidyverse)
# Download and unzip file
temp_shapefile <- tempfile()
download.file('http://prism.oregonstate.edu/projects/public/phm/phm_us_shp.zip', temp_shapefile)
unzip(temp_shapefile)
# Read full shapefile
shp_hardness <- read_sf('phm_us_shp.shp')
# Subset to zones 9b and higher
shp_hardness_subset <- shp_hardness %>%
filter(str_detect(ZONE, '9b|10a|10b|11a|11b'))
# Plot it
ggplot() +
geom_sf(data = shp_hardness_subset, aes(fill = ZONE)) +
geom_polygon(data = map_data("state"), # add states for context
aes(x=long, y=lat,group=group),
color = 'black',
fill = NA) +
theme_void() # remove lat/long grid lines

ggplot maps - unwanted horizontal lines when using coord_map

I've problem with projecting properly map of Europe when I use coord_map in ggplot package. The code below gives me a weird and unwanted horizontal lines in random places. Does anyone know how to overcome this problem?
I don't want to use coord_quickmap nor coord_cartesian because I want to preserve straight lines of countries.
library(ggplot2)
map.world <- map_data("world")
ggplot(map.world, aes(x = long, y = lat)) +
geom_polygon(mapping = aes(x = long, y = lat, group = group), fill = "#B3B1B5", color = "#D9D8DA",size = 0.4) +
theme_minimal() +
theme(axis.text = element_blank(), text = element_blank(), panel.grid = element_blank()) +
coord_map(xlim = c(-27,36), ylim = c(34,67))
There are two ways to work around your problem:
The straightforward way
This way just needs a little tweaking of your code (note that I use magrittr's forward pipes):
library(maps)
library(magrittr)
library(maptools)
library(broom)
library(ggplot2)
europe <- maps::map("world", fill=TRUE, plot=FALSE) %>%
maptools::pruneMap(xlim = c(-27,36), ylim = c(34,67))
ggplot(data= broom::tidy(europe)) +
geom_polygon(mapping = aes(x = long, y = lat, group = group),
fill = "#B3B1B5", color = "#D9D8DA",size = 0.4) +
theme_void() +
coord_map()
The "owin/extent" approach
Another way to work around your problem can be done by using owin objects. This kind of objects will allow you to create a spatial window. Then you can represent only the intersection of such window over the world map.
Using this approach your code will be something as follows (also using magrittr's forward pipes and setting up a general CRS)
library(maps)
library(magrittr)
library(spatstat)
library(maptools)
library(raster)
library(broom)
library(ggplot2)
#Defining a general EPSG so there won't be any superposition problems
epsg <- "+proj=longlat +datum=WGS84 +no_defs"
#Using the original maps package, then converting map into SpatialPolygons object
map.world <- maps::map("world", fill=TRUE) %$%
maptools::map2SpatialPolygons(., IDs=names,proj4string=CRS(epsg))
#In order to keep the names of the countries we create the following data.frame
country.labs <- sapply(slot(map.world, "polygons"), function(x) slot(x, "ID")) %>%
data.frame( ID=1:length(map.world), name=., row.names = .)
#We convert object map.world into a SpatialPolygonsDataFrame object
map.world.SPDF <- sp::SpatialPolygonsDataFrame(map.world, country.labs)
#Creating owin object using your zooming coordinates
#This step always requires to load packages 'spatstat' and 'maptools'
zoom <- as(spatstat::as.owin(c(-27,36,34,67)), "SpatialPolygons")
raster::projection(zoom)=epsg
#Storing intersection between 'zoom' and 'world.map'
europe <- raster::intersect(map.world.SPDF, zoom)
#*country names of object europe can be accessed via europe#data
#Representing object 'europe' using broom::tidy to create a data.frame object
ggplot() +
geom_polygon(data = broom::tidy(europe, region="name"),
mapping = aes(x = long, y = lat, group = group),
fill = "#B3B1B5", color = "#D9D8DA",size = 0.4) +
theme_void() +
coord_map()
#*country names after tidying 'europe' this way are in a new column called 'id'
Depending on what you are doing, you may want to use
zoom <- as(raster::extent(c(-27,36,34,67)), "SpatialPolygons")
To create an extent object instead of an owin object (The result here is going to be the same)
The result obtained with any method is displayed below
In case you need to do different zooms you can easily wrap any of the alternatives up in a function.
I hope it helps
BONUS BALL: It might be interesting for you to check out the tmap package

Plotting roads from an osmar object on a ggplot map

I have created an elevation map from a raster object (elevation data from worldclim) of my study sites in China, using ggplot code (simplified version of the code). The relevant raster objects have been downloaded from worldclim.org and converted to a data.frame using the raster package. Here is a link to the data used for this plot.
# load library
library("tidyverse")
load(file = "gongga.RData")
ggplot() +
geom_raster(data = gongga, aes(x=x, y=y, fill = elev)) +
coord_equal() +
scale_fill_gradient(name = "Elevation", low = "grey0", high = "grey100") +
scale_x_continuous(expand = c(0,0)) +
scale_y_continuous(expand = c(0,0)) +
theme(aspect.ratio=1/1, text = element_text(size=15))
For clarity I would like to add roads to the map. I came across the osmar package that extracts roads from Openstreetmap.
Using code from here, I extract the roads for the right section, but I don't know how to plot them to my existing ggplot.
# EXTRACT ROADS FROM OPENSTREETMAP AND PLOT THEM WITH RANDOM POINTS
# Load libraries
library('osmar')
library('geosphere')
# Define the spatial extend of the OSM data we want to retrieve
moxi.box <- center_bbox(center_lon = 102.025, center_lat = 29.875,
width = 10000, height = 10000)
# Download all osm data inside this area
api <- osmsource_api()
moxi <- get_osm(moxi.box, source = api)
# Find highways
ways <- find(moxi, way(tags(k == "highway")))
ways <- find_down(moxi, way(ways))
ways <- subset(moxi, ids = ways)
# SpatialLinesDataFrame object
hw_lines <- as_sp(ways, "lines")
# Plot points
plot(hw_lines, xlab = "Lon", ylab = "Lat")
box()
Does the object need any transformation to plot it in ggplot?
Or is there a better solution than osmar package for my purpose?
You can fortify the SpatialLinesDataFrame and then plot that with ggplot
fortify(hw_lines) %>%
ggplot(aes(x = long, y = lat, group = group)) +
geom_path()
The group aesthetic stops ggplot from joining all the roads together into one long line.

Resources