{"spec_id":"map-projections","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nmap-projections: World Map with Different Projections\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-05-23\n\"\"\"\n\nimport json\nimport os\nimport urllib.request\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\n\n\n# Theme tokens\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\"\n\n# Geographic semantic colors (theme-adaptive)\nOCEAN = \"#D4E8F7\" if THEME == \"light\" else \"#192837\"\nLAND = \"#C4D5B0\" if THEME == \"light\" else \"#2A3D26\"\nLAND_EDGE = \"#4A6A40\" if THEME == \"light\" else \"#3A5534\"\nGRID = \"#9A9A9A\" if THEME == \"light\" else \"#4A4A4A\"\n\nsns.set_theme(\n    style=\"white\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": OCEAN,\n        \"axes.edgecolor\": INK_SOFT,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n    },\n)\n\n# Load Natural Earth 110m country boundaries (~177 countries)\n_NE_URL = (\n    \"https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_110m_admin_0_countries.geojson\"\n)\nwith urllib.request.urlopen(_NE_URL, timeout=20) as _resp:\n    _geojson = json.loads(_resp.read())\n\n# Extract outer rings for each country polygon\ncountry_rings = []\nfor _feat in _geojson[\"features\"]:\n    _geom = _feat[\"geometry\"]\n    if _geom[\"type\"] == \"Polygon\":\n        country_rings.append(np.array(_geom[\"coordinates\"][0]))\n    elif _geom[\"type\"] == \"MultiPolygon\":\n        for _poly in _geom[\"coordinates\"]:\n            country_rings.append(np.array(_poly[0]))\n\n# Major city reference points for seaborn scatterplot layer\ncities = pd.DataFrame(\n    {\n        \"name\": [\"London\", \"São Paulo\", \"Mumbai\", \"Cairo\", \"Beijing\", \"Sydney\"],\n        \"lon\": [0.1, -46.6, 72.8, 31.2, 116.4, 151.2],\n        \"lat\": [51.5, -23.5, 19.1, 30.1, 39.9, -33.9],\n    }\n)\n\n\ndef project(lons, lats, proj):\n    lons = np.asarray(lons, dtype=float)\n    lats = np.asarray(lats, dtype=float)\n    lr = np.radians(lons)\n    pr = np.radians(lats)\n\n    if proj == \"orthographic\":\n        # Perspective from space, centered 20°N 20°E (Europe/Africa/Asia view)\n        p0, l0 = np.radians(20.0), np.radians(20.0)\n        cos_c = np.sin(p0) * np.sin(pr) + np.cos(p0) * np.cos(pr) * np.cos(lr - l0)\n        x = np.cos(pr) * np.sin(lr - l0)\n        y = np.cos(p0) * np.sin(pr) - np.sin(p0) * np.cos(pr) * np.cos(lr - l0)\n        x = np.where(cos_c >= 0, x, np.nan)\n        y = np.where(cos_c >= 0, y, np.nan)\n\n    elif proj == \"aitoff\":\n        # Compromise elliptical — full globe, balanced distortion\n        alpha = np.arccos(np.clip(np.cos(pr) * np.cos(lr / 2), -1.0, 1.0))\n        sin_a = np.sin(alpha)\n        k = np.where(sin_a < 1e-10, 1.0, alpha / sin_a)\n        x = 2 * k * np.cos(pr) * np.sin(lr / 2)\n        y = k * np.sin(pr)\n\n    elif proj == \"hammer\":\n        # Equal-area elliptical (Hammer–Aitoff)\n        z = np.sqrt(1 + np.cos(pr) * np.cos(lr / 2))\n        x = 2 * np.sqrt(2) * np.cos(pr) * np.sin(lr / 2) / z\n        y = np.sqrt(2) * np.sin(pr) / z\n\n    else:  # lambert_cylindrical — equal-area, strongly squashed poles\n        x = lr\n        y = np.sin(pr)\n\n    return x, y\n\n\n# Projection configurations\nproj_configs = [\n    (\"orthographic\", \"Orthographic\\n(Globe Perspective, 20°N 20°E)\", (-1.15, 1.15), (-1.15, 1.15)),\n    (\"aitoff\", \"Aitoff\\n(Compromise Elliptical)\", (-3.5, 3.5), (-1.9, 1.9)),\n    (\"hammer\", \"Hammer\\n(Equal-Area Elliptical)\", (-3.1, 3.1), (-1.6, 1.6)),\n    (\"lambert_cylindrical\", \"Lambert Cylindrical\\n(Equal-Area, Squashed)\", (-3.5, 3.5), (-1.1, 1.1)),\n]\n\nfig, axes = plt.subplots(2, 2, figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\naxes = axes.flatten()\n\nfor idx, (proj_key, title, xlim, ylim) in enumerate(proj_configs):\n    ax = axes[idx]\n    ax.set_facecolor(OCEAN)\n\n    # Graticule: meridians every 30°\n    for lon in range(-180, 181, 30):\n        lts = np.linspace(-85, 85, 120)\n        xs, ys = project(np.full_like(lts, float(lon)), lts, proj_key)\n        ax.plot(xs, ys, color=GRID, linewidth=0.5, alpha=0.6)\n\n    # Graticule: parallels every 30°\n    for lat in range(-60, 61, 30):\n        lns = np.linspace(-180, 180, 300)\n        xs, ys = project(lns, np.full_like(lns, float(lat)), proj_key)\n        ax.plot(xs, ys, color=GRID, linewidth=0.5, alpha=0.6)\n\n    # Country boundaries (~177 countries) — fills land, edges form country borders\n    for ring in country_rings:\n        lons_c, lats_c = ring[:, 0], ring[:, 1]\n        xs, ys = project(lons_c, lats_c, proj_key)\n        if (~np.isnan(xs)).sum() >= 3:\n            ax.fill(xs, ys, color=LAND, edgecolor=LAND_EDGE, linewidth=0.4, alpha=0.9, zorder=2)\n\n    # Horizon circle for orthographic\n    if proj_key == \"orthographic\":\n        t = np.linspace(0, 2 * np.pi, 360)\n        ax.plot(np.cos(t), np.sin(t), color=INK_SOFT, linewidth=0.8, zorder=5)\n\n    # City reference dots — seaborn scatterplot layer; s=70 for 6 sparse points\n    cx, cy = project(cities[\"lon\"].values, cities[\"lat\"].values, proj_key)\n    city_df = pd.DataFrame({\"x\": cx, \"y\": cy})\n    visible = ~np.isnan(cx)\n    if visible.any():\n        sns.scatterplot(\n            data=city_df[visible],\n            x=\"x\",\n            y=\"y\",\n            color=\"#009E73\",\n            s=70,\n            marker=\"o\",\n            edgecolor=PAGE_BG,\n            linewidth=0.5,\n            legend=False,\n            zorder=6,\n            ax=ax,\n        )\n\n    ax.set_xlim(xlim)\n    ax.set_ylim(ylim)\n    ax.set_aspect(\"equal\")\n    ax.set_xticks([])\n    ax.set_yticks([])\n    ax.set_xlabel(\"\")\n    ax.set_ylabel(\"\")\n    ax.set_title(title, fontsize=8, fontweight=\"bold\", color=INK, pad=4)\n    for spine in ax.spines.values():\n        spine.set_edgecolor(INK_SOFT)\n        spine.set_linewidth(0.8)\n\nfig.suptitle(\"map-projections · python · seaborn · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK, y=0.995)\n\nplt.tight_layout(rect=[0, 0, 1, 0.955])\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\nplt.close()\n"}