Skip to contents

Introduction

This vignette describes the process of generating gridmap layouts in gridmappr. Given a set of geographic point locations, gridmappr creates a grid (with stated row, column dimensions) and places each point in a grid cell such that the distance between points in geographic space and that within the grid space is minimised. After exploring various gridmap arrangements, this vignette demonstrates how those maps can be plotted for analysis: building up a polygon file for grid layout solutions to encode them as thematic maps and showing how geographically-arranged charts can be created using the allocation and with standard calls to ggplot2.

gridmappr is an R implementation of Jo Wood’s Observable notebooks on Linear Programming solvers and their application to the Gridmap Allocation problem.

Setup

The following libraries are required to run this vignette:

Generate allocation with points_to_grid()

The main allocation function in gridmappr is points_to_grid(). This will return grid cell positions (row and column identifiers) for a given set of geographic locations. It is parameterised with:

  • pts A tibble of geographic points (x,y) to be allocated to a grid.
  • n_row Maximum number of rows in grid.
  • n_col Maximum number of columns in grid.
  • compactness Optional parameter between 0 and 1 where 0 allocates towards edges, 0.5 preserves scaled geographic location and 1 allocates towards centre of grid. Default is 1 (compact cluster).
  • spacers Optional list of grid cell locations defining grid location of fixed spacers which cannot be allocated points. Coordinates are in (row, column) order with origin (1,1) in bottom-left. Default is an empty list.

London Boroughs

For generating a gridmap layout of 33 London boroughs, we try an 8x8 regular grid.

  • n_row Set to 8.
  • n_col Set to 8.
  • compactness Set to .6, attempting to preserve the geographic layout with a degree of compactness around the grid centre.
n_row <- 8
n_col <- 8
pts <- london_boroughs |>
  st_drop_geometry() |>
  select(area_name, x = easting, y = northing)
solution <- points_to_grid(pts, n_row, n_col, compactness = .6)

gridmappr allows for spacers (light grey) to be specified: grid cells that further constrain the distribution by not allowing points to be allocated to them. Adding some targeted spacers, we can get close to the LondonSquared layout.

n_row <- 7
n_col <- 8
spacers <- list(
  c(1, 3), c(1, 5), c(1, 6),
  c(2, 2), c(2, 7),
  c(3, 1),
  c(6, 1), c(6, 2), c(6, 7), c(6, 8),
  c(7, 2), c(7, 3), c(7, 4), c(7, 6), c(7, 7)
)
pts <- london_boroughs |>
  st_drop_geometry() |>
  select(area_name, x = easting, y = northing)
solution <- points_to_grid(pts, n_row, n_col, compactness = 1, spacers)

US States

There are other instances where some manual control over the allocation is desirable – including Alaska, Hawaii and Puerto Rico in the grid of US states for example.

n_row <- 7
n_col <- 12
pts <- us_states |>
  st_drop_geometry() |>
  select(STUSPS, x, y)
solution <- points_to_grid(pts, n_row, n_col, compactness = .8)

Again this can be addressed by judiciously inserting spacers.

n_row <- 7
n_col <- 12
spacers <- list(
  c(4, 2), c(4, 3),
  c(3, 5), c(3, 4), c(3, 3), c(3, 12), c(3, 11),
  c(2, 4), c(2, 5), c(2, 6), c(2, 7), c(2, 8)
)
pts <- us_states |>
  st_drop_geometry() |>
  select(STUSPS, x, y)
solution <- points_to_grid(pts, n_row, n_col, compactness = .9, spacers)

Leicestershire Wards

Geographies with ‘holes’ are a particular challenge for grid layouts. By setting the compactness to zero, allocations are pushed to the edge of the grid, preserving the internal space containing the separate City of Leicester.

n_row <- 14
n_col <- 14
pts <- leics_wards |>
  st_drop_geometry() |>
  select(ward_name, x = easting, y = northing)
solution <- points_to_grid(pts, n_row, n_col, compactness = 0)

Building gridded polygon objects with make_grid()

Once a layout has been generated, you may wish to create a corresponding polygon object so that the layout can be mapped. This can be achieved with make_grid().

