How to create transparent slices for missing categories in scatterpie charts on maps?

I'm creating pie charts overlaid on a map using R with ggplot2, sf, and scatterpie. My point shapefile contains 58 cities with binary land use columns (retail, industrial, airport) where 1 = present and 0 = absent.

The issue is that cities with fewer land use types show pies with fewer slices (e.g., a city with only industrial land use shows a single-slice pie). I want all pie charts to have exactly 3 slices, where missing land use types appear as transparent slices for visual consistency.

# Load required libraries
library(sf)
library(ggplot2)
library(dplyr)
library(scatterpie)

# Read the shapefiles
world_cities <- read_sf("path/world_cities_filtered.shp")

# extract coordinates from the geometry column
coords <- st_coordinates(world_cities)
world_cities_df <- world_cities %>%
  st_drop_geometry() %>%
  mutate(
    lon = coords[, 1],
    lat = coords[, 2]
  )

# map with pie charts
map_plot <- ggplot() +
  theme_void() +
  theme(
    panel.grid.major = element_line(color = "darkgray", size = 0.3, linetype = 2),
    legend.position = "bottom",
    legend.title = element_text(size = 12, face = "bold"),
    legend.text = element_text(size = 10),
    plot.title = element_text(size = 16, face = "bold", hjust = 0.5),
    plot.subtitle = element_text(size = 12, hjust = 0.5)
  ) +
  coord_sf(expand = FALSE,
           datum = st_crs(countries)) +
  geom_scatterpie(data = world_cities_df,
                  aes(x = lon, y = lat),
                  cols = c("retail", "industrial", "airport"),
                  pie_scale = 1.5,  # Adjust this to change pie size
                  alpha = 0.8) +
  scale_fill_manual(values = c("retail" = "#E74C3C", 
                               "industrial" = "#3498DB", 
                               "airport" = "#2ECC71"),
                    name = "Archetype",
                    labels = c("Airport", "Industrial", "Retail"))

print(map_plot)

This approach creates very thin slices for missing categories, but they're still somewhat visible rather than truly transparent. Sample data:

