Create random points based in distance and boundary conditions - r

In my example, I have:
# Packages
library(sf)
library(ggplot2)
# Create some points
set.seed(1)
df <- data.frame(
gr = c(rep("a",5),rep("b",5)),
x = rnorm(10),
y = rnorm(10)
)
df <- st_as_sf(df,coords = c("x","y"),remove = F, crs = 4326)
df.laea = st_transform(
df,
crs = "+proj=laea +x_0=4600000 +y_0=4600000 +lon_0=0.13 +lat_0=0.24 +datum=WGS84 +units=m"
)
# Create a countour of the area
ch <- st_convex_hull(st_union(df.laea))
ggplot() +
geom_sf(data = ch, fill = "white", color = "black") +
geom_sf(data = df.laea,color = "black")
Now, I'd like to create 10 random points but the conditions are that this points must be inside the ch boundaries and a minimum distance of 10 meters of each df.laea points that exist inside this ch area.
Please, any help with it?

I think the only tricky thing here is that a simple st_difference() of your polygon and the buffered points will return ten polygons, each with one of the points removed. Thus you have to either use a for loop or reduce() to remove one buffered point after the other from the polygon. To use reduce() you have to transform the vector to a proper list of sf instead of an sfc vector. This is what I did below.
# Packages
library(sf)
library(ggplot2)
library(purrr)
ch_minus <- df.laea$geometry |>
st_buffer(10000) |>
{\(vec) map(seq_along(vec), \(x) vec[x])}() |> # Transform buffered points to reducible list
reduce(.init = ch, st_difference)
sampled_points <- st_sample(ch_minus, 10)
ch_minus |>
ggplot() +
geom_sf() +
geom_sf(data = sampled_points)

You can buffer the points by the distance you'd like, then intersect those polygons with the ch polygon. From there, use st_sample and the associated arguments to get the points you want.
Example code:
## buffer df.laea 10m
laea_buff <- st_buffer(df.laea, dist = 10000) #changed dist to 10km to make it noticable in plot
# area to sample from:
sample_area <- st_intersection(ch, laea_buff)
# sample above area, all within 10km of a point and inside the `ch` polygon
points <- st_sample(sample_area, size = 10)
#plotting:
ggplot() +
geom_sf(data = points, color = 'red') +
geom_sf(data = laea_buff, color = 'black', fill = NA) +
geom_sf(data = ch, color = 'black', fill = NA) +
geom_sf(data = sample_area, color = 'pink', fill = NA) +
geom_sf(data = df.laea, color = 'black', size = .5)
Created on 2023-02-14 by the reprex package (v2.0.1)

As a comment on the nice answer by shs: it is possible to first use a sf::st_combine() call on the df.laea object & merge the 10 points to a single multipoint geometry.
This, when buffered, will work as an input for the necessary sf::st_difference() call to form a sampling area with holes, removing the need for a for cycle / map & reduce call.
# Packages
library(sf)
library(ggplot2)
# Create some points
set.seed(1)
df <- data.frame(
gr = c(rep("a",5),rep("b",5)),
x = rnorm(10),
y = rnorm(10)
)
df <- st_as_sf(df,coords = c("x","y"),remove = F, crs = 4326)
df.laea = st_transform(
df,
crs = "+proj=laea +x_0=4600000 +y_0=4600000 +lon_0=0.13 +lat_0=0.24 +datum=WGS84 +units=m"
)
# merge 10 points to 1 multipoing
mod_laea <- df.laea %>%
st_combine()
# sampling area = difference between hull and buffered points
sampling_area <- mod_laea %>%
st_convex_hull() %>%
st_difference(st_buffer(mod_laea, 10000))
# sample over sampling area
sampled_points <- st_sample(sampling_area, 10)
# a visual overview
ggplot() +
geom_sf(data = sampling_area, fill = "white", color = "black") +
geom_sf(data = df.laea, color = "black") +
geom_sf(data = sampled_points, color = "red", pch = 4)

Related

R mapping - plot area boundaries + add layer plotting 2,500 metre buffers for a set of plot points

