Search code examples
rggplot2geom-sf

ggplot scale_fill_stepsn color palette not respecting discrete color palette input


I'm plotting a heatmap for countries of the world based on number of publications for a research topic. The map is almost working, but the color bar won't behave... I want 0 values to be grey and subsequent bins to follow a green gradient. The palette I'm using has worked in other ggplot applications, but I can't get it to work with scale_fill_stepsn or any of the associated functions. Why won't this plot one grey bin and the green gradient for the remaining bins?


Data used:

# Load the necessary libraries
library(ggplot2)
library(sf)
library(dplyr)
library(rnaturalearth)
library(rnaturalearthdata)

# Load world map data (countries and continents)
world <- ne_countries(scale = "medium", returnclass = "sf")

Plus an added column "Publications" with the number of publications for each country.


Code:

# Define the color palette for the bins (light grey to dark green)
# Create color palette
green_palette <- colorRampPalette(c("#ADBC9F", "#12372A"))(6)
custom_palette <- c("#B4B4B8", green_palette)
first_color <- "#B4B4B8"  # Gray color for 0 values

# Create a world map with countries color-coded by number of publications using binned colors
map_countries <- ggplot(world_data) +
  geom_sf(aes(fill = Publications), color = "white") +  # Color by publications
  scale_fill_stepsn(
    breaks = c(0, 1, 5, 10, 25, 50, 75, Inf),  # Define bin breaks
    colors = c(first_color, green_palette),  # First color is gray, rest are green gradient
    na.value = first_color  # Handle NA values with the same gray color
  ) +
  
  # Adjust the legend appearance
  guides(
    fill = guide_coloursteps(
      even.steps = TRUE,
      show.limits = TRUE,
      barwidth = unit(15, 'cm'),  # Adjust the width of the colorbar
      title = "Publications"  # Optional: Add title to colorbar
    )
  ) +
  
  theme_minimal() +
  
  theme(
    legend.position = "bottom",
    legend.key.height = unit(1, "cm"),
    legend.title = element_text(face = "bold"),
    legend.spacing = unit(0.25, "cm"),
    legend.justification = "center",
    plot.title = element_text(hjust = 0.5)
  ) +
  
  # Customize the legend and colorbar appearance
  #theme_minimal() +
  labs(
    title = "World Map: Publications by Country (Binned Colorbar)",
    subtitle = "Total number of publications per country"
  ) +
  
  # Change projection
  coord_sf(crs = "+proj=robin")  # Use the Robinson projection

# Print country map
print(map_countries)

The plot being producted:

The map produced


The color bar should look like this: Expected color bar

Thank you.


Solution

  • If you want a binned scale where

    colours should not be evenly positioned along the gradient

    then you have to pass vector to the values= argument where

    the vector gives the position (between 0 and 1) for each colour in the colours vector.

    However, for your desired result you might simply bin the variable mapped on fill manually and use scale_fill_manual:

    library(ggplot2)
    library(rnaturalearth)
    library(rnaturalearthdata)
    #> 
    #> Attaching package: 'rnaturalearthdata'
    #> The following object is masked from 'package:rnaturalearth':
    #> 
    #>     countries110
    
    world <- ne_countries(scale = "medium", returnclass = "sf")
    
    green_palette <- colorRampPalette(c("#ADBC9F", "#12372A"))(6)
    custom_palette <- c("#B4B4B8", green_palette)
    first_color <- "#B4B4B8" # Gray color for 0 values
    
    set.seed(123)
    
    world_data <- world
    world_data$Publications <- sample(200, nrow(world), replace = TRUE)
    breaks <- c(0, 1, 5, 10, 25, 50, 75, Inf)
    
    world_data$pub_cut <- cut(
      world_data$Publications,
      breaks = breaks,
      right = TRUE,
      include.lowest = TRUE
    )
    
    ggplot(world_data) +
      geom_sf(
        aes(fill = pub_cut),
        color = "white",
        show.legend = TRUE
      ) +
      scale_fill_manual(
        values = c(first_color, green_palette),
        labels = c(
          0, "1 - 5", "6 - 10", "11 - 25", "26 - 50", "51 - 75",
          "76+"
        ),
        drop = FALSE
      ) +
      guides(
        fill = guide_legend(
          title = "Publications",
          theme = theme(
            legend.text.position = "bottom",
            legend.key.width = unit(1.5, "cm"),
            legend.key.height = unit(1, "cm"),
            legend.key.spacing.x = unit(0, "pt")
          ),
          override.aes = list(linewidth = 0, color = NA),
          nrow = 1
        )
      ) +
      theme_minimal() +
      theme(
        legend.position = "bottom",
        legend.title = element_text(face = "bold"),
        legend.justification = "center",
        plot.title = element_text(hjust = 0.5)
      ) +
      labs(
        title = "World Map: Publications by Country (Binned Colorbar)",
        subtitle = "Total number of publications per country"
      ) +
      coord_sf(crs = "+proj=robin") # Use the Robinson projection