Connection Lines Map (Origin-Destination) — Seaborn

A geographic map visualization showing connection lines (arcs or great circles) between pairs of locations. This plot excels at revealing spatial patterns in flows, routes, and relationships between places. The curved lines naturally represent shortest paths on Earth's surface while avoiding visual overlap with straight lines. Line properties like thickness or color can encode additional variables such as traffic volume or connection type.

Connection Lines Map (Origin-Destination) rendered with Seaborn

Python source (Seaborn)

""" anyplot.ai
map-connection-lines: Connection Lines Map (Origin-Destination)
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 89/100 | Updated: 2026-05-28
"""

import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib.lines import Line2D


# Theme tokens
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"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]
BRAND = IMPRINT_PALETTE[0]  # green — connection lines
PORT_COLOR = IMPRINT_PALETTE[2]  # blue — port markers

LAND_COLOR = "#D8D3C4" if THEME == "light" else "#2D2D26"
LAND_EDGE = INK_MUTED

sns.set_theme(
    style="ticks",
    rc={
        "figure.facecolor": PAGE_BG,
        "axes.facecolor": PAGE_BG,
        "axes.edgecolor": INK_SOFT,
        "axes.labelcolor": INK,
        "text.color": INK,
        "xtick.color": INK_SOFT,
        "ytick.color": INK_SOFT,
        "grid.color": INK,
        "grid.alpha": 0.15,
        "legend.facecolor": ELEVATED_BG,
        "legend.edgecolor": INK_SOFT,
    },
)

# Data: Major global maritime shipping routes (cargo in million TEUs / year)
np.random.seed(42)

ports = pd.DataFrame(
    {
        "city": ["Shanghai", "Los Angeles", "Rotterdam", "Singapore", "Busan", "Dubai", "New York", "Mumbai"],
        "lat": [31.2, 33.7, 51.9, 1.3, 35.1, 25.0, 40.7, 18.9],
        "lon": [121.5, -118.3, 4.5, 103.8, 129.0, 55.1, -74.0, 72.8],
    }
)

routes = pd.DataFrame(
    {
        "origin": [
            "Shanghai",
            "Shanghai",
            "Shanghai",
            "Singapore",
            "Busan",
            "Rotterdam",
            "Singapore",
            "Busan",
            "Dubai",
            "Los Angeles",
        ],
        "origin_lat": [31.2, 31.2, 31.2, 1.3, 35.1, 51.9, 1.3, 35.1, 25.0, 33.7],
        "origin_lon": [121.5, 121.5, 121.5, 103.8, 129.0, 4.5, 103.8, 129.0, 55.1, -118.3],
        "dest": [
            "Los Angeles",
            "Rotterdam",
            "Singapore",
            "Rotterdam",
            "Los Angeles",
            "New York",
            "Dubai",
            "Rotterdam",
            "Rotterdam",
            "Busan",
        ],
        "dest_lat": [33.7, 51.9, 1.3, 51.9, 33.7, 40.7, 25.0, 51.9, 51.9, 35.1],
        "dest_lon": [-118.3, 4.5, 103.8, 4.5, -118.3, -74.0, 55.1, 4.5, 4.5, 129.0],
        "cargo_mteu": [13.2, 10.5, 12.1, 8.4, 5.6, 3.9, 7.2, 3.5, 5.2, 4.1],
    }
)

cargo_min = routes["cargo_mteu"].min()
cargo_max = routes["cargo_mteu"].max()
routes["line_width"] = 1.2 + (routes["cargo_mteu"] - cargo_min) / (cargo_max - cargo_min) * 5.0

# Continent polygons (lon, lat) — simplified outlines for geographic context
land_polygons = [
    # North America
    (
        np.array([-170, -140, -125, -120, -117, -90, -83, -80, -77, -66, -65, -80, -100, -140, -170]),
        np.array([72, 72, 50, 34, 22, 16, 10, 10, 8, 47, 52, 62, 68, 70, 72]),
    ),
    # South America
    (
        np.array([-80, -50, -35, -40, -43, -52, -65, -72, -75, -80]),
        np.array([8, 0, -5, -22, -23, -33, -55, -48, -30, 8]),
    ),
    # Europe
    (
        np.array([-10, 5, 15, 22, 28, 36, 30, 20, 25, 28, 15, 5, -5, -10, -8, -10]),
        np.array([36, 43, 38, 44, 41, 40, 46, 55, 64, 72, 70, 60, 56, 50, 44, 36]),
    ),
    # Africa
    (
        np.array([-18, -18, -12, 10, 25, 35, 43, 42, 36, 28, 18, 0, -18]),
        np.array([15, 20, 30, 37, 32, 22, 12, 0, -18, -35, -35, 5, 15]),
    ),
    # Asia (simplified — main landmass tracing coast then Arctic closure)
    (
        np.array(
            [
                26,
                36,
                46,
                58,
                62,
                68,
                75,
                85,
                98,
                103,
                115,
                122,
                130,
                142,
                160,
                168,
                158,
                148,
                130,
                120,
                108,
                90,
                80,
                60,
                60,
                70,
                100,
                140,
                100,
                70,
                50,
                36,
                26,
            ]
        ),
        np.array(
            [
                42,
                36,
                22,
                18,
                18,
                22,
                18,
                8,
                10,
                1,
                22,
                38,
                35,
                45,
                68,
                62,
                52,
                50,
                45,
                42,
                52,
                58,
                60,
                65,
                70,
                72,
                76,
                72,
                56,
                50,
                40,
                40,
                42,
            ]
        ),
    ),
    # Australia
    (np.array([114, 116, 130, 142, 153, 148, 136, 122, 114]), np.array([-22, -34, -33, -38, -28, -18, -15, -18, -22])),
]