I have the below code which is intended to
a) Draw a base outline layer of all Middle Super Output Areas in the East of England
b) Generate 2,500 buffer boundaries around each plotted point from an imported dataset
c) Plot the buffer boundary layer over the base outline layer.
#Call necessary packages
library(tidyverse)
library (readxl)
library (openxlsx)
library(maptools)
library(classInt)
library(RColorBrewer)
library(sf)
library(tmap)
library(tmaptools)
library(geodata)
#Read in shape file for mapping
shp_name <- C:/Users/JWP/East of England/MSOA/Middle_Layer_Super_Output_Areas_December_2011_Generalised_Clipped_Boundaries_in_England_and_Wales.shp"
EofEMSOAsFinalList <- st_read(shp_name)%>%
st_as_sf
# Create union shape of polygons
union <- st_union(EofEMSOAsFinalList)
#Read in point location data
LocationData <- read_excel("C:/Users/JWP/LocationData.xlsx",
sheet = "Location Data")
#Geocode the address list with 2,500m boundaries around each point
LocationDataPlotted <- st_as_sf(LocationData, coords = c('Latitude', 'Longitude'), crs = 4326)
#Remove geometry
LocationDataPlotted2 <- LocationDataPlotted %>%
as.data.frame() %>%
# calculate around each point a buffer zone of 2,500m
mutate(buffer = st_buffer(geometry, dist = 2500)) %>%
select(-geometry) %>%
st_as_sf()
#Generate bounding box
mask_union <- union %>% as_tibble() %>%
mutate(bbox = st_as_sfc(st_bbox(c(xmin = -5.5, xmax = 9, ymax = 51.5, ymin = 42), crs = st_crs(4326)))) %>%
st_as_sf()
# compute difference between bounding box and union polygon to
# use as mask in the final layer
diff <- st_difference(mask_union$bbox, mask_union$geometry)
# Build map
OutputMap <-
# plot only shapes filled red
tm_shape(EofEMSOAsFinalList) +
tm_fill(col = "red") +
# plot only buffer zones of each point in green
tm_shape(LocationDataPlotted2)+
tm_fill(col = "forestgreen") +
# add mask
tm_shape(diff) +
tm_fill(col = "white") +
# plot borders of shape
tm_shape(EofEMSOAsFinalList) +
tm_borders(col = "white",
lwd = 1,
lty = "solid") +
# add custom legend
tm_add_legend(type = "symbol",
labels = c("Not Within 2500m", "Within 2500m"),
col = c("red", "forestgreen"),
title = "Access type",
size = 1.5,
shape = 21)
The correct output should therefore look similar to the above:
However I am now getting output like the below:
Can anyone please amend the above so it works correctly?
Many thanks

R mapping (ideally TMAP) - draw polygon of area(s) that are not within 500 metres of any data points within dataset