> dput(world_cities)
structure(list(CITY_NAME = c("Shenzhen", "Santiago", "Lima", 
"Buenos Aires", "Sao Paulo", "Montevide", "Rio de Janeiro", 
"Calgary", "Los Angeles", "Dallas", "Mexico City", "Toronto", 
"Chicago", "Rome", "Cairo", "Athens", "Istanbul", "Jeddah", "Frankfurt", 
"Milan", "Vienna", "Munich", "Berlin", "Lahore", "Delhi", "Almaty", 
"Mumbai", "Pune", "Shanghai", "Wuhan", "Guangzhou", "Beijing", 
"Seoul", "Fukuoka", "Hong Kong", "Tokyo", "Osaka", "Brisbane", 
"Washington D.C.", "New York", "Caracas", "London", "Manchester", 
"Madrid", "Paris", "Amsterdam", "Geneva", "Warsaw", "Riyadh", 
"Dubai", "Abu Dhabi", "Baku", "Cape Town", "Dar es Salaam", "Nairobi", 
"Johannesburg", "Sydney", "Melbourne"), lu_num = c(2L, 2L, 2L, 
2L, 2L, 1L, 1L, 3L, 3L, 2L, 2L, 2L, 2L, 2L, 2L, 1L, 1L, 2L, 1L, 
3L, 1L, 1L, 1L, 2L, 2L, 3L, 2L, 2L, 1L, 2L, 2L, 1L, 2L, 3L, 2L, 
2L, 1L, 3L, 1L, 3L, 2L, 2L, 3L, 3L, 2L, 3L, 1L, 3L, 3L, 2L, 2L, 
2L, 2L, 2L, 2L, 2L, 3L, 3L), retail = c(0L, 0L, 1L, 1L, 1L, 0L, 
0L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 0L, 0L, 0L, 0L, 0L, 1L, 0L, 0L, 
0L, 0L, 0L, 1L, 0L, 0L, 0L, 1L, 0L, 0L, 0L, 1L, 0L, 0L, 0L, 1L, 
0L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 0L, 0L, 1L, 0L, 0L, 
0L, 1L, 1L, 1L), industrial = c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 
1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 
1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 
1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 
1L, 1L), airport = c(1L, 1L, 0L, 0L, 0L, 0L, 0L, 1L, 1L, 0L, 
0L, 0L, 0L, 0L, 1L, 0L, 0L, 1L, 0L, 1L, 0L, 0L, 0L, 1L, 1L, 1L, 
1L, 1L, 0L, 0L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 1L, 0L, 1L, 0L, 1L, 
1L, 1L, 0L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 0L, 1L, 1L
), geometry = structure(list(structure(c(114.052516072688, 22.6710752741631
), class = c("XY", "POINT", "sfg")), structure(c(-70.647515553854, 
-33.4750230512851), class = c("XY", "POINT", "sfg")), structure(c(-77.0450036007241, 
-12.0819959357647), class = c("XY", "POINT", "sfg")), structure(c(-58.4498336968446, 
-34.622496010243), class = c("XY", "POINT", "sfg")), structure(c(-46.6229965826814, 
-23.5809989994226), class = c("XY", "POINT", "sfg")), structure(c(-56.1699985882875, 
-34.9200000502336), class = c("XY", "POINT", "sfg")), structure(c(-43.4551855922148, 
-22.7215710345035), class = c("XY", "POINT", "sfg")), structure(c(-114.049997573253, 
51.0299999453473), class = c("XY", "POINT", "sfg")), structure(c(-118.250000641271, 
34.0000019590779), class = c("XY", "POINT", "sfg")), structure(c(-96.6636896048789, 
32.7637260006132), class = c("XY", "POINT", "sfg")), structure(c(-99.1275746461327, 
19.4270490779828), class = c("XY", "POINT", "sfg")), structure(c(-79.4126335823368, 
43.7207669366832), class = c("XY", "POINT", "sfg")), structure(c(-87.6412976068233, 
41.8265459875429), class = c("XY", "POINT", "sfg")), structure(c(12.519999338143, 
41.8799970439333), class = c("XY", "POINT", "sfg")), structure(c(31.250799318015, 
30.0779099967854), class = c("XY", "POINT", "sfg")), structure(c(23.6529993798512, 
37.9439999862214), class = c("XY", "POINT", "sfg")), structure(c(29.0060014026546, 
41.0660009627707), class = c("XY", "POINT", "sfg")), structure(c(39.173004319785, 
21.5430030712411), class = c("XY", "POINT", "sfg")), structure(c(8.66816131201369, 
50.1300000207709), class = c("XY", "POINT", "sfg")), structure(c(9.18999930279142, 
45.4730040647418), class = c("XY", "POINT", "sfg")), structure(c(16.3209784439172, 
48.2021190334445), class = c("XY", "POINT", "sfg")), structure(c(11.5429503873952, 
48.1409729869083), class = c("XY", "POINT", "sfg")), structure(c(13.3275693578572, 
52.5162689233538), class = c("XY", "POINT", "sfg")), structure(c(74.340999441186, 
31.5450000806422), class = c("XY", "POINT", "sfg")), structure(c(77.2166614428691, 
28.6666650214145), class = c("XY", "POINT", "sfg")), structure(c(76.9126234460844, 
43.2550619959582), class = c("XY", "POINT", "sfg")), structure(c(72.8260023344842, 
19.077002983341), class = c("XY", "POINT", "sfg")), structure(c(73.8522724138133, 
18.5357430029184), class = c("XY", "POINT", "sfg")), structure(c(121.473000419805, 
31.2479999383934), class = c("XY", "POINT", "sfg")), structure(c(114.279003280991, 
30.5730000363321), class = c("XY", "POINT", "sfg")), structure(c(113.293611306089, 
23.0961870216222), class = c("XY", "POINT", "sfg")), structure(c(116.388036416661, 
39.9061890457427), class = c("XY", "POINT", "sfg")), structure(c(126.935244328844, 
37.5423570795889), class = c("XY", "POINT", "sfg")), structure(c(130.401990296501, 
33.5799989714409), class = c("XY", "POINT", "sfg")), structure(c(114.176997333231, 
22.2740009886894), class = c("XY", "POINT", "sfg")), structure(c(139.809006365241, 
35.683002048058), class = c("XY", "POINT", "sfg")), structure(c(135.51900335441, 
34.6359960388313), class = c("XY", "POINT", "sfg")), structure(c(153.026001368553, 
-27.453995931682), class = c("XY", "POINT", "sfg")), structure(c(-76.9538336884421, 
38.8909080742766), class = c("XY", "POINT", "sfg")), structure(c(-73.9052366295063, 
40.7078640410705), class = c("XY", "POINT", "sfg")), structure(c(-66.8982775618213, 
10.4960429483843), class = c("XY", "POINT", "sfg")), structure(c(-0.178001676555652, 
51.4879109366984), class = c("XY", "POINT", "sfg")), structure(c(-2.26178068198436, 
53.4796649757786), class = c("XY", "POINT", "sfg")), structure(c(-3.69097169824494, 
40.4422200735065), class = c("XY", "POINT", "sfg")), structure(c(2.3549531482218, 
48.8582874334995), class = c("XY", "POINT", "sfg")), structure(c(4.89483932469335, 
52.3730429819271), class = c("XY", "POINT", "sfg")), structure(c(6.13400429687772, 
46.2020039324906), class = c("XY", "POINT", "sfg")), structure(c(21.0118773681439, 
52.2449460530621), class = c("XY", "POINT", "sfg")), structure(c(46.770003317039, 
24.6500009682933), class = c("XY", "POINT", "sfg")), structure(c(55.3290033394721, 
25.2710010701508), class = c("XY", "POINT", "sfg")), structure(c(54.3709984136918, 
24.4760040024004), class = c("XY", "POINT", "sfg")), structure(c(49.8159993038217, 
40.3239960652242), class = c("XY", "POINT", "sfg")), structure(c(18.4820043939735, 
-33.9789959226824), class = c("XY", "POINT", "sfg")), structure(c(39.2533472981898, 
-6.8173560640002), class = c("XY", "POINT", "sfg")), structure(c(36.8039973486453, 
-1.26999894459972), class = c("XY", "POINT", "sfg")), structure(c(28.0043104457209, 
-26.1789570809208), class = c("XY", "POINT", "sfg")), structure(c(151.028199398186, 
-33.8897699469433), class = c("XY", "POINT", "sfg")), structure(c(145.075104313526, 
-37.8529559698376), class = c("XY", "POINT", "sfg"))), n_empty = 0L, crs = structure(list(
    input = "WGS 84", wkt = "GEOGCRS[\"WGS 84\",\n    DATUM[\"World Geodetic System 1984\",\n        ELLIPSOID[\"WGS 84\",6378137,298.257223563,\n            LENGTHUNIT[\"metre\",1]]],\n    PRIMEM[\"Greenwich\",0,\n        ANGLEUNIT[\"degree\",0.0174532925199433]],\n    CS[ellipsoidal,2],\n        AXIS[\"latitude\",north,\n            ORDER[1],\n            ANGLEUNIT[\"degree\",0.0174532925199433]],\n        AXIS[\"longitude\",east,\n            ORDER[2],\n            ANGLEUNIT[\"degree\",0.0174532925199433]],\n    ID[\"EPSG\",4326]]"), class = "crs"), class = c("sfc_POINT", 
"sfc"), precision = 0, bbox = structure(c(xmin = -118.250000641271, 
ymin = -37.8529559698376, xmax = 153.026001368553, ymax = 53.4796649757786
), class = "bbox"))), row.names = c(NA, -58L), class = c("sf", 
"tbl_df", "tbl", "data.frame"), sf_column = "geometry", agr = structure(c(CITY_NAME = NA_integer_, 
lu_num = NA_integer_, retail = NA_integer_, industrial = NA_integer_, 
airport = NA_integer_), class = "factor", levels = c("constant", 
"aggregate", "identity")))

