{"spec_id":"map-connection-lines","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nmap-connection-lines: Connection Lines Map (Origin-Destination)\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-28\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.lines import Line2D\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nBRAND = IMPRINT_PALETTE[0]  # green — connection lines\nPORT_COLOR = IMPRINT_PALETTE[2]  # blue — port markers\n\nLAND_COLOR = \"#D8D3C4\" if THEME == \"light\" else \"#2D2D26\"\nLAND_EDGE = INK_MUTED\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Data: Major global maritime shipping routes (cargo in million TEUs / year)\nnp.random.seed(42)\n\nports = pd.DataFrame(\n    {\n        \"city\": [\"Shanghai\", \"Los Angeles\", \"Rotterdam\", \"Singapore\", \"Busan\", \"Dubai\", \"New York\", \"Mumbai\"],\n        \"lat\": [31.2, 33.7, 51.9, 1.3, 35.1, 25.0, 40.7, 18.9],\n        \"lon\": [121.5, -118.3, 4.5, 103.8, 129.0, 55.1, -74.0, 72.8],\n    }\n)\n\nroutes = pd.DataFrame(\n    {\n        \"origin\": [\n            \"Shanghai\",\n            \"Shanghai\",\n            \"Shanghai\",\n            \"Singapore\",\n            \"Busan\",\n            \"Rotterdam\",\n            \"Singapore\",\n            \"Busan\",\n            \"Dubai\",\n            \"Los Angeles\",\n        ],\n        \"origin_lat\": [31.2, 31.2, 31.2, 1.3, 35.1, 51.9, 1.3, 35.1, 25.0, 33.7],\n        \"origin_lon\": [121.5, 121.5, 121.5, 103.8, 129.0, 4.5, 103.8, 129.0, 55.1, -118.3],\n        \"dest\": [\n            \"Los Angeles\",\n            \"Rotterdam\",\n            \"Singapore\",\n            \"Rotterdam\",\n            \"Los Angeles\",\n            \"New York\",\n            \"Dubai\",\n            \"Rotterdam\",\n            \"Rotterdam\",\n            \"Busan\",\n        ],\n        \"dest_lat\": [33.7, 51.9, 1.3, 51.9, 33.7, 40.7, 25.0, 51.9, 51.9, 35.1],\n        \"dest_lon\": [-118.3, 4.5, 103.8, 4.5, -118.3, -74.0, 55.1, 4.5, 4.5, 129.0],\n        \"cargo_mteu\": [13.2, 10.5, 12.1, 8.4, 5.6, 3.9, 7.2, 3.5, 5.2, 4.1],\n    }\n)\n\ncargo_min = routes[\"cargo_mteu\"].min()\ncargo_max = routes[\"cargo_mteu\"].max()\nroutes[\"line_width\"] = 1.2 + (routes[\"cargo_mteu\"] - cargo_min) / (cargo_max - cargo_min) * 5.0\n\n# Continent polygons (lon, lat) — simplified outlines for geographic context\nland_polygons = [\n    # North America\n    (\n        np.array([-170, -140, -125, -120, -117, -90, -83, -80, -77, -66, -65, -80, -100, -140, -170]),\n        np.array([72, 72, 50, 34, 22, 16, 10, 10, 8, 47, 52, 62, 68, 70, 72]),\n    ),\n    # South America\n    (\n        np.array([-80, -50, -35, -40, -43, -52, -65, -72, -75, -80]),\n        np.array([8, 0, -5, -22, -23, -33, -55, -48, -30, 8]),\n    ),\n    # Europe\n    (\n        np.array([-10, 5, 15, 22, 28, 36, 30, 20, 25, 28, 15, 5, -5, -10, -8, -10]),\n        np.array([36, 43, 38, 44, 41, 40, 46, 55, 64, 72, 70, 60, 56, 50, 44, 36]),\n    ),\n    # Africa\n    (\n        np.array([-18, -18, -12, 10, 25, 35, 43, 42, 36, 28, 18, 0, -18]),\n        np.array([15, 20, 30, 37, 32, 22, 12, 0, -18, -35, -35, 5, 15]),\n    ),\n    # Asia (simplified — main landmass tracing coast then Arctic closure)\n    (\n        np.array(\n            [\n                26,\n                36,\n                46,\n                58,\n                62,\n                68,\n                75,\n                85,\n                98,\n                103,\n                115,\n                122,\n                130,\n                142,\n                160,\n                168,\n                158,\n                148,\n                130,\n                120,\n                108,\n                90,\n                80,\n                60,\n                60,\n                70,\n                100,\n                140,\n                100,\n                70,\n                50,\n                36,\n                26,\n            ]\n        ),\n        np.array(\n            [\n                42,\n                36,\n                22,\n                18,\n                18,\n                22,\n                18,\n                8,\n                10,\n                1,\n                22,\n                38,\n                35,\n                45,\n                68,\n                62,\n                52,\n                50,\n                45,\n                42,\n                52,\n                58,\n                60,\n                65,\n                70,\n                72,\n                76,\n                72,\n                56,\n                50,\n                40,\n                40,\n                42,\n            ]\n        ),\n    ),\n    # Australia\n    (np.array([114, 116, 130, 142, 153, 148, 136, 122, 114]), np.array([-22, -34, -33, -38, -28, -18, -15, -18, -22])),\n]\n\n# Plot\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Draw continents\nfor lons, lats in land_polygons:\n    ax.fill(lons, lats, color=LAND_COLOR, edgecolor=LAND_EDGE, linewidth=0.5, alpha=0.9, zorder=0)\n\n# Draw connection lines (Bezier arcs with antimeridian handling)\nn_points = 80\nt = np.linspace(0, 1, n_points)\n\nfor _, row in routes.iterrows():\n    lon1, lat1 = row[\"origin_lon\"], row[\"origin_lat\"]\n    lon2, lat2 = row[\"dest_lon\"], row[\"dest_lat\"]\n\n    # Unwrap lon2 so the curve takes the shorter path (handles Pacific crossing)\n    diff = lon2 - lon1\n    if diff > 180:\n        lon2 -= 360\n    elif diff < -180:\n        lon2 += 360\n\n    mid_lon = (lon1 + lon2) / 2\n    mid_lat = (lat1 + lat2) / 2\n    dist = np.sqrt((lon2 - lon1) ** 2 + (lat2 - lat1) ** 2)\n    curve_height = dist * 0.13\n\n    lons = (1 - t) ** 2 * lon1 + 2 * (1 - t) * t * mid_lon + t**2 * lon2\n    lats = (1 - t) ** 2 * lat1 + 2 * (1 - t) * t * (mid_lat + curve_height) + t**2 * lat2\n\n    # Wrap to [-180, 180] and split at antimeridian discontinuities\n    lons_w = ((lons + 180) % 360) - 180\n    breaks = np.where(np.abs(np.diff(lons_w)) > 90)[0] + 1\n    starts = np.concatenate([[0], breaks])\n    ends = np.concatenate([breaks, [n_points]])\n\n    for s, e in zip(starts, ends, strict=False):\n        if e > s + 1:\n            ax.plot(\n                lons_w[s:e],\n                lats[s:e],\n                color=BRAND,\n                linewidth=row[\"line_width\"],\n                alpha=0.55,\n                solid_capstyle=\"round\",\n                zorder=2,\n            )\n\n# Port markers via seaborn scatterplot\nsns.scatterplot(\n    data=ports,\n    x=\"lon\",\n    y=\"lat\",\n    s=220,\n    color=PORT_COLOR,\n    edgecolor=PAGE_BG,\n    linewidth=1.5,\n    ax=ax,\n    zorder=4,\n    legend=False,\n)\n\n# City labels with custom offsets to avoid overlap\nlabel_offsets = {\n    \"Shanghai\": (8, -16),  # below dot to separate from Busan label\n    \"Los Angeles\": (-72, -16),\n    \"Rotterdam\": (-68, 8),\n    \"Singapore\": (8, -16),\n    \"Busan\": (8, 8),\n    \"Dubai\": (8, 8),\n    \"New York\": (-65, 8),\n    \"Mumbai\": (-55, -16),\n}\n\nfor _, row in ports.iterrows():\n    dx, dy = label_offsets.get(row[\"city\"], (8, 8))\n    ax.annotate(\n        row[\"city\"],\n        xy=(row[\"lon\"], row[\"lat\"]),\n        xytext=(dx, dy),\n        textcoords=\"offset points\",\n        fontsize=8,\n        fontweight=\"bold\",\n        color=INK,\n        zorder=5,\n    )\n\n# Storytelling annotation: highlight world's busiest shipping lane\n# Arc point at t≈0.2 for Shanghai→LA route (east of Japan, ~146°E 37°N)\nax.annotate(\n    \"World's busiest lane\\nShanghai → Los Angeles: 13.2M TEU\",\n    xy=(146, 37),\n    xytext=(138, 66),\n    textcoords=\"data\",\n    fontsize=7.5,\n    color=INK,\n    ha=\"center\",\n    arrowprops={\"arrowstyle\": \"->\", \"color\": INK_SOFT, \"lw\": 0.8, \"shrinkA\": 3, \"shrinkB\": 4},\n    bbox={\"boxstyle\": \"round,pad=0.35\", \"facecolor\": ELEVATED_BG, \"edgecolor\": INK_SOFT, \"alpha\": 0.88},\n    zorder=6,\n)\n\n# Style\nax.set_xlim(-180, 180)\nax.set_ylim(-60, 80)\nax.set_xlabel(\"Longitude (°)\", fontsize=10, color=INK)\nax.set_ylabel(\"Latitude (°)\", fontsize=10, color=INK)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nfor sp in (\"left\", \"bottom\"):\n    ax.spines[sp].set_color(INK_SOFT)\nax.yaxis.grid(True, alpha=0.12, linewidth=0.5, color=INK)\nax.xaxis.grid(False)\n\ntitle = \"map-connection-lines · python · seaborn · anyplot.ai\"\nax.set_title(title, fontsize=12, fontweight=\"medium\", color=INK)\n\n# Legend showing cargo volume scale\nmin_vol, max_vol = routes[\"cargo_mteu\"].min(), routes[\"cargo_mteu\"].max()\nmid_vol = (min_vol + max_vol) / 2\nlegend_elements = [\n    Line2D([0], [0], color=BRAND, linewidth=1.5, alpha=0.7, label=f\"{min_vol:.1f}M TEU\"),\n    Line2D([0], [0], color=BRAND, linewidth=3.5, alpha=0.7, label=f\"{mid_vol:.1f}M TEU\"),\n    Line2D([0], [0], color=BRAND, linewidth=6.0, alpha=0.7, label=f\"{max_vol:.1f}M TEU\"),\n]\nax.legend(\n    handles=legend_elements,\n    loc=\"lower left\",\n    fontsize=8,\n    title=\"Annual Cargo\",\n    title_fontsize=8,\n    framealpha=0.9,\n    edgecolor=INK_SOFT,\n)\n\n# Save\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}