r - How to display two sets of proportional symbols (Urban vs. Rural) with varying color gradients in ggplot2 and newggscale? -

admin2025-04-21  2

I'm trying to create a map in R using ggplot2 and the newggscale packages, where I show proportional symbols for both urban and rural locations, with varying color gradients depicting the size of the proportional symbol. My goal is to have:

  • Urban Locations shaded with blue, ranging from lighter blue (for lower counts) to darker blue (for higher counts.
  • Rural Locations shaded with red, ranging from lighter red (for lower counts) to darker red (for higher counts).

Currently, I'm facing an issue when using new_scale_color() or new_scale_fill() to manage multiple color gradients. Instead of showing just one legend for the proportional symbols and count, I'm getting that plus two DMA legends and no propoer color gradient for the urban and rural locations.

Here's what I've tried:

ok_cities_correct_clean <- ok_cities_correct_clean %>%
  mutate(
    color_gradient = case_when(
      # For Urban cities, custom color ranges based on COUNT
      CLASSIF == "Urban" & COUNT <= 10 ~ "lightblue",      # Urban: Counts 1-10
      CLASSIF == "Urban" & COUNT <= 20 ~ "lightskyblue",   # Urban: Counts 10-20
      CLASSIF == "Urban" & COUNT <= 30 ~ "dodgerblue",     # Urban: Counts 20-30
      CLASSIF == "Urban" & COUNT > 30 ~ "darkblue",        # Urban: Counts 30+
      
      # For Rural cities, custom color ranges based on COUNT
      CLASSIF == "Rural" & COUNT <= 10 ~ "lightpink",      # Rural: Counts 1-10
      CLASSIF == "Rural" & COUNT <= 20 ~ "lightcoral",     # Rural: Counts 10-20
      CLASSIF == "Rural" & COUNT <= 30 ~ "firebrick",      # Rural: Counts 20-30
      CLASSIF == "Rural" & COUNT > 30 ~ "darkred",         # Rural: Counts 30+
      
      TRUE ~ "gray"  # Default color for any unmatched cases
    )
  )

ggplot() +
  
  # Add OK counties (light grey fill, black borders)
  geom_sf(
    data = ok_counties_reproj,
    fill = "#f5f5f5",
    color = "black",
    size = 0  # No borders for counties in this layer
  ) +
  
  # Add DMA boundaries (Tulsa and Oklahoma City)
  geom_sf(
    data = ok_DMA,
    aes(fill = DMA, color = DMA),
    linewidth = 1.5,      # Set line width for borders
    alpha = 0.75       # Transparency for DMAs
  ) +
  
  # Set fill colors for Tulsa and Oklahoma City
  scale_fill_manual(
    values = c("Tulsa, OK" = "#4575b4", 
               "Oklahoma City, OK" = "#5ab4ac")
  ) +
  
  # Set border colors for Tulsa and Oklahoma City
  scale_color_manual(
    values = c("Tulsa, OK" = "#4575b4", 
               "Oklahoma City, OK" = "#5ab4ac")
  ) +
  
  # Add Oklahoma counties again with borders, but no fill
  geom_sf(
    data = ok_counties_reproj,
    fill = NA,
    color = "black",    # Keep the black border for counties
    size = 1            # Make the county borders visible
  ) +
  
  new_scale_color() +
  
  # Add cities with color_gradient based on urban/rural classification and their counts
  geom_sf(
    data = ok_cities_correct_clean %>%
      filter(COUNT > 0),  # Only cities with counts greater than 0
    aes(size = COUNT, fill = color_gradient),  # **Fill by `color_gradient`**
    show.legend = TRUE
  ) +
  
  # Adjust size scale for proportional symbols
  scale_size_continuous(
    range = c(1, 10),
    name = "Frequency of Mentions"
  )[![enter image description here][1]][1]

What I expect:

  • One proportional symbols legend with sizes corresponding to count.
  • A single color legend with urban showing shades of blue (light, medium, and dark) and rural showing shades of red (light, medium, dark).

What I'm getting:

  • Two DMA legends
  • No shading of colors based on count for the urban and rural classifications.

Question: How can I properly create a map with:

  1. ONE DMA legend
  2. Two color legends showing proportional symbols for urban locations in shades of blue and proportional symbols for rural locations in shades of red, each based on their respective counts.

UPDATE:

The code below (provided by @stefan) seemed to address my issues. However, I am now seeing duplicate DMA legends. One is correct (bottom of figure) and the other has the correct border but a gray fill (top of figure). I'd like to remove the duplicate DMA legend (top of figure), but when I do, it messes up other things in my map. Suggestions?

# Filter out zero counts and apply binning
ok_cities_correct_clean <- ok_cities_correct_clean %>%
  filter(COUNT > 0) %>%
  mutate(
    color_gradient = cut(
      COUNT,  # Cut the COUNT values into the specified ranges
      breaks = c(0, 10, 20, 30, Inf),  # Include 0 as the lower bound for first bin
      labels = c("1 - 10", "11 - 20", "21 - 30", "> 30"),  # Correct number of labels
      right = TRUE  # Ensure the upper bound is included in each bin
    )
  )

# Define color palettes
cities_urban <- c("lightblue", "lightskyblue", "dodgerblue", "darkblue")
names(cities_urban) <- labels


cities_rural <- c("lightpink", "lightcoral", "firebrick", "darkred")
names(cities_rural) <- labels



ggplot() +
  
  # Add OK counties
  geom_sf(
    data = ok_counties_reproj,
    fill = "#f5f5f5",
    color = "black",
    size = 0
  ) +
  
  # Add DMA 
  geom_sf(
    data = ok_DMA,
    aes(fill = DMA, color = DMA),
    linewidth = 1.5,
    alpha = 0.75,
    show.legend = c(fill = TRUE, color = FALSE)
  ) +
  
  # OK counties (border, no fill)
  geom_sf(
    data = ok_counties_reproj,
    fill = NA,
    color = "black",
    size = 1
  ) +
  
  # Set fill colors for DMA
  scale_fill_manual(
    values = c(
      "Tulsa, OK" = "#4575b4",
      "Oklahoma City, OK" = "#5ab4ac"
    ),
    aesthetics = c("fill", "color"),
    guide = guide_legend(override.aes = list(
      color = c("#5ab4ac", "#4575b4")
    ))
  ) +
  
  new_scale_color() +
  
  # Add cities with color_gradient based on urban/rural classification
  geom_sf(
    data = ok_cities_correct_clean %>%
      filter(COUNT > 0, CLASSIF == "Urban"),
    aes(size = COUNT, color = color_gradient)
  ) +
  
  scale_color_manual(
    values = cities_urban, na.value = "gray", name = "Urban"
  ) +
  
  new_scale_color() +
  
  # Add cities with color g radient based on urban/rural classification
  geom_sf(
    data = ok_cities_correct_clean %>%
      filter(COUNT > 0, CLASSIF == "Rural"),
    aes(size = COUNT, color = color_gradient)
  ) +
  
  scale_color_manual(
    values = cities_rural, na.value = "gray", name = "Rural"
  ) +
  
  scale_size_continuous(
    range = c(1, 10),
    name = "Frequency of Mentions"
  )
'''


  [1]: .png
  [2]: .png

I'm trying to create a map in R using ggplot2 and the newggscale packages, where I show proportional symbols for both urban and rural locations, with varying color gradients depicting the size of the proportional symbol. My goal is to have:

  • Urban Locations shaded with blue, ranging from lighter blue (for lower counts) to darker blue (for higher counts.
  • Rural Locations shaded with red, ranging from lighter red (for lower counts) to darker red (for higher counts).

Currently, I'm facing an issue when using new_scale_color() or new_scale_fill() to manage multiple color gradients. Instead of showing just one legend for the proportional symbols and count, I'm getting that plus two DMA legends and no propoer color gradient for the urban and rural locations.

Here's what I've tried:

ok_cities_correct_clean <- ok_cities_correct_clean %>%
  mutate(
    color_gradient = case_when(
      # For Urban cities, custom color ranges based on COUNT
      CLASSIF == "Urban" & COUNT <= 10 ~ "lightblue",      # Urban: Counts 1-10
      CLASSIF == "Urban" & COUNT <= 20 ~ "lightskyblue",   # Urban: Counts 10-20
      CLASSIF == "Urban" & COUNT <= 30 ~ "dodgerblue",     # Urban: Counts 20-30
      CLASSIF == "Urban" & COUNT > 30 ~ "darkblue",        # Urban: Counts 30+
      
      # For Rural cities, custom color ranges based on COUNT
      CLASSIF == "Rural" & COUNT <= 10 ~ "lightpink",      # Rural: Counts 1-10
      CLASSIF == "Rural" & COUNT <= 20 ~ "lightcoral",     # Rural: Counts 10-20
      CLASSIF == "Rural" & COUNT <= 30 ~ "firebrick",      # Rural: Counts 20-30
      CLASSIF == "Rural" & COUNT > 30 ~ "darkred",         # Rural: Counts 30+
      
      TRUE ~ "gray"  # Default color for any unmatched cases
    )
  )

ggplot() +
  
  # Add OK counties (light grey fill, black borders)
  geom_sf(
    data = ok_counties_reproj,
    fill = "#f5f5f5",
    color = "black",
    size = 0  # No borders for counties in this layer
  ) +
  
  # Add DMA boundaries (Tulsa and Oklahoma City)
  geom_sf(
    data = ok_DMA,
    aes(fill = DMA, color = DMA),
    linewidth = 1.5,      # Set line width for borders
    alpha = 0.75       # Transparency for DMAs
  ) +
  
  # Set fill colors for Tulsa and Oklahoma City
  scale_fill_manual(
    values = c("Tulsa, OK" = "#4575b4", 
               "Oklahoma City, OK" = "#5ab4ac")
  ) +
  
  # Set border colors for Tulsa and Oklahoma City
  scale_color_manual(
    values = c("Tulsa, OK" = "#4575b4", 
               "Oklahoma City, OK" = "#5ab4ac")
  ) +
  
  # Add Oklahoma counties again with borders, but no fill
  geom_sf(
    data = ok_counties_reproj,
    fill = NA,
    color = "black",    # Keep the black border for counties
    size = 1            # Make the county borders visible
  ) +
  
  new_scale_color() +
  
  # Add cities with color_gradient based on urban/rural classification and their counts
  geom_sf(
    data = ok_cities_correct_clean %>%
      filter(COUNT > 0),  # Only cities with counts greater than 0
    aes(size = COUNT, fill = color_gradient),  # **Fill by `color_gradient`**
    show.legend = TRUE
  ) +
  
  # Adjust size scale for proportional symbols
  scale_size_continuous(
    range = c(1, 10),
    name = "Frequency of Mentions"
  )[![enter image description here][1]][1]

What I expect:

  • One proportional symbols legend with sizes corresponding to count.
  • A single color legend with urban showing shades of blue (light, medium, and dark) and rural showing shades of red (light, medium, dark).

What I'm getting:

  • Two DMA legends
  • No shading of colors based on count for the urban and rural classifications.

Question: How can I properly create a map with:

  1. ONE DMA legend
  2. Two color legends showing proportional symbols for urban locations in shades of blue and proportional symbols for rural locations in shades of red, each based on their respective counts.

UPDATE:

The code below (provided by @stefan) seemed to address my issues. However, I am now seeing duplicate DMA legends. One is correct (bottom of figure) and the other has the correct border but a gray fill (top of figure). I'd like to remove the duplicate DMA legend (top of figure), but when I do, it messes up other things in my map. Suggestions?

# Filter out zero counts and apply binning
ok_cities_correct_clean <- ok_cities_correct_clean %>%
  filter(COUNT > 0) %>%
  mutate(
    color_gradient = cut(
      COUNT,  # Cut the COUNT values into the specified ranges
      breaks = c(0, 10, 20, 30, Inf),  # Include 0 as the lower bound for first bin
      labels = c("1 - 10", "11 - 20", "21 - 30", "> 30"),  # Correct number of labels
      right = TRUE  # Ensure the upper bound is included in each bin
    )
  )

# Define color palettes
cities_urban <- c("lightblue", "lightskyblue", "dodgerblue", "darkblue")
names(cities_urban) <- labels


cities_rural <- c("lightpink", "lightcoral", "firebrick", "darkred")
names(cities_rural) <- labels



ggplot() +
  
  # Add OK counties
  geom_sf(
    data = ok_counties_reproj,
    fill = "#f5f5f5",
    color = "black",
    size = 0
  ) +
  
  # Add DMA 
  geom_sf(
    data = ok_DMA,
    aes(fill = DMA, color = DMA),
    linewidth = 1.5,
    alpha = 0.75,
    show.legend = c(fill = TRUE, color = FALSE)
  ) +
  
  # OK counties (border, no fill)
  geom_sf(
    data = ok_counties_reproj,
    fill = NA,
    color = "black",
    size = 1
  ) +
  
  # Set fill colors for DMA
  scale_fill_manual(
    values = c(
      "Tulsa, OK" = "#4575b4",
      "Oklahoma City, OK" = "#5ab4ac"
    ),
    aesthetics = c("fill", "color"),
    guide = guide_legend(override.aes = list(
      color = c("#5ab4ac", "#4575b4")
    ))
  ) +
  
  new_scale_color() +
  
  # Add cities with color_gradient based on urban/rural classification
  geom_sf(
    data = ok_cities_correct_clean %>%
      filter(COUNT > 0, CLASSIF == "Urban"),
    aes(size = COUNT, color = color_gradient)
  ) +
  
  scale_color_manual(
    values = cities_urban, na.value = "gray", name = "Urban"
  ) +
  
  new_scale_color() +
  
  # Add cities with color g radient based on urban/rural classification
  geom_sf(
    data = ok_cities_correct_clean %>%
      filter(COUNT > 0, CLASSIF == "Rural"),
    aes(size = COUNT, color = color_gradient)
  ) +
  
  scale_color_manual(
    values = cities_rural, na.value = "gray", name = "Rural"
  ) +
  
  scale_size_continuous(
    range = c(1, 10),
    name = "Frequency of Mentions"
  )
'''


  [1]: https://i.sstatic.net/3EVCeDlD.png
  [2]: https://i.sstatic.net/JCcIMo2C.png
Share Improve this question edited Jan 23 at 18:59 Victoria Johnson asked Jan 22 at 22:09 Victoria JohnsonVictoria Johnson 235 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 1

The first issue with your code is that you have to use new_scale_fill instead of new_scale_color to add additional fill scales. Second, you also have to filter your data for urban and rural cities or counties. Third, concerning the issue with the DMA legends. Not sure about that. This looks like an issue with ggnewscale and perhaps is a bug, i.e. both legends get merged into one when not using ggnewscale. To fix that I dropped the color legend and use the override.aes argument of guide_legend to set the colors for the fill legend. Finally, instead of mapping color codes on the aesthetics I bin the COUNT variable using cut and set the colors via scale_fill_manual.

Using a minimal reproducible example based on the default example from ?geom_sf:

library(tidyverse)
library(ggnewscale)

## Make example data
set.seed(123)

nc_counties <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)

