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