The function takes an sf data frame of ‘real’ geography and returns an sf data frame representing a grid, with variables identifying column and row IDs (bottom left is origin) and geographic centroids of grid squares. The gridded object can then be joined on a gridmap solution returned from (points_to_grid()) in order to create an object in which each grid cell corresponds to a gridmap allocation position.

make_grid() takes the following arguments:

  • sf_file An sf object to pass grid over.
  • n_row Number of rows in grid.
  • n_col Number of columns in grid.

Create layout solution and grid object

First we generate a gridmap layout solution (named solution) for Leicestershire wards.

n_row <- 14
n_col <- 14
pts <- leics_wards |>
  st_drop_geometry() |>
  select(ward_name, x = easting, y = northing)
solution <- points_to_grid(pts, n_row, n_col, 0)

Next, create the gridded polygon object using make_grid(). Supply the sf file of Leicestershire polygons (leics_wards, comes with gridmappr) and the dimensions of the grid, the same as those used to generate the layout solution.

grid <- make_grid(leics_wards, n_row, n_col)

Plot grid object using geom_sf()

Cells of the grid that have a ward allocated to them can then be isolated by joining the gridded object (grid) on the layout solution.

Below we plot the gridded sf object using ggplot2’s geom_sf geom.

grid |>
  left_join(solution) |>
  mutate(is_alloc = !is.na(ward_name)) |>
  ggplot() +
  geom_sf(fill = "transparent", colour = "#EEEEEE", linewidth = .6) +
  geom_text(aes(x = x, y = y, label = str_extract(ward_name, "^.{3}")), size = 4, colour = "#451C14")

Show displacement vectors with odvis::get_trajectory()

When evaluating different layout solutions, you may wish to encode explicitly the geographic distortion introduced by grid layouts. In the example, displacement vectors are drawn connecting the centroid of each ward in Leicestershire in real and grid space. Note that the vectors start straight at the real (origin) geographies and curve towards the grid (destination) geographies. This is achieved with get_trajectory() from odvis, a package providing helper functions for parameterising flow maps in gglot2.

First install odvis.

# install.packages("devtools")
devtools::install_github("rogerbeecham/odvis")

Next combine the grid and leics_wards (real geography) objects into a single simple features data frame.

leics_geogs <- bind_rows(
  leics_wards |> mutate(type = "real") |> select(ward_name, x = easting, y = northing, type),
  gridmap |> left_join(solution) |> mutate(type = "grid") |> select(ward_name, x, y, type, geometry = geom)
)

Both the origin (real) and destination (grid) locations are contained in the merged leics_wards dataset. Control points, which affect the path of the vectors so that they curve towards the destination, are generated using odvis::get_trajectory(), which implements the parameterisation published in Wood et al. 2011. In the code below we map() over each real-to-grid location pair calling get_trajectory() to generate a data frame of trajectories – origins, destinations and control points.

trajectories <- leics_geogs |>
  st_drop_geometry() |>
  filter(!is.na(ward_name)) |>
  pivot_wider(names_from = type, values_from = c(x, y)) |>
  mutate(id = row_number()) |>
  nest(data = c(ward_name, x_real, y_real, x_grid, y_grid)) |>
  mutate(
    trajectory = map(data, ~ get_trajectory(.x$x_real, .x$y_real, .x$x_grid, .x$y_grid, .x$ward_name))
  ) |>
  select(trajectory) |>
  unnest(cols = trajectory)

Finally trajectories are plotted with ggforce::geom_bezier0(), with separate lines (group=) for each real-to-grid OD pair.

ggplot() +
  geom_sf(data = leics_geogs |> mutate(type = factor(type, levels = c("real", "grid"))), aes(fill = type, colour = type), linewidth = .3) +
  ggforce::geom_bezier0(data = trajectories, aes(x = x, y = y, group = od_pair), colour = "#451C14", linewidth = .2) +
  scale_fill_manual(values = c("#F1DDD1", "transparent"), guide = "none") +
  scale_colour_manual(values = c("#FFFFFF", "#d9d9d9"), guide = "none")