# Plot
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)

# Draw continents
for lons, lats in land_polygons:
    ax.fill(lons, lats, color=LAND_COLOR, edgecolor=LAND_EDGE, linewidth=0.5, alpha=0.9, zorder=0)

# Draw connection lines (Bezier arcs with antimeridian handling)
n_points = 80
t = np.linspace(0, 1, n_points)

for _, row in routes.iterrows():
    lon1, lat1 = row["origin_lon"], row["origin_lat"]
    lon2, lat2 = row["dest_lon"], row["dest_lat"]

    # Unwrap lon2 so the curve takes the shorter path (handles Pacific crossing)
    diff = lon2 - lon1
    if diff > 180:
        lon2 -= 360
    elif diff < -180:
        lon2 += 360

    mid_lon = (lon1 + lon2) / 2
    mid_lat = (lat1 + lat2) / 2
    dist = np.sqrt((lon2 - lon1) ** 2 + (lat2 - lat1) ** 2)
    curve_height = dist * 0.13

    lons = (1 - t) ** 2 * lon1 + 2 * (1 - t) * t * mid_lon + t**2 * lon2
    lats = (1 - t) ** 2 * lat1 + 2 * (1 - t) * t * (mid_lat + curve_height) + t**2 * lat2

    # Wrap to [-180, 180] and split at antimeridian discontinuities
    lons_w = ((lons + 180) % 360) - 180
    breaks = np.where(np.abs(np.diff(lons_w)) > 90)[0] + 1
    starts = np.concatenate([[0], breaks])
    ends = np.concatenate([breaks, [n_points]])

    for s, e in zip(starts, ends, strict=False):
        if e > s + 1:
            ax.plot(
                lons_w[s:e],
                lats[s:e],
                color=BRAND,
                linewidth=row["line_width"],
                alpha=0.55,
                solid_capstyle="round",
                zorder=2,
            )

# Port markers via seaborn scatterplot
sns.scatterplot(
    data=ports,
    x="lon",
    y="lat",
    s=220,
    color=PORT_COLOR,
    edgecolor=PAGE_BG,
    linewidth=1.5,
    ax=ax,
    zorder=4,
    legend=False,
)

# City labels with custom offsets to avoid overlap
label_offsets = {
    "Shanghai": (8, -16),  # below dot to separate from Busan label
    "Los Angeles": (-72, -16),
    "Rotterdam": (-68, 8),
    "Singapore": (8, -16),
    "Busan": (8, 8),
    "Dubai": (8, 8),
    "New York": (-65, 8),
    "Mumbai": (-55, -16),
}

for _, row in ports.iterrows():
    dx, dy = label_offsets.get(row["city"], (8, 8))
    ax.annotate(
        row["city"],
        xy=(row["lon"], row["lat"]),
        xytext=(dx, dy),
        textcoords="offset points",
        fontsize=8,
        fontweight="bold",
        color=INK,
        zorder=5,
    )

# Storytelling annotation: highlight world's busiest shipping lane
# Arc point at t≈0.2 for Shanghai→LA route (east of Japan, ~146°E 37°N)
ax.annotate(
    "World's busiest lane\nShanghai → Los Angeles: 13.2M TEU",
    xy=(146, 37),
    xytext=(138, 66),
    textcoords="data",
    fontsize=7.5,
    color=INK,
    ha="center",
    arrowprops={"arrowstyle": "->", "color": INK_SOFT, "lw": 0.8, "shrinkA": 3, "shrinkB": 4},
    bbox={"boxstyle": "round,pad=0.35", "facecolor": ELEVATED_BG, "edgecolor": INK_SOFT, "alpha": 0.88},
    zorder=6,
)

# Style
ax.set_xlim(-180, 180)
ax.set_ylim(-60, 80)
ax.set_xlabel("Longitude (°)", fontsize=10, color=INK)
ax.set_ylabel("Latitude (°)", fontsize=10, color=INK)
ax.tick_params(axis="both", labelsize=8, colors=INK_SOFT)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
for sp in ("left", "bottom"):
    ax.spines[sp].set_color(INK_SOFT)
ax.yaxis.grid(True, alpha=0.12, linewidth=0.5, color=INK)
ax.xaxis.grid(False)

title = "map-connection-lines · python · seaborn · anyplot.ai"
ax.set_title(title, fontsize=12, fontweight="medium", color=INK)

# Legend showing cargo volume scale
min_vol, max_vol = routes["cargo_mteu"].min(), routes["cargo_mteu"].max()
mid_vol = (min_vol + max_vol) / 2
legend_elements = [
    Line2D([0], [0], color=BRAND, linewidth=1.5, alpha=0.7, label=f"{min_vol:.1f}M TEU"),
    Line2D([0], [0], color=BRAND, linewidth=3.5, alpha=0.7, label=f"{mid_vol:.1f}M TEU"),
    Line2D([0], [0], color=BRAND, linewidth=6.0, alpha=0.7, label=f"{max_vol:.1f}M TEU"),
]
ax.legend(
    handles=legend_elements,
    loc="lower left",
    fontsize=8,
    title="Annual Cargo",
    title_fontsize=8,
    framealpha=0.9,
    edgecolor=INK_SOFT,
)

# Save
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)

Part of Connection Lines Map (Origin-Destination) on anyplot.ai.

Other implementations