Making Dot Density Maps

library(dots)
library(ggplot2)
library(sf)
#> Linking to GEOS 3.9.1, GDAL 3.3.2, PROJ 7.2.1; sf_use_s2() is TRUE

The dots Package

Dot density maps are useful to display the relative distribution of people across geographies. These maps are often used in election research and litigation to highlight the distribution of voters where choropleths tell a different story. dots simplifies this process, allowing for a tidy, simple interface. Maps can be created automatically with dots::dots(). The points behind these maps can be generated with dots::dots_points().

Now, various methods for creating random points exist in different R ecosystems, notably in terra, sf, and sp. Each of these different inputs and produce completely different outputs. To standardize these to one interface, dots creates a set of wrapper engine_* functions. Each engine_* function takes the same arguments: shp and col. Shape is an sf tibble and col is the string name of a column in shp. Within each of the engine functions, additional work is done to standardize the outputs so that they will be largely comparable.

Making Dot Density Maps

To make a dot density map, we need a shapefile with populations, votes, or some other numeric attribute tied to each geography. Here, we load 2020 Census data for Suffolk County, MA, included with the dots package.

data("suffolk")

To make a basic dot density map for the Hispanic voting age population, we can run the following:

dots::dots(shp = suffolk, cols = vap_hisp)

This works because vap_hisp is a numeric column in suffolk. Any numeric column would work. We can even use multiple columns. cols uses tidyselect to select multiple columns. For example, to make a map using the White and Hispanic voting age population, we could do the following:

dots::dots(shp = suffolk, cols = c(vap_hisp, vap_white))

Now, the points here are a bit crowded, so we can raise the number of adults that each point stands for by increasing the divisor argument. The default is 250, but here we will raise it to 1250.

dots::dots(shp = suffolk, cols = c(vap_hisp, vap_white), divisor = 1250)

The color of the dots uses the color aesthetic, so we can change it using different scale_color functions:

dots::dots(shp = suffolk, cols = c(vap_hisp, vap_white), divisor = 1250) +
  scale_color_brewer(name = 'Race/Ethnicity', palette = 'Accent')

Now, this is kind of bland, but it is a ggplot output, so we can update it using any of the regular tools. For example, we can clean up the background and give it clearer labels.

dots::dots(shp = suffolk, cols = c(vap_hisp, vap_white), divisor = 1250) +
  scale_color_brewer(name = 'Race/Ethnicity', palette = 'Accent') + 
  labs(
    title = 'White and Hispanic Adults in Suffolk County, MA',
    caption = 'One point per 1250 adults.'
  ) + 
  theme_void()

The downside of using the automatic plot generated by dots::dots() is that it limits us to a few plot defaults. If we switch to dots::dots_points(), we can make more specific choices by generating the plot.

To make the same basic plot, we could run the following:

dots::dots_points(shp = suffolk, cols = c(vap_hisp, vap_white), divisor = 1250)  %>% 
  ggplot() +
  geom_sf(data = suffolk, fill = NA, color = 'black') +
  geom_sf(aes(color = dots_type)) +
  scale_color_brewer(name = 'Race/Ethnicity', palette = 'Accent') + 
  labs(
    title = 'White and Hispanic Adults in Suffolk County, MA',
    caption = 'One point per 1250 adults.'
  ) + 
  theme_void()

Then, we can customize additional aspects of it, again using ggplot2 functions.

dots::dots_points(shp = suffolk, cols = c(vap_hisp, vap_white), divisor = 1250)  %>% 
  dplyr::mutate(dots_type = dplyr::case_when(
    dots_type == 'vap_white' ~ 'White',
    dots_type == 'vap_hisp' ~ 'Hispanic'
  )) %>% 
  ggplot() +
  geom_sf(data = suffolk, fill = NA, color = 'black') + 
  geom_sf(aes(color = dots_type), size = 0.75) + # change size
  scale_color_brewer(name = 'Race/Ethnicity', palette = 'Accent') + 
  labs(
    title = 'White and Hispanic Adults in Suffolk County, MA',
    caption = 'One point per 1250 adults.'
  ) + 
  theme_void()