{"spec_id":"flowmap-origin-destination","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nflowmap-origin-destination: Origin-Destination Flow Map\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 84/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\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\"\nLAND_COLOR = \"#C8C2B8\" if THEME == \"light\" else \"#3A3A35\"  # higher contrast than prior values\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.10,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Simplified world coastline polygons for geographic context\nWORLD_COASTLINES = [\n    # North America\n    [\n        (-168, 66),\n        (-141, 70),\n        (-130, 70),\n        (-120, 60),\n        (-125, 50),\n        (-125, 40),\n        (-117, 33),\n        (-105, 25),\n        (-97, 26),\n        (-82, 25),\n        (-81, 30),\n        (-75, 35),\n        (-70, 42),\n        (-67, 45),\n        (-60, 47),\n        (-55, 52),\n        (-60, 60),\n        (-65, 68),\n        (-80, 70),\n        (-100, 73),\n        (-120, 75),\n        (-145, 72),\n        (-168, 66),\n    ],\n    # South America\n    [\n        (-82, 10),\n        (-77, 0),\n        (-80, -5),\n        (-70, -15),\n        (-60, -5),\n        (-50, 0),\n        (-35, -5),\n        (-40, -23),\n        (-55, -35),\n        (-68, -55),\n        (-75, -50),\n        (-75, -40),\n        (-70, -20),\n        (-80, -5),\n        (-82, 10),\n    ],\n    # Europe\n    [\n        (-10, 36),\n        (-10, 45),\n        (-5, 48),\n        (0, 52),\n        (5, 55),\n        (10, 58),\n        (20, 60),\n        (28, 70),\n        (35, 70),\n        (30, 60),\n        (25, 55),\n        (20, 50),\n        (15, 45),\n        (20, 40),\n        (25, 35),\n        (35, 35),\n        (28, 42),\n        (20, 38),\n        (10, 38),\n        (-10, 36),\n    ],\n    # Africa\n    [\n        (-17, 15),\n        (-17, 28),\n        (-5, 36),\n        (10, 38),\n        (20, 33),\n        (35, 30),\n        (45, 12),\n        (52, 12),\n        (45, 0),\n        (42, -10),\n        (35, -25),\n        (25, -34),\n        (18, -35),\n        (12, -20),\n        (15, -5),\n        (5, 5),\n        (-10, 5),\n        (-17, 15),\n    ],\n    # Asia (connected to Europe/Africa via Middle East)\n    [\n        (35, 30),\n        (45, 42),\n        (52, 45),\n        (70, 42),\n        (80, 30),\n        (75, 15),\n        (90, 22),\n        (100, 15),\n        (105, 22),\n        (110, 5),\n        (120, 25),\n        (130, 35),\n        (140, 45),\n        (145, 55),\n        (135, 70),\n        (100, 78),\n        (70, 75),\n        (50, 70),\n        (30, 70),\n        (35, 50),\n        (45, 45),\n        (35, 30),\n    ],\n    # Australia\n    [\n        (113, -22),\n        (120, -18),\n        (135, -12),\n        (145, -15),\n        (152, -25),\n        (150, -38),\n        (140, -38),\n        (130, -33),\n        (115, -35),\n        (113, -22),\n    ],\n]\n\n# Data: Migration flows between major world cities\nnp.random.seed(42)\n\ncities = {\n    \"New York\": (40.71, -74.01),\n    \"London\": (51.51, -0.13),\n    \"Tokyo\": (35.68, 139.69),\n    \"Sydney\": (-33.87, 151.21),\n    \"Dubai\": (25.20, 55.27),\n    \"Singapore\": (1.35, 103.82),\n    \"Paris\": (48.86, 2.35),\n    \"Los Angeles\": (34.05, -118.24),\n    \"Hong Kong\": (22.32, 114.17),\n    \"Frankfurt\": (50.11, 8.68),\n    \"Mumbai\": (19.08, 72.88),\n    \"Sao Paulo\": (-23.55, -46.63),\n}\n\n# Label offsets — LA moved right + below to clear y-axis tick '40' at lat=40\nlabel_offsets = {\n    \"New York\": (5, 8),\n    \"London\": (-60, 10),\n    \"Tokyo\": (-42, 8),\n    \"Sydney\": (8, 5),\n    \"Dubai\": (8, 5),\n    \"Singapore\": (8, -15),\n    \"Paris\": (-45, -18),\n    \"Los Angeles\": (12, -10),\n    \"Hong Kong\": (8, -15),\n    \"Frankfurt\": (8, 8),\n    \"Mumbai\": (8, -15),\n    \"Sao Paulo\": (8, 5),\n}\n\nflows = [\n    (\"New York\", \"London\", 850),\n    (\"New York\", \"Los Angeles\", 720),\n    (\"London\", \"Paris\", 580),\n    (\"London\", \"Dubai\", 450),\n    (\"Tokyo\", \"Hong Kong\", 620),\n    (\"Tokyo\", \"Singapore\", 480),\n    (\"Sydney\", \"Singapore\", 390),\n    (\"Dubai\", \"Mumbai\", 510),\n    (\"Los Angeles\", \"Tokyo\", 340),\n    (\"Paris\", \"Frankfurt\", 420),\n    (\"Hong Kong\", \"Singapore\", 550),\n    (\"New York\", \"Sao Paulo\", 280),\n    (\"London\", \"Frankfurt\", 380),\n    (\"Singapore\", \"Sydney\", 310),\n    (\"Mumbai\", \"Dubai\", 460),\n    (\"Frankfurt\", \"Dubai\", 290),\n    (\"Paris\", \"New York\", 410),\n    (\"Tokyo\", \"Los Angeles\", 370),\n]\n\ndf_flows = pd.DataFrame(flows, columns=[\"origin\", \"destination\", \"flow\"])\ndf_flows[\"origin_lat\"] = df_flows[\"origin\"].map(lambda x: cities[x][0])\ndf_flows[\"origin_lon\"] = df_flows[\"origin\"].map(lambda x: cities[x][1])\ndf_flows[\"dest_lat\"] = df_flows[\"destination\"].map(lambda x: cities[x][0])\ndf_flows[\"dest_lon\"] = df_flows[\"destination\"].map(lambda x: cities[x][1])\n\nflow_min, flow_max = df_flows[\"flow\"].min(), df_flows[\"flow\"].max()\n# Linewidth proportional to flow magnitude: 1.0 (min) to 5.0 (max)\ndf_flows[\"lw\"] = 1.0 + 4.0 * (df_flows[\"flow\"] - flow_min) / (flow_max - flow_min)\n\n# Arc colors from cividis: CVD-safe, dark navy at low end avoids viridis yellow-on-light issue\nn_colors = 256\narc_palette = sns.color_palette(\"cividis\", n_colors=n_colors)\ndf_flows[\"color_idx\"] = ((df_flows[\"flow\"] - flow_min) / (flow_max - flow_min) * (n_colors - 1)).astype(int)\n\n# Per-city total incoming flow for seaborn size= aesthetic\ncity_incoming = df_flows.groupby(\"destination\")[\"flow\"].sum().reset_index()\ncity_incoming.columns = [\"city\", \"incoming_flow\"]\ndf_cities = pd.DataFrame([{\"city\": name, \"lat\": coords[0], \"lon\": coords[1]} for name, coords in cities.items()])\ndf_cities = df_cities.merge(city_incoming, on=\"city\", how=\"left\").fillna(0)\n\n# Plot\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\nax.set_xlim(-180, 180)\nax.set_ylim(-60, 80)\n\n# Draw simplified world coastlines — linewidth=0.8 for visible land/sea boundary\nfor coastline in WORLD_COASTLINES:\n    lons = [p[0] for p in coastline]\n    lats = [p[1] for p in coastline]\n    ax.fill(lons, lats, color=LAND_COLOR, edgecolor=INK_SOFT, linewidth=0.8, alpha=0.9, zorder=0)\n\n# Draw curved arcs: quadratic Bezier with linewidth proportional to flow\nn_points = 50\nfor _, row in df_flows.iterrows():\n    t = np.linspace(0, 1, n_points)\n    x0, y0 = row[\"origin_lon\"], row[\"origin_lat\"]\n    x2, y2 = row[\"dest_lon\"], row[\"dest_lat\"]\n    mid_x = (x0 + x2) / 2\n    mid_y = (y0 + y2) / 2\n    dx, dy = x2 - x0, y2 - y0\n    length = np.sqrt(dx**2 + dy**2)\n    offset = length * 0.15\n    ctrl_x = mid_x - dy / length * offset\n    ctrl_y = mid_y + dx / length * offset\n    x = (1 - t) ** 2 * x0 + 2 * (1 - t) * t * ctrl_x + t**2 * x2\n    y = (1 - t) ** 2 * y0 + 2 * (1 - t) * t * ctrl_y + t**2 * y2\n    color = arc_palette[int(row[\"color_idx\"])]\n    ax.plot(x, y, color=color, linewidth=row[\"lw\"], alpha=0.65, solid_capstyle=\"round\", zorder=1)\n\n# City nodes: seaborn scatterplot with size= encoding per-city total incoming flow\nsns.scatterplot(\n    data=df_cities,\n    x=\"lon\",\n    y=\"lat\",\n    size=\"incoming_flow\",\n    sizes=(60, 350),\n    color=\"#009E73\",\n    edgecolor=PAGE_BG,\n    linewidth=1.5,\n    ax=ax,\n    zorder=3,\n    legend=\"brief\",\n)\n\n# Style the size legend\nleg = ax.get_legend()\nif leg is not None:\n    leg.set_title(\"Incoming\\nFlow\", prop={\"size\": 6})\n    leg.get_frame().set_facecolor(ELEVATED_BG)\n    leg.get_frame().set_edgecolor(INK_SOFT)\n    plt.setp(leg.get_title(), color=INK)\n    for text in leg.get_texts():\n        text.set_color(INK)\n        text.set_fontsize(5)\n    for handle in leg.legend_handles:\n        try:\n            handle.set_facecolor(\"#009E73\")\n            handle.set_edgecolor(PAGE_BG)\n        except AttributeError:\n            pass\n    leg.set_loc(\"lower left\")\n\n# City labels with custom offsets to prevent overlap\nfor _, row in df_cities.iterrows():\n    offset = label_offsets.get(row[\"city\"], (5, 5))\n    ax.annotate(\n        row[\"city\"],\n        (row[\"lon\"], row[\"lat\"]),\n        xytext=offset,\n        textcoords=\"offset points\",\n        fontsize=6,\n        fontweight=\"bold\",\n        color=INK,\n        zorder=4,\n    )\n\n# Geographic reference lines\nax.axhline(y=0, color=INK_SOFT, linestyle=\"--\", linewidth=0.5, alpha=0.4, zorder=0)\nax.axvline(x=0, color=INK_SOFT, linestyle=\"--\", linewidth=0.5, alpha=0.4, zorder=0)\n\n# Style\nax.set_xlabel(\"Longitude\", fontsize=10, color=INK)\nax.set_ylabel(\"Latitude\", fontsize=10, color=INK)\nax.set_title(\"flowmap-origin-destination · python · seaborn · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT)\nsns.despine(ax=ax, left=False, bottom=False)\n\n# Colorbar for flow magnitude using cividis to match arc colors\nnorm = plt.Normalize(vmin=flow_min, vmax=flow_max)\nsm = plt.cm.ScalarMappable(cmap=\"cividis\", norm=norm)\nsm.set_array([])\ncbar = plt.colorbar(sm, ax=ax, shrink=0.6, aspect=20, pad=0.02)\ncbar.set_label(\"Flow Volume\", fontsize=8, color=INK)\ncbar.ax.tick_params(labelsize=7, colors=INK_SOFT)\ncbar.outline.set_edgecolor(INK_SOFT)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}