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.

""" anyplot.ai
map-projections: World Map with Different Projections
Library: plotnine 0.15.4 | Python 3.13.13
Quality: 89/100 | Updated: 2026-05-23
"""
import os
import sys
# Remove the script's own directory from sys.path so 'plotnine' resolves to
# the installed package, not this file (which shares the same name).
_here = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if os.path.abspath(p) != _here]
import numpy as np
import pandas as pd
from plotnine import (
aes,
coord_fixed,
element_blank,
element_rect,
element_text,
facet_wrap,
geom_path,
geom_polygon,
geom_text,
ggplot,
labs,
theme,
)
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
LAND_COLOR = "#009E73"
OCEAN_BG = "#D0E8F5" if THEME == "light" else "#0F1F2D"
np.random.seed(42)
# Robinson projection lookup table
_LAT = np.array([0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90])
_XF = np.array(
[
1.0,
0.9986,
0.9954,
0.99,
0.9822,
0.973,
0.96,
0.9427,
0.9216,
0.8962,
0.8679,
0.835,
0.7986,
0.7597,
0.7186,
0.6732,
0.6213,
0.5722,
0.5322,
]
)
_YF = np.array(
[
0.0,
0.062,
0.124,
0.186,
0.248,
0.31,
0.372,
0.434,
0.4958,
0.5571,
0.6176,
0.6769,
0.7346,
0.7903,
0.8435,
0.8936,
0.9394,
0.9761,
1.0,
]
)
def _mollweide(lon, lat):
la = np.radians(lat)
t = la.copy()
for _ in range(15):
den = 2 + 2 * np.cos(2 * t)
den = np.where(np.abs(den) < 1e-10, 1e-10, den)
t -= (2 * t + np.sin(2 * t) - np.pi * np.sin(la)) / den
return 2 * np.sqrt(2) / np.pi * np.radians(lon) * np.cos(t), np.sqrt(2) * np.sin(t)
PROJ_FNS = {
"Equirectangular": lambda lon, lat: (np.radians(lon), np.radians(lat)),
"Mercator": lambda lon, lat: (np.radians(lon), np.log(np.tan(np.pi / 4 + np.radians(np.clip(lat, -85, 85)) / 2))),
"Robinson": lambda lon, lat: (
np.radians(lon) * np.interp(np.abs(lat), _LAT, _XF) * 0.8487,
np.interp(np.abs(lat), _LAT, _YF) * np.sign(lat) * 1.3523,
),
"Mollweide": _mollweide,
}
# === Continent coastlines ===
# Coordinates trace outer boundary; enough points to be recognizable at map scale
# North America: Alaska -> Pacific coast -> Mexico/Central Am -> Gulf -> Florida ->
# East coast -> Maritime Canada -> Northern Canada -> back
_NA_LON = np.array(
[
-168,
-166,
-162,
-160,
-153,
-149,
-141, # Alaska W peninsula -> SE Alaska
-135,
-131,
-127,
-124, # BC coast to US border
-124,
-124,
-122,
-120,
-117, # WA/OR/CA
-116,
-110,
-110,
-107,
-104,
-100,
-95, # Baja/Mexico Pacific/Tehuantepec
-91,
-88,
-86,
-83,
-80,
-77, # Central America (Pacific side)
-77,
-78,
-82,
-85,
-87,
-89,
-92,
-96,
-97, # Panama->Gulf coast W
-97,
-95,
-91,
-89,
-86,
-84,
-81, # TX/LA/MS/AL/FL panhandle
-82,
-80,
-80,
-82,
-81, # Florida
-81,
-77,
-75,
-74,
-72,
-71,
-70,
-67,
-64, # East coast
-60,
-53,
-55,
-59, # Maritime/Newfoundland
-66,
-78,
-87,
-95,
-97, # Hudson Strait / NW Hudson Bay
-102,
-120,
-133,
-141,
-153,
-162,
-168, # N Canada / Arctic coast
]
)
_NA_LAT = np.array(
[
55,
57,
57,
58,
57,
61,
60,
57,
54,
51,
49,
47,
43,
38,
34,
32,
30,
28,
23,
20,
18,
16,
16,
15,
14,
13,
10,
9,
8,
9,
10,
11,
14,
16,
18,
20,
24,
27,
26,
28,
29,
30,
30,
30,
30,
29,
25,
25,
29,
31,
32,
35,
37,
40,
41,
42,
43,
44,
45,
46,
47,
52,
55,
60,
64,
69,
70,
68,
63,
70,
72,
70,
66,
63,
55,
]
)
# South America: Caribbean coast -> NE Brazil -> SE/S Brazil -> Patagonia ->
# Cape Horn -> Chile Pacific -> Colombia Pacific -> back
_SA_LON = np.array(
[
-77,
-74,
-72,
-68,
-64,
-63,
-62,
-61, # Caribbean (Panama->Trinidad area)
-52,
-50,
-45,
-39,
-35, # NE Brazil coast
-37,
-39,
-41,
-44,
-48,
-51, # SE Brazil
-52,
-51,
-52,
-55,
-58,
-62,
-66,
-68, # S Argentina / Patagonia
-68,
-69,
-71,
-69,
-67, # Tierra del Fuego / Cape Horn
-70,
-72,
-75,
-78,
-80, # Chile Pacific coast north
-77, # Colombia Pacific / back to start
]
)
_SA_LAT = np.array(
[
9,
12,
11,
12,
7,
5,
4,
6,
4,
2,
-2,
-9,
-9,
-11,
-14,
-19,
-23,
-28,
-33,
-38,
-42,
-47,
-51,
-52,
-53,
-55,
-55,
-54,
-54,
-53,
-50,
-47,
-33,
-24,
-16,
-7,
0,
9,
]
)
# Europe: Iberia -> Bay of Biscay -> France -> N Sea -> Scandinavia ->
# Baltic -> E Europe -> Balkans -> Adriatic -> Apennines -> Iberia
_EU_LON = np.array(
[
-9,
-9,
-6,
-4,
-2,
0,
2,
3, # Portugal/Spain south -> Gibraltar -> France
5,
7,
8,
10,
14,
15,
18,
20, # Riviera -> Italy W coast -> Adriatic
14,
14,
16,
20,
24,
26,
28,
31, # Italy toe -> Adriatic -> Greece -> Turkey W
29,
28,
24,
22,
20,
17,
14, # Black Sea coast -> Greece back
12,
13,
16,
18,
20,
24,
28,
28, # Adriatic E -> Balkans -> Poland
20,
14,
10,
8,
5,
2,
0, # Poland -> Germany -> North Sea
-1,
-4,
-5,
-3,
-2,
0, # Netherlands/Belgium -> S England (omit island)
2,
8,
14,
20,
25,
28, # Denmark / Sweden / Finland
30,
28,
22,
18,
14,
9, # Baltic north / Norway -> Scandinavia
5,
2,
0,
-3,
-7,
-9, # Norway W coast / Scotland area -> Portugal
-9,
]
)
_EU_LAT = np.array(
[
37,
39,
44,
44,
43,
43,
43,
43,
44,
44,
44,
43,
41,
40,
40,
40,
38,
36,
37,
39,
37,
37,
37,
38,
41,
42,
41,
38,
36,
37,
37,
44,
45,
46,
46,
55,
55,
54,
56,
57,
56,
56,
55,
55,
56,
58,
51,
51,
55,
56,
55,
56,
56,
57,
57,
59,
60,
65,
71,
71,
70,
68,
65,
61,
62,
61,
60,
59,
57,
37,
37,
]
)
# Africa: Morocco -> NW coast -> Gulf of Guinea -> Congo -> S Africa ->
# E Africa -> Horn -> Red Sea / Suez -> N Africa -> Morocco
_AF_LON = np.array(
[
-5,
-6,
-8,
-10,
-13,
-16,
-17, # Morocco -> Dakar (Senegal)
-15,
-15,
-14,
-13,
-11,
-8, # Guinea coast
-5,
-2,
1,
3,
8,
9, # Ghana -> Nigeria -> Cameroon
9,
10,
12,
12,
14,
18,
22,
25, # Cameroon -> Gabon -> Congo -> Angola
24,
23,
22,
19,
17, # Angola south
15,
17,
20,
26,
28,
33,
36, # Namibia -> S Africa -> Cape -> E Cape
37,
36,
34,
40,
40,
41,
44, # Mozambique -> Tanzania -> Kenya -> Somalia
49,
51,
51,
50,
44,
43,
42, # Somalia (Horn)
40,
36,
34,
33,
31,
29,
28, # Red Sea W -> Sudan/Eritrea -> Egypt
25,
20,
15,
10,
5,
0,
-5, # Libya -> Tunisia -> Algeria -> Morocco
-5,
]
)
_AF_LAT = np.array(
[
36,
36,
33,
29,
22,
14,
14,
13,
11,
10,
10,
9,
7,
5,
5,
5,
6,
4,
4,
4,
2,
1,
-2,
-5,
-9,
-16,
-17,
-17,
-18,
-22,
-28,
-29,
-29,
-29,
-29,
-34,
-34,
-32,
-30,
-26,
-22,
-18,
-15,
-11,
-8,
-5,
-2,
0,
2,
5,
8,
10,
12,
15,
18,
20,
22,
30,
31,
32,
33,
33,
32,
32,
33,
35,
36,
36,
]
)
# Asia: Turkey -> Middle East -> Arabian Peninsula -> India -> SE Asia ->
# China coast -> Korea/Japan coast -> Russia Far East -> Siberia/Arctic -> Turkey
_AS_LON = np.array(
[
26,
30,
36,
38,
41,
44,
45, # Turkey -> Syria -> Red Sea -> Yemen W
45,
50,
55,
57,
57,
55,
50, # Yemen -> Oman -> Gulf of Oman
58,
63,
66,
63,
62, # Pakistan coast
67,
72,
73,
75,
78,
80,
80, # India W coast -> Gujarat -> tip
77,
80,
83,
87,
89,
92, # India E coast
92,
97,
100,
104,
103,
100, # Bangladesh -> Myanmar -> Thailand/Malay
103,
107,
110,
113,
118,
121,
122, # Vietnam coast -> China coast
126,
129,
129,
130,
131,
141,
143, # Korea -> Vladivostok -> Sakhalin -> Japan coast
141,
153,
161,
164,
168,
168, # Hokkaido coast -> Kamchatka -> Chukotka
180,
170,
160,
148,
142,
135, # E Siberia / Arctic coast
130,
120,
110,
100,
90,
80,
70, # N Siberia / Russia Arctic coast
60,
50,
40,
36,
28,
26, # Ural -> Caspian -> Caucasus -> Turkey N
26,
]
)
_AS_LAT = np.array(
[
37,
41,
41,
37,
37,
37,
35,
28,
22,
16,
22,
23,
24,
24,
25,
26,
25,
24,
22,
24,
22,
21,
20,
10,
8,
8,
9,
13,
14,
22,
20,
20,
22,
16,
14,
2,
2,
2,
1,
10,
15,
20,
22,
23,
30,
34,
37,
39,
41,
41,
41,
43,
45,
51,
52,
56,
60,
66,
72,
73,
76,
74,
73,
73,
73,
74,
73,
74,
73,
72,
68,
60,
58,
55,
46,
41,
37,
]
)
# Australia
_AU_LON = np.array(
[
114,
115,
117,
119,
122,
124,
128,
130, # SW coast -> S Australia
131,
136,
139,
141,
144,
146, # SA -> Victoria
150,
151,
152,
153,
152,
150, # NSW -> Queensland N coast
148,
146,
145,
141,
136,
130,
123,
116, # Cape York -> N coast -> NW
114,
]
)
_AU_LAT = np.array(
[
-26,
-22,
-20,
-21,
-19,
-17,
-16,
-14,
-12,
-13,
-12,
-14,
-15,
-16,
-18,
-25,
-30,
-33,
-38,
-38,
-38,
-38,
-37,
-35,
-34,
-26,
-22,
-22,
-26,
]
)
# Antarctica (keep as formula-based)
_AN_LON = np.linspace(-180, 180, 40)
_AN_LAT_N = np.array([-62 - 8 * np.abs(np.sin(np.radians(lo))) for lo in _AN_LON])
_AN_LON_F = np.concatenate([_AN_LON, _AN_LON[::-1], [_AN_LON[0]]])
_AN_LAT_F = np.concatenate([_AN_LAT_N, np.full(40, -85), [_AN_LAT_N[0]]])
continents_raw = [
("North America", _NA_LON, _NA_LAT),
("South America", _SA_LON, _SA_LAT),
("Europe", _EU_LON, _EU_LAT),
("Africa", _AF_LON, _AF_LAT),
("Asia", _AS_LON, _AS_LAT),
("Australia", _AU_LON, _AU_LAT),
("Antarctica", _AN_LON_F, _AN_LAT_F),
]
# === Country borders (major borders as path lines) ===
# Each entry: (name, lon_array, lat_array)
# Coordinates approximate key turning points of the border
_borders_raw = [
# USA–Canada: 49th parallel (Pacific -> Great Lakes area -> Maine)
("US-CAN-W", np.array([-124, -120, -115, -110, -105, -100, -95]), np.array([49, 49, 49, 49, 49, 49, 49])),
(
"US-CAN-E",
np.array([-95, -88, -85, -83, -79, -76, -72, -70, -67]),
np.array([49, 47, 46, 46, 44, 45, 45, 47, 47]),
),
# USA–Mexico border (Pacific -> Rio Grande -> Gulf)
("US-MEX", np.array([-117, -114, -111, -108, -104, -100, -97]), np.array([32, 32, 31, 32, 31, 29, 26])),
# Brazil–Bolivia/Peru
("BR-W", np.array([-73, -70, -68, -65, -61, -58]), np.array([-10, -11, -13, -17, -22, -28])),
# Brazil–Argentina/Paraguay
("BR-S", np.array([-58, -57, -55, -54, -53]), np.array([-28, -30, -33, -33, -31])),
# India borders (Pakistan W + Bangladesh E)
("IND-PAK", np.array([67, 68, 70, 72, 74, 75]), np.array([24, 27, 29, 31, 32, 34])),
("IND-BGD", np.array([88, 89, 90, 92, 92]), np.array([26, 25, 24, 23, 22])),
# China–Russia (Amur River)
("CHN-RUS", np.array([110, 115, 120, 126, 130, 134]), np.array([49, 49, 52, 52, 48, 47])),
# China–Mongolia
("CHN-MNG", np.array([85, 90, 95, 100, 105, 112, 118]), np.array([48, 49, 49, 49, 48, 45, 43])),
# China–India (LAC line, simplified)
("CHN-IND", np.array([78, 82, 86, 90, 94, 98]), np.array([34, 34, 33, 28, 28, 28])),
# Russia–Kazakhstan (approximate W segment)
("RUS-KAZ", np.array([52, 58, 62, 66, 72, 78]), np.array([52, 52, 52, 54, 56, 56])),
# European borders (key recognizable ones)
# France–Spain (Pyrenees)
("FR-ES", np.array([-2, 0, 2, 3]), np.array([43, 43, 43, 43])),
# France–Italy / France–Germany (Alps/Rhine)
("FR-IT-DE", np.array([7, 7, 8, 8]), np.array([44, 46, 48, 51])),
# Germany–Poland (Oder-Neisse)
("DE-PL", np.array([14, 14, 18]), np.array([51, 54, 55])),
# Ukraine–Russia (approximate)
("UA-RU", np.array([32, 36, 38, 40]), np.array([52, 50, 48, 47])),
# Finland–Russia
("FI-RU", np.array([28, 29, 30, 30, 28]), np.array([61, 63, 65, 68, 70])),
# Africa: Sudan–South Sudan
("SDN-SSD", np.array([24, 28, 33, 37]), np.array([9, 9, 10, 11])),
# Africa: Congo DRC border (simplified)
("COD-W", np.array([12, 16, 18, 22]), np.array([4, -2, -6, -10])),
# Africa: South Africa borders
("ZAF-N", np.array([17, 20, 25, 28, 31, 34]), np.array([-29, -26, -22, -23, -24, -26])),
# Africa: Nigeria–Chad (Lake Chad area)
("NGA-TCD", np.array([8, 12, 14]), np.array([14, 13, 13])),
# Africa: Ethiopia–Sudan
("ETH-SDN", np.array([33, 35, 38]), np.array([12, 11, 8])),
]
# Build continent DataFrame
cont_records = []
for name, lons, lats in continents_raw:
for i, (lo, la) in enumerate(zip(lons, lats, strict=False)):
cont_records.append({"continent": name, "order": i, "lon": lo, "lat": la})
df_cont = pd.DataFrame(cont_records)
# Build borders DataFrame
bord_records = []
for seg_id, (name, lons, lats) in enumerate(_borders_raw):
for i, (lo, la) in enumerate(zip(lons, lats, strict=False)):
bord_records.append({"border": name, "seg": seg_id, "order": i, "lon": lo, "lat": la})
df_bord = pd.DataFrame(bord_records)
# Build graticule DataFrame
grat_records = []
for lo_val in range(-180, 181, 30):
lats = np.linspace(-85, 85, 200)
for i, la_val in enumerate(lats):
grat_records.append({"group": f"lon_{lo_val}", "lon": lo_val, "lat": la_val, "order": i})
for la_val in range(-60, 61, 30):
lons = np.linspace(-180, 180, 300)
for i, lo_val in enumerate(lons):
grat_records.append({"group": f"lat_{la_val}", "lon": lo_val, "lat": la_val, "order": i})
df_grat = pd.DataFrame(grat_records)
# Apply projections
proj_order = ["Equirectangular", "Mercator", "Robinson", "Mollweide"]
all_cont, all_grat, all_bord = [], [], []
for proj in proj_order:
fn = PROJ_FNS[proj]
pc = df_cont.copy()
pc["x"], pc["y"] = fn(pc["lon"].values, pc["lat"].values)
pc["projection"] = proj
pc["proj_continent"] = proj + "_" + pc["continent"]
all_cont.append(pc)
pg = df_grat.copy()
pg["x"], pg["y"] = fn(pg["lon"].values, pg["lat"].values)
pg["projection"] = proj
pg["proj_group"] = proj + "_" + pg["group"]
all_grat.append(pg)
pb = df_bord.copy()
pb["x"], pb["y"] = fn(pb["lon"].values, pb["lat"].values)
pb["projection"] = proj
pb["proj_border"] = proj + "_" + pb["border"]
all_bord.append(pb)
df_all_cont = pd.concat(all_cont, ignore_index=True)
df_all_grat = pd.concat(all_grat, ignore_index=True)
df_all_bord = pd.concat(all_bord, ignore_index=True)
for df in (df_all_cont, df_all_grat, df_all_bord):
df["projection"] = pd.Categorical(df["projection"], categories=proj_order, ordered=True)
# Per-panel distortion annotations (DE-03)
PROJ_NOTES = {
"Equirectangular": "equidistant cylindrical",
"Mercator": "conformal · area-distorting",
"Robinson": "compromise · balanced",
"Mollweide": "equal-area pseudocylindrical",
}
annot_records = []
for proj in proj_order:
x_a, y_a = PROJ_FNS[proj](np.array([0.0]), np.array([-50.0]))
annot_records.append({"projection": proj, "x": float(x_a[0]), "y": float(y_a[0]), "label": PROJ_NOTES[proj]})
df_annot = pd.DataFrame(annot_records)
df_annot["projection"] = pd.Categorical(df_annot["projection"], categories=proj_order, ordered=True)
# Plot
plot = (
ggplot()
+ geom_path(aes(x="x", y="y", group="proj_group"), data=df_all_grat, color=INK_SOFT, size=0.25, alpha=0.3)
+ geom_polygon(
aes(x="x", y="y", group="proj_continent"),
data=df_all_cont,
fill=LAND_COLOR,
color=INK_SOFT,
size=0.3,
alpha=0.85,
)
+ geom_path(aes(x="x", y="y", group="proj_border"), data=df_all_bord, color=INK, size=0.35, alpha=0.65)
+ geom_text(aes(x="x", y="y", label="label"), data=df_annot, color=INK_SOFT, size=7, ha="center")
+ facet_wrap("~projection", ncol=2, scales="free")
+ coord_fixed(ratio=1.0)
+ labs(
title="map-projections · python · plotnine · anyplot.ai",
subtitle="Cartographic projections compared: Equirectangular, Mercator, Robinson, Mollweide",
)
+ theme(
figure_size=(8, 4.5),
plot_title=element_text(size=12, weight="bold", ha="center", color=INK),
plot_subtitle=element_text(size=10, ha="center", color=INK_SOFT),
strip_text=element_text(size=9, weight="bold", color=INK),
strip_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
axis_text=element_blank(),
axis_title=element_blank(),
axis_ticks=element_blank(),
panel_grid=element_blank(),
panel_background=element_rect(fill=OCEAN_BG),
panel_border=element_rect(color=INK_SOFT, fill=None, size=0.5),
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
legend_position="none",
)
)
plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in")
Part of World Map with Different Projections on anyplot.ai.