Is there a better method in scatterpie to create truly transparent slices for categories with value 0, while maintaining consistent 3-slice pie structure across all cities?

> sessionInfo()
R version 4.5.1 (2025-06-13 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 26100)

Matrix products: default
  LAPACK version 3.12.1

locale:
[1] LC_COLLATE=English_United States.utf8  LC_CTYPE=English_United States.utf8    LC_MONETARY=English_United States.utf8
[4] LC_NUMERIC=C                           LC_TIME=English_United States.utf8    

time zone: Europe/Bucharest
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] scatterpie_0.2.6 ggplot2_4.0.0    dplyr_1.1.4      sf_1.0-21       

loaded via a namespace (and not attached):
 [1] gtable_0.3.6       crayon_1.5.3       compiler_4.5.1     tidyselect_1.2.1   Rcpp_1.1.0         dichromat_2.0-0.1  tidyr_1.3.1       
 [8] ggfun_0.2.0        scales_1.4.0       R6_2.6.1           generics_0.1.4     classInt_0.4-11    yulab.utils_0.2.1  MASS_7.3-65       
[15] polyclip_1.10-7    tibble_3.3.0       units_0.8-7        DBI_1.2.3          pillar_1.11.0      RColorBrewer_1.1-3 rlang_1.1.6       
[22] fs_1.6.6           S7_0.2.0           cli_3.6.5          withr_3.0.2        magrittr_2.0.4     tweenr_2.0.3       class_7.3-23      
[29] digest_0.6.37      grid_4.5.1         rstudioapi_0.17.1  ggforce_0.5.0      rappdirs_0.3.3     lifecycle_1.0.4    vctrs_0.6.5       
[36] KernSmooth_2.23-26 proxy_0.4-27       glue_1.8.0         farver_2.1.2       e1071_1.7-16       purrr_1.1.0        tools_4.5.1       
[43] pkgconfig_2.0.3