dma <- sample(nrow(nc_counties), 2)

ok_counties_reproj <- nc_counties

ok_DMA <- nc_counties[dma, ]
ok_DMA$DMA <- c("Tulsa, OK", "Oklahoma City, OK")

ok_cities_correct_clean <- nc_counties[-dma, ]
ok_cities_correct_clean$CLASSIF <- sample(
  c("Urban", "Rural"),
  nrow(ok_cities_correct_clean),
  replace = TRUE
)
ok_cities_correct_clean$COUNT <- sample(
  seq(0, 40), 
  nrow(ok_cities_correct_clean),
  replace = TRUE
)
###

breaks <- c(-Inf, 10, 20, 30, Inf)
labels <- labels <- c("1 - 10", "11 - 20", "21 - 30", "> 30")

## Bin the COUNT variable
ok_cities_correct_clean$color_gradient <- cut(ok_cities_correct_clean$COUNT, breaks, labels)

## Define color palettes
pal_urban <- c("lightblue", "lightskyblue", "dodgerblue", "darkblue")
names(pal_urban) <- labels

pal_rural <- c("lightpink", "lightcoral", "firebrick", "darkred")
names(pal_rural) <- labels

ggplot() +
  # Add OK counties (light grey fill, black borders)
  geom_sf(
    data = ok_counties_reproj,
    fill = "#f5f5f5",
    color = "black",
    size = 0 # No borders for counties in this layer
  ) +
  # Add DMA boundaries (Tulsa and Oklahoma City)
  geom_sf(
    data = ok_DMA,
    aes(fill = DMA, color = DMA),
    linewidth = 1.5, # Set line width for borders
    alpha = 0.75, # Transparency for DMAs,
    show.legend = c(fill = TRUE, color = FALSE)
  ) +
  # Add Oklahoma counties again with borders, but no fill
  geom_sf(
    data = ok_counties_reproj,
    fill = NA,
    color = "black", # Keep the black border for counties
    size = 1 # Make the county borders visible
  ) +
  # Set fill colors for Tulsa and Oklahoma City
  scale_fill_manual(
    values = c(
      "Tulsa, OK" = "#4575b4",
      "Oklahoma City, OK" = "#5ab4ac"
    ),
    aesthetics = c("fill", "color"),
    guide = guide_legend(override.aes = list(
      color = c("#5ab4ac", "#4575b4")
    ))
  ) +
  new_scale_fill() +
  # Add cities with color_gradient based on urban/rural classification and their counts
  geom_sf(
    data = ok_cities_correct_clean %>%
      filter(COUNT > 0, CLASSIF == "Urban"),
    aes(size = COUNT, fill = color_gradient)
  ) +
  scale_fill_manual(
    values = pal_urban, na.value = "gray", name = "Urban"
  ) +
  new_scale_fill() +
  # Add cities with color_gradient based on urban/rural classification and their counts
  geom_sf(
    data = ok_cities_correct_clean %>%
      filter(COUNT > 0, CLASSIF == "Rural"),
    aes(size = COUNT, fill = color_gradient)
  ) +
  scale_fill_manual(
    values = pal_rural, na.value = "gray", name = "Rural"
  ) +
  scale_size_continuous(
    range = c(1, 10),
    name = "Frequency of Mentions"
  )

转载请注明原文地址:http://anycun.com/QandA/1745226503a90471.html