I have a dataset with several hundred geographical points expressed as lat/long values that I plot as dots using tm_dots, on top of some boundaries that I plot using tm_shape (both using tmap).
Does anyone know of a way that I can draw polygons to represent areas within the boundaries of the underlying layer that are not within 500 metres of any of the points plotted? I'd be happy to use other R mapping resources (e.g. ggplot/ggmap) if better for this task.
Current code is:
#Call necessary packages
library(tidyverse)
library (readxl)
library(maptools)
library(classInt)
library(RColorBrewer)
library(sf)
library(tmap)
library(scales)
library(tmaptools)
library(geodata)
#Read in boundary polygon data
#This shape file is from https://www.data.gov.uk/dataset/2cf1f346-2f74-4c06-bd4b-30d7e4df5ae7/middle-layer-super-output-area-msoa-boundaries
shp_name <- "//ims.gov.uk//homedrive//users//JW2002//My Documents//Data//Demography, Mapping & Lookups//Shape Files//East of England//MSOA//Middle_Layer_Super_Output_Areas_December_2011_Generalised_Clipped_Boundaries_in_England_and_Wales.shp"
EofEMSOAs <- st_read(shp_name)%>%
st_as_sf()
#Read deprivation data from another source (not specifically relevant to the mapping section of this project but provides list for subsequent subset to East of England MSOAs only)
EofEMSOAsIMD <- read_excel("~/Data/Demography, Mapping & Lookups/IoD/National & EofE IoD 2019/National&IoD 2019 MSOAs.xlsx",
sheet = "East of England MSOAs")
#Subset MSOA list to East of England Only
EofEMSOAsCodeListOnly <- dplyr::pull(EofEMSOAsIMD, "Area Code")
EofEMSOAsCodeListOnly <- paste(EofEMSOAsCodeListOnly, collapse = '|')
EofEMSOAsFinalList <- EofEMSOAs[grep(EofEMSOAsCodeListOnly, EofEMSOAs$msoa11cd),]
#Generate point data
PointData <- read.table(textConnection("ID Latitude Longitude
A 52.9742585 0.5526301
B 52.972643 0.8495693
C 52.972643 0.8495693
D 51.46133804 0.36403501"), header=TRUE)
#Geocode the point list
PointDataPlotted = st_as_sf(PointData, coords = c('Longitude', 'Latitude'), crs = 4326)
#Remove geometry
PointDataPlotted2 <- PointDataPlotted %>%
as.data.frame() %>%
mutate(buffer = st_buffer(geometry, dist = 5000)) %>%
select(-geometry) %>%
st_as_sf()
#Create union shape of polygons
union <- st_union(EofEMSOAsFinalList)
# generate bounding box
mask_union <- union %>% as_tibble() %>%
mutate(bbox = st_as_sfc(st_bbox(c(xmin = -5.5, xmax = 9, ymax = 51.5, ymin = 42), crs = st_crs(4326)))) %>%
st_as_sf()
# compute difference between bounding box and union polygon to
# use as mask in the final layer
diff <- st_difference(mask_union$bbox, mask_union$geometry)
# Build map
OutputMap <-
# plot only shapes filled red
tm_shape(EofEMSOAsFinalList) +
tm_fill(col = "red") +
# plot only buffer zones of each point in green
tm_shape(PointDataPlotted2)+
tm_fill(col = "forestgreen") +
# add mask
tm_shape(diff) +
tm_fill(col = "white") +
# plot borders of shape
tm_shape(EofEMSOAsFinalList) +
tm_borders(col = "white",
lwd = 1,
lty = "solid") +
# add custom legend
tm_add_legend(type = "symbol",
labels = c("Restricted", "Public"),
col = c("red", "forestgreen"),
title = "Access type",
size = 1.5,
shape = 21)
Here's a solution to find areas within the boundaries of the underlying layer that are within 50 km of any of the points plotted.
library(tidyverse)
library(sf)
library(geodata)
# example polygons of France
polygon <- gadm(country = "FRA", level = 1, path = tempdir()) %>%
st_as_sf() %>%
filter(NAME_1 != "Corse")
# get 100 sample points within union shape of polygons
set.seed(42)
union <- st_union(polygon)
points <- st_sample(x = union, size = 100, type = "random") %>%
as.data.frame() %>%
mutate(id = row_number()) %>% # add an id for later joining
st_as_sf() %>%
# calculate aound each point a buffer zone of 50km
mutate(buffer = st_buffer(geometry, dist = 50000))
# add for each point the polygon (state) in which it is located
points <- st_join(points, polygon, join = st_within) %>%
as.data.frame() %>%
dplyr::select(id, NAME_1) %>%
left_join(points) %>%
filter(NAME_1 != "Corse")
# for each polygon calculate the union shapes of the
# corresponding buffers zones within
points_buff_union <- points %>%
dplyr::select(-geometry) %>%
st_as_sf() %>%
group_by(NAME_1) %>%
summarise()
# plot content
polygon %>%
ggplot() +
geom_sf(data = points_buff_union, aes(geometry = buffer, fill = NAME_1)) +
geom_sf(fill = NA) +
scale_fill_brewer(palette = "Paired") +
geom_sf(data = points, aes(geometry = geometry), color = "black", size = .5)
From this point I guess it's easy to find the areas not within XX meters of any of the points plotted.
If you want to find area across the underlying polygons you can simply use the following (blue areas are within 50 km of any of the points plotted while red areas don't):
# calculate union shape for all buffers
points_buff_union <- points %>%
filter(NAME_1 != "Corse") %>%
dplyr::select(-geometry) %>%
st_as_sf() %>%
summarise()
# generate bounding box
mask_union <- union %>% as_tibble() %>%
mutate(bbox = st_as_sfc(st_bbox(c(xmin = -5.5, xmax = 9, ymax = 51.5, ymin = 42), crs = st_crs(4326)))) %>%
st_as_sf()
# compute difference between bounding box and union polygon to
# use as mask in the final layer
diff <- st_difference(mask_union$bbox, mask_union$geometry)
# plot content
polygon %>%
ggplot() +
geom_sf(fill = "red3") +
geom_sf(data = points_buff_union, aes(geometry = buffer), fill = "lightblue") +
geom_sf(data = points, aes(geometry = geometry), color = "black", size = .5) +
geom_sf(fill = NA) +
geom_sf(data = diff, fill = "white")
Of course you can plot the individual layers computed with sf also using tmap:
library(tidyverse)
library(sf)
library(geodata)
library(tmap)
# example polygons of France
EofEMSOAs <- gadm(country = "FRA", level = 1, path = tempdir()) %>%
st_as_sf() %>%
filter(NAME_1 != "Corse")
# get 100 sample points within union shape of polygons
set.seed(42)
union <- st_union(EofEMSOAs)
PointDataPlot <- st_sample(x = union, size = 100, type = "random") %>%
as.data.frame() %>%
# calculate around each point a buffer zone of 50km
mutate(buffer = st_buffer(geometry, dist = 50000)) %>%
select(-geometry) %>%
st_as_sf()
# generate bounding box
mask_union <- union %>% as_tibble() %>%
mutate(bbox = st_as_sfc(st_bbox(c(xmin = -5.5, xmax = 9, ymax = 51.5, ymin = 42), crs = st_crs(4326)))) %>%
st_as_sf()
# compute difference between bounding box and union polygon to
# use as mask in the final layer
diff <- st_difference(mask_union$bbox, mask_union$geometry)
# Build map
OutputMap <-
# plot only shapes filled red
tm_shape(EofEMSOAs) +
tm_fill(col = "red") +
# plot only buffer zones of each point in green
tm_shape(PointDataPlot)+
tm_fill(col = "forestgreen") +
# add mask
tm_shape(diff) +
tm_fill(col = "white") +
# plot borders of shape
tm_shape(EofEMSOAs) +
tm_borders(col = "white",
lwd = 1,
lty = "solid") +
# add custom legend
tm_add_legend(type = "symbol",
labels = c("Restricted", "Public"),
col = c("red", "forestgreen"),
title = "Access type",
size = 1.5,
shape = 21)
UPDATE using UK data
#Call necessary packages
library(tidyverse)
library (readxl)
library(maptools)
library(classInt)
library(RColorBrewer)
library(sf)
library(tmap)
library(scales)
library(tmaptools)
library(geodata)
# Read in boundary polygon data
EofEMSOAs <- st_read("MSOA_EngWal_Dec_2011_Generalised_ClippedEW_0/Middle_Layer_Super_Output_Areas_December_2011_Generalised_Clipped_Boundaries_in_England_and_Wales.shp")%>%
st_as_sf(crs = 4326) %>%
st_make_valid() %>%
# use only a subset of the data
st_crop(c(xmin = 550000, ymin =320000, xmax = 600000, ymax = 360000))
# Generate point data
PointData <- read.table(textConnection("ID Latitude Longitude
A 52.9742585 0.5526301
B 52.972643 0.8495693
C 52.972643 0.8495693
D 51.46133804 0.36403501"), header=TRUE)
# Geocode the point list
PointDataPlotted = st_as_sf(PointData, coords = c('Longitude','Latitude'), crs = 4326)
# Remove geometry
PointDataPlotted2 <- PointDataPlotted %>%
as.data.frame() %>%
mutate(buffer = st_buffer(geometry, dist = 5000)) %>%
select(-geometry) %>%
st_as_sf(crs = 4326)
# Create union shape of polygons
union <- st_union(EofEMSOAs)
# generate bounding box
mask_union <- union %>% as_tibble() %>%
mutate(bbox = st_as_sfc(st_bbox(geometry), crs = 4326)) %>%
st_as_sf()
# compute difference between bounding box and union polygon to
# use as mask in the final layer
diff <- st_difference(mask_union$bbox, mask_union$geometry)
# Build map
OutputMap <-
# plot only shapes filled red
tm_shape(EofEMSOAs) +
tm_fill(col = "red") +
# plot only buffer zones of each point in green
tm_shape(PointDataPlotted2)+
tm_fill(col = "forestgreen") +
# add mask
tm_shape(diff) +
tm_fill(col = "white") +
# plot borders of shape
tm_shape(EofEMSOAs) +
tm_borders(col = "white",
lwd = 1,
lty = "solid") +
# add custom legend
tm_add_legend(type = "symbol",
labels = c("Restricted", "Public"),
col = c("red", "forestgreen"),
title = "Access type",
size = 1.5,
shape = 21)

R: Polar map projection of polygon data

What I have:
points in the arctic and antarctic
raster data from various geophysical entities in arctic and antarctic
What I want:
A map in stereographic or any other polar projection with background map or coastlines, cropped to the extent of the points. In other words: A map like above with base map of my own choice.
What I did so far:
I loaded all the data (including land surface data from naturalearthdata; see MWE), projected them into stereographic and plotted that. The result including the polygon data looks then like this:
My MWE:
library(raster)
library(sf)
library(ggplot2)
library(rgdal)
# file load ---------------------------------------------------------------
# sea ice raster data
if (!file.exists("seaiceraster.tif")) {
url = "https://seaice.uni-bremen.de/data/smos/tif/20100514_hvnorth_rfi_l1c.tif"
download.file(url, destfile = 'seaiceraster.tif')
}
si.raster = raster::raster('seaiceraster.tif')
# land surface shapefile
if (!file.exists("110m-admin-0-countries")) {
url_land = "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/physical/ne_10m_land.zip"
download.file(url_land, destfile = "110m-admin-0-countries")
unzip("110m-admin-0-countries")
}
world_shp = rgdal::readOGR("ne_10m_land.shp")
# points
p.data = structure(
list(
Lat = c(
73.0114126168676,70.325555278764,77.467797903163,
58.6423827457304,66.3616310851294,59.2097857474643,
75.3135274436283,60.1983078512275,72.6614399747201,
61.1566678672946,73.0822309615673,55.7759666826898,
75.1651656433833,69.0130753414173,62.3288262448589
),
Lon = c(
-59.9175490701543,-80.1900239630732,-40.4609968914928,
-61.0914448815381,-60.0703668488408,-21.027205418284,
-100.200463810276,-74.861777073788,-55.1093773178206,
-29.4108649230234,-64.5878251008461,-36.5343322019187,
-31.647365623387,-67.466355105829,-64.1162329769077
)
),
row.names = c(
1911L, 592L,2110L,3552L,3426L,1524L,635L,4668L,
3945L,2848L,3609L,36L,4262L,3967L,2725L
),
class = "data.frame"
)
p = sf::st_as_sf(p.data, coords = c("Lon", "Lat"),
crs = "+init=epsg:4326")
# project -----------------------------------------------------------------
polar.crs = CRS("+init=epsg:3995")
si.raster.proj = projectRaster(si.raster, crs = polar.crs)
world_shp.proj = sp::spTransform(world_shp, polar.crs)
p.proj = sf::st_transform(p, polar.crs)
# preparation -------------------------------------------------------------
AG = ggplot2::fortify(world_shp.proj)
# make raster to data.frame
si.raster.df = si.raster.proj %>%
raster::crop(., p.proj) %>%
raster::rasterToPoints(., spatial = TRUE) %>%
as.data.frame(.)
colnames(si.raster.df) = c("val", "x", "y")
# plot --------------------------------------------------------------------
ggplot() +
# geom_polygon(data = AG, aes(long, lat, group = group)) + # un-comment to see
geom_raster(data = si.raster.df, aes(x = x, y = y, fill = val)) +
geom_sf(data = p.proj, color = "green", size = 3)
I've changed the workflow in your example a bit to add the stars package for the sea ice data, but I think it should get you what you're looking for. You'll need to adjust the crop size to expand it a little, as the points p are right on the edge of the plotted area. st_buffer might help with that.
I used the crs from the seaicebuffer.tif file for all of the objects.
The .tif file has a crs that I'm not able to easily transform on my computer. It seems to be able to use meters as a lengthunit and might be a polar stereographic (variant B) projection. The points & world data don't seem to have a problem transforming to it though, which is why I've used it throughout.
library(raster)
library(sf)
library(ggplot2)
library(rgdal)
library(stars)
si <- stars::read_stars('seaiceraster.tif')
world_sf = rgdal::readOGR("ne_10m_land.shp") %>%
st_as_sf() %>%
st_transform(st_crs(si))
# p <- ... same as example and then:
p <- st_transform(p, st_crs(si))
# get a bounding box for the points to crop si & world.
p_bbox <- st_bbox(p) %>%
st_as_sfc() %>%
st_as_sf() %>%
st_buffer(100000)
# crop si & world_sf to an area around the points (p)
world_cropped <- st_crop(world_sf, p_bbox)
si_cropped <- st_crop(si, p_bbox)
#Plot
ggplot() +
geom_sf(data = world_cropped,
color = 'black',
fill = 'NA',
size = .2) +
geom_stars(data = si_cropped) +
geom_sf(data = p, color = 'red') +
scale_fill_continuous(na.value = 0)
Ugly hack for the southern .tif that stars reads as factors:
si <- stars::read_stars('20150324_hvsouth_rfi_l1c.tif', NA_value = 0 )
si$"20150324_hvsouth_rfi_l1c.tif" <- as.numeric(si$"20150324_hvsouth_rfi_l1c.tif")
ggplot() + geom_stars(data = si)

r sf package centroid within polygon

I need to add labels to polygons and I normally use the centroid, however the centroid does not fall inside the polygon. I found this question Calculate Centroid WITHIN / INSIDE a SpatialPolygon but I'm using the sf package.
Below is a toy data
rm(list = ls(all = TRUE)) #start with empty workspace
library(sf)
library(tidyverse)
library(ggrepel)
pol <- st_polygon(list(rbind(c(144, 655),c(115, 666)
,c(97, 660),c(86, 640)
,c(83, 610),c(97, 583)
,c(154, 578),c(140, 560)
,c(72, 566),c(59, 600)
,c(65, 634),c(86, 678)
,c(145, 678),c(144, 655)))) %>%
st_sfc()
a = data.frame(NAME = "A")
st_geometry(a) = pol
a <- a %>%
mutate(lon = map_dbl(geometry, ~st_centroid(.x)[[1]]),
lat = map_dbl(geometry, ~st_centroid(.x)[[2]]))
ggplot() +
geom_sf(data = a, fill = "orange") +
geom_label_repel(data = a, aes(x = lon, y = lat, label = NAME))
which results in the following
The simple answer is to replace st_centroid with st_point_on_surface. This won't return the true centroid in cases where the centroid is inside the polygon.
a2 <- a %>%
mutate(lon = map_dbl(geometry, ~st_point_on_surface(.x)[[1]]),
lat = map_dbl(geometry, ~st_point_on_surface(.x)[[2]]))
ggplot() +
ggplot2::geom_sf(data = a2, fill = "orange") +
geom_label_repel(data = a2, aes(x = lon, y = lat, label = NAME))
Alternatively
If the polygon has a centroid that is inside the polygon, use that, otherwise, find a point within the polygon.
st_centroid_within_poly <- function (poly) {
# check if centroid is in polygon
centroid <- poly %>% st_centroid()
in_poly <- st_within(centroid, poly, sparse = F)[[1]]
# if it is, return that centroid
if (in_poly) return(centroid)
# if not, calculate a point on the surface and return that
centroid_in_poly <- st_point_on_surface(poly)
return(centroid_in_poly)
}
a3 <- a %>%
mutate(lon = map_dbl(geometry, ~st_centroid_within_poly(.x)[[1]]),
lat = map_dbl(geometry, ~st_centroid_within_poly(.x)[[2]]))
ggplot() +
ggplot2::geom_sf(data = a3, fill = "orange") +
geom_label_repel(data = a3, aes(x = lon, y = lat, label = NAME))
The function above st_centroid_within_polygon is adapted from the question you reference for the sf package. A more thorough review of how st_point_on_surface works can be found here.
Expanding on Mitch's answer because the st_centroid_within_poly function provided above only works on single polygons.
To use on multiple polygons, use:
st_centroid_within_poly <- function (poly) {
# check if centroid is in polygon
ctrd <- st_centroid(poly, of_largest_polygon = TRUE)
in_poly <- diag(st_within(ctrd, poly, sparse = F))
# replace geometries that are not within polygon with st_point_on_surface()
st_geometry(ctrd[!in_poly,]) <- st_geometry(st_point_on_surface(poly[!in_poly,]))
ctrd
}

Create shaded polygons around points with ggplot2

I saw yesterday this beautiful map of McDonalds restaurants in USA. I wanted to replicate it for France (I found some data that can be downloaded here).
I have no problem plotting the dots:
library(readxl)
library(ggplot2)
library(raster)
#open data
mac_do_FR <- read_excel("./mcdo_france.xlsx")
mac_do_FR_df <- as.data.frame(mac_do_FR)
#get a map of France
mapaFR <- getData("GADM", country="France", level=0)
#plot dots on the map
ggplot() +
geom_polygon(data = mapaFR, aes(x = long, y = lat, group = group),
fill = "transparent", size = 0.1, color="black") +
geom_point(data = mac_do_FR_df, aes(x = lon, y = lat),
colour = "orange", size = 1)
I tried several methods (Thiessen polygons, heat maps, buffers), but the results I get are very poor. I can't figure out how the shaded polygons were plotted on the American map. Any pointers?
Here's my result, but it did take some manual data wrangling.
Step 1: Get geospatial data.
library(sp)
# generate a map of France, along with a fortified dataframe version for ease of
# referencing lat / long ranges
mapaFR <- raster::getData("GADM", country="France", level=0)
map.FR <- fortify(mapaFR)
# generate a spatial point version of the same map, defining your own grid size
# (a smaller size yields a higher resolution heatmap in the final product, but will
# take longer to calculate)
grid.size = 0.01
points.FR <- expand.grid(
x = seq(min(map.FR$long), max(map.FR$long), by = grid.size),
y = seq(min(map.FR$lat), max(map.FR$lat), by = grid.size)
)
points.FR <- SpatialPoints(coords = points.FR, proj4string = mapaFR#proj4string)
Step 2: Generate a voronoi diagram based on store locations, & obtain the corresponding polygons as a SpatialPolygonsDataFrame object.
library(deldir)
library(dplyr)
voronoi.tiles <- deldir(mac_do_FR_df$lon, mac_do_FR_df$lat,
rw = c(min(map.FR$long), max(map.FR$long),
min(map.FR$lat), max(map.FR$lat)))
voronoi.tiles <- tile.list(voronoi.tiles)
voronoi.center <- lapply(voronoi.tiles,
function(l) data.frame(x.center = l$pt[1],
y.center = l$pt[2],
ptNum = l$ptNum)) %>%
data.table::rbindlist()
voronoi.polygons <- lapply(voronoi.tiles,
function(l) Polygon(coords = matrix(c(l$x, l$y),
ncol = 2),
hole = FALSE) %>%
list() %>%
Polygons(ID = l$ptNum)) %>%
SpatialPolygons(proj4string = mapaFR#proj4string) %>%
SpatialPolygonsDataFrame(data = voronoi.center,
match.ID = "ptNum")
rm(voronoi.tiles, voronoi.center)
Step 3. Check which voronoi polygon each point on the map overlaps with, & calculate its distance to the corresponding nearest store.
which.voronoi <- over(points.FR, voronoi.polygons)
points.FR <- cbind(as.data.frame(points.FR), which.voronoi)
rm(which.voronoi)
points.FR <- points.FR %>%
rowwise() %>%
mutate(dist = geosphere::distm(x = c(x, y), y = c(x.center, y.center))) %>%
ungroup() %>%
mutate(dist = ifelse(is.na(dist), max(dist, na.rm = TRUE), dist)) %>%
mutate(dist = dist / 1000) # convert from m to km for easier reading
Step 4. Plot, adjusting the fill gradient parameters as needed. I felt the result of a square root transformation looks quite good for emphasizing distances close to a store, while a log transformation is rather too exaggerated, but your mileage may vary.
ggplot() +
geom_raster(data = points.FR %>%
mutate(dist = pmin(dist, 100)),
aes(x = x, y = y, fill = dist)) +
# optional. shows outline of France for reference
geom_polygon(data = map.FR,
aes(x = long, y = lat, group = group),
fill = NA, colour = "white") +
# define colour range, mid point, & transformation (if desired) for fill
scale_fill_gradient2(low = "yellow", mid = "red", high = "black",
midpoint = 4, trans = "sqrt") +
labs(x = "longitude",
y = "latitude",
fill = "Distance in km") +
coord_quickmap()

Resources