What I did was to add three variables that is the reverse of the three land uses, e. g.

world_cities_df <- world_cities_df |>
  mutate(  
    across(retail:airport, \(x) as.integer(x == 0), .names = "non_{.col}")
  )

Now, of the six land use columns exactly three have a 1 for each city. Use all six columns for the pie chart but set the three artificial columns to a transparent color, e. g.

geom_scatterpie(data = world_cities_df,
              aes(x = lon, y = lat),
              cols = c("retail", "non_retail",
                       "industrial", "non_industrial",
                       "airport", "non_airport"),
              pie_scale = 1.5
            ) +
scale_fill_manual(values = c("retail" = "#E74C3CCC",                           
                           "industrial" = "#3498DBCC",                         
                           "airport" = "#2ECC71CC",
                           "non_industrial" = "#00000000",
                           "non_retail" = "#00000000",
                           "non_airport" = "#00000000"
                           ),
                name = "Archetype",
                breaks = c("airport", "industrial", "retail"), # only show these three in the legend
                labels = c("Airport", "Industrial", "Retail"))

This way, I couldn't set alpha in geom_scatterpie as this will overwrite the alpha value of the provided colors. In the above, alpha is set to 0.8 for the original columns (that's the "CC" at the end). So the full code is:

# Reproject to an equal-area projection (EPSG:8857)
countries_equal_area <- st_transform(countries, 8857)
world_cities_equal_area <- st_transform(world_cities, 8857)

coords <- st_coordinates(world_cities)
world_cities_df <- world_cities_equal_area %>%
  st_drop_geometry() %>%
  mutate(
    lon = coords[, 1],
    lat = coords[, 2]
  ) %>%
  # Create complementary "non_" columns for transparency
  mutate(
    across(c(retail, industrial, airport), ~ as.integer(.x == 0), .names = "non_{.col}")
  )

map_plot <- ggplot() +
  geom_sf(data = countries_equal_area, 
          fill = "#F1F1F1", 
          color = "lightgrey", 
          size = 0.1) + 
  theme_void() +
  theme(
    panel.grid.major = element_line(color = "darkgray", size = 0.3, linetype = 2),
    legend.position = "bottom",
    legend.title = element_text(size = 12, face = "bold"),
    legend.text = element_text(size = 10),
    plot.title = element_text(size = 16, face = "bold", hjust = 0.5),
    plot.subtitle = element_text(size = 12, hjust = 0.5)
  ) +
  coord_sf(expand = FALSE,
           datum = st_crs(world_cities)) +  # Adjusted to use CRS from your shapefile
  geom_scatterpie(data = world_cities_df,
                  aes(x = lon, y = lat),
                  cols = c("retail", "non_retail",
                           "industrial", "non_industrial",
                           "airport", "non_airport"),
                  pie_scale = 0.3) +  # Don't set alpha here
  scale_fill_manual(values = c(
    "retail" = "#1b9e77",         # Red with alpha ~0.8
    "industrial" = "#d95f02",     # Blue with alpha ~0.8
    "airport" = "#7570b3",        # Green with alpha ~0.8
    "non_retail" = "#00000000",     # Fully transparent
    "non_industrial" = "#00000000",
    "non_airport" = "#00000000"
  ),
  name = "Archetype",
  breaks = c("airport", "industrial", "retail"),  # Only show real categories in legend
  labels = c("Airport", "Industrial", "Retail")) + 
  theme(
    legend.position = "bottom",
    legend.direction = "horizontal",
    panel.grid.major = element_line(color = "lightgrey", linetype = "dashed")
  ) +
  guides(fill = guide_legend(title.position = "top", title.hjust = 0.5))

print(map_plot)

Original response on my Reddit post/question.

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.