World Map with Different Projections — ggplot2

A world map demonstrating different cartographic projections and their distortion characteristics. This visualization showcases how the same geographic data appears under various map projections (Mercator, Robinson, Mollweide, Orthographic, etc.), revealing how each projection preserves or distorts area, shape, distance, or direction. The plot includes graticule (latitude/longitude grid lines) and optionally Tissot indicatrices to illustrate projection distortion patterns.

World Map with Different Projections rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' map-projections: World Map with Different Projections
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 85/100 | Created: 2026-05-23

library(ggplot2)
library(maps)
library(ragg)

# Theme tokens
THEME       <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG     <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
INK         <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT    <- if (THEME == "light") "#4A4A44" else "#B8B7B0"
OCEAN_BG    <- if (THEME == "light") "#C4DCF0" else "#152030"
LAND_BG     <- if (THEME == "light") "#D4C9A8" else "#2A3020"

IMPRINT <- c(
    "#009E73", "#C475FD", "#AE3030", "#4467A3",
    "#99B314", "#954477", "#BD8233"
)

# --- Mollweide projection math -----------------------------------------------
# Solve 2*theta + sin(2*theta) = pi*sin(lat) via Newton-Raphson
solve_theta <- function(phi_vec) {
    sapply(phi_vec, function(p) {
        if (is.na(p)) return(NA_real_)
        if (abs(p) >= pi / 2 - 1e-9) return(sign(p) * pi / 2)
        t <- p
        for (i in seq_len(60)) {
            delta <- (2 * t + sin(2 * t) - pi * sin(p)) / (2 + 2 * cos(2 * t))
            t     <- t - delta
            if (abs(delta) < 1e-11) break
        }
        t
    })
}

mollweide <- function(lon_deg, lat_deg) {
    lon   <- lon_deg * pi / 180
    lat   <- lat_deg * pi / 180
    theta <- solve_theta(lat)
    data.frame(
        x = (2 * sqrt(2) / pi) * lon * cos(theta),
        y = sqrt(2) * sin(theta)
    )
}

# --- World country boundaries (projected) ------------------------------------
world      <- map_data("world")
world_proj <- mollweide(world$long, world$lat)
world$x    <- world_proj$x
world$y    <- world_proj$y

# --- Projection boundary (Mollweide ellipse: a=2√2, b=√2) -------------------
t_ell    <- seq(0, 2 * pi, length.out = 721)
boundary <- data.frame(
    x = 2 * sqrt(2) * cos(t_ell),
    y = sqrt(2) * sin(t_ell)
)

# --- Graticule (lat/lon grid at 30° intervals) --------------------------------
lat_dense <- seq(-90,  90,  length.out = 541)
lon_dense <- seq(-180, 180, length.out = 1081)

meridians <- do.call(rbind, lapply(seq(-180, 180, by = 30), function(lon0) {
    pts <- mollweide(rep(lon0, length(lat_dense)), lat_dense)
    data.frame(x = pts$x, y = pts$y, group = paste0("mer_", lon0))
}))

parallels <- do.call(rbind, lapply(seq(-90, 90, by = 30), function(lat0) {
    pts <- mollweide(lon_dense, rep(lat0, length(lon_dense)))
    data.frame(x = pts$x, y = pts$y, group = paste0("par_", lat0))
}))

graticule <- rbind(meridians, parallels)

# --- Tissot indicatrices (small geodesic circles on the sphere) ---------------
# Radius in degrees; longitude offset corrected for latitude (cos projection)
r_deg    <- 5
t_circle <- seq(0, 2 * pi, length.out = 73)   # 73 pts → closed polygon

lat_centers <- seq(-60, 60, by = 30)   # 5 latitude bands
lon_centers <- seq(-150, 150, by = 60) # 6 longitude positions

tissot <- do.call(rbind, lapply(lat_centers, function(lat0) {
    cos_lat <- max(cos(lat0 * pi / 180), 0.08)
    do.call(rbind, lapply(lon_centers, function(lon0) {
        lon_c <- lon0 + r_deg * cos(t_circle) / cos_lat
        lat_c <- pmax(pmin(lat0 + r_deg * sin(t_circle), 89.9), -89.9)
        pts   <- mollweide(lon_c, lat_c)
        data.frame(
            x     = pts$x,
            y     = pts$y,
            group = paste0("t_", lon0, "_", lat0)
        )
    }))
}))

# --- Equator and prime meridian highlights -----------------------------------
equator <- mollweide(lon_dense, rep(0, length(lon_dense)))
equator$group <- "equator"

prime_merid <- mollweide(rep(0, length(lat_dense)), lat_dense)
prime_merid$group <- "prime"

# --- Plot --------------------------------------------------------------------
p <- ggplot() +
    # Ocean fill
    geom_polygon(
        data = boundary, aes(x = x, y = y),
        fill = OCEAN_BG, color = NA
    ) +
    # Land masses (country polygons)
    geom_polygon(
        data = world, aes(x = x, y = y, group = group),
        fill = LAND_BG, color = NA
    ) +
    # Country borders / coastlines
    geom_path(
        data = world, aes(x = x, y = y, group = group),
        color = INK_SOFT, linewidth = 0.10, alpha = 0.55
    ) +
    # Standard graticule (30° grid)
    geom_path(
        data = graticule, aes(x = x, y = y, group = group),
        color = INK_SOFT, linewidth = 0.18, alpha = 0.45
    ) +
    # Equator and prime meridian — slightly bolder
    geom_path(
        data = equator, aes(x = x, y = y, group = group),
        color = INK_SOFT, linewidth = 0.35, alpha = 0.70
    ) +
    geom_path(
        data = prime_merid, aes(x = x, y = y, group = group),
        color = INK_SOFT, linewidth = 0.35, alpha = 0.70
    ) +
    # Tissot indicatrices — anyplot brand green (position 1)
    geom_polygon(
        data = tissot, aes(x = x, y = y, group = group),
        fill = IMPRINT[1], color = PAGE_BG,
        linewidth = 0.06, alpha = 0.78
    ) +
    # Ellipse outline
    geom_path(
        data = boundary, aes(x = x, y = y),
        color = INK_SOFT, linewidth = 0.40
    ) +
    coord_fixed() +
    labs(
        title    = "map-projections · r · ggplot2 · anyplot.ai",
        subtitle = "Mollweide equal-area projection  ·  Tissot indicatrices (green) confirm equal-area: every circle covers identical surface"
    ) +
    theme_void(base_size = 8) +
    theme(
        plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG),
        plot.title      = element_text(
            color  = INK, size = 12, hjust = 0.5,
            margin = margin(t = 14, b = 6)
        ),
        plot.subtitle   = element_text(
            color  = INK_SOFT, size = 9.5, hjust = 0.5,
            margin = margin(b = 12)
        ),
        plot.margin     = margin(8, 50, 12, 50)
    )

# --- Save --------------------------------------------------------------------
ggsave(
    filename = sprintf("plot-%s.png", THEME),
    plot     = p,
    device   = ragg::agg_png,
    width    = 8,
    height   = 4.5,
    units    = "in",
    dpi      = 400
)

Part of World Map with Different Projections on anyplot.ai.

Other implementations