{"spec_id":"flowmap-origin-destination","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nflowmap-origin-destination: Origin-Destination Flow Map\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 84/100 | Updated: 2026-05-20\n\"\"\"\n\nimport math\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Basemap colors (theme-adaptive)\nif THEME == \"light\":\n    LAND_COLOR = \"rgb(235, 232, 220)\"\n    OCEAN_COLOR = \"rgb(215, 230, 245)\"\n    COAST_COLOR = \"rgb(180, 175, 165)\"\n    COUNTRY_COLOR = \"rgb(200, 195, 185)\"\nelse:\n    LAND_COLOR = \"rgb(55, 55, 50)\"\n    OCEAN_COLOR = \"rgb(28, 42, 60)\"\n    COAST_COLOR = \"rgb(90, 88, 80)\"\n    COUNTRY_COLOR = \"rgb(75, 73, 65)\"\n\n# Okabe-Ito blue (position 3) for arcs; green (position 1) for city nodes\nARC_RGB = \"0, 114, 178\"\nCITY_COLOR = \"#009E73\"\n\n# Data: Major international trade flows between world ports\nnp.random.seed(42)\n\ncities = {\n    \"Shanghai\": (31.2, 121.5),\n    \"Los Angeles\": (34.1, -118.2),\n    \"Rotterdam\": (51.9, 4.5),\n    \"Singapore\": (1.3, 103.8),\n    \"Dubai\": (25.3, 55.3),\n    \"New York\": (40.7, -74.0),\n    \"Tokyo\": (35.7, 139.7),\n    \"Hamburg\": (53.6, 10.0),\n    \"Busan\": (35.2, 129.1),\n    \"Hong Kong\": (22.3, 114.2),\n    \"Santos\": (-23.9, -46.3),\n    \"Mumbai\": (19.1, 72.9),\n    \"Sydney\": (-33.9, 151.2),\n    \"Cape Town\": (-33.9, 18.4),\n    \"Panama City\": (9.0, -79.5),\n}\n\n# Per-city label positions — East Asian cluster spread aggressively to avoid overlap\nlabel_positions = {\n    \"Shanghai\": \"top left\",\n    \"Tokyo\": \"top right\",\n    \"Busan\": \"top center\",\n    \"Hong Kong\": \"bottom left\",\n    \"Singapore\": \"bottom right\",\n    \"Rotterdam\": \"top left\",\n    \"Hamburg\": \"top right\",\n    \"New York\": \"top left\",\n    \"Los Angeles\": \"bottom left\",\n    \"Dubai\": \"bottom right\",\n    \"Mumbai\": \"bottom left\",\n    \"Santos\": \"bottom left\",\n    \"Cape Town\": \"bottom left\",\n    \"Sydney\": \"bottom right\",\n    \"Panama City\": \"top left\",\n}\n\nflows_data = [\n    (\"Shanghai\", \"Los Angeles\", 85),\n    (\"Shanghai\", \"Rotterdam\", 72),\n    (\"Singapore\", \"Rotterdam\", 68),\n    (\"Shanghai\", \"New York\", 58),\n    (\"Hong Kong\", \"Los Angeles\", 52),\n    (\"Tokyo\", \"Shanghai\", 48),\n    (\"Rotterdam\", \"New York\", 45),\n    (\"Dubai\", \"Rotterdam\", 42),\n    (\"Busan\", \"Los Angeles\", 40),\n    (\"Shanghai\", \"Hamburg\", 38),\n    (\"Mumbai\", \"Singapore\", 35),\n    (\"Shanghai\", \"Sydney\", 32),\n    (\"Hong Kong\", \"Dubai\", 30),\n    (\"Santos\", \"Rotterdam\", 28),\n    (\"Cape Town\", \"Singapore\", 25),\n    (\"Shanghai\", \"Dubai\", 45),\n    (\"Singapore\", \"Hong Kong\", 55),\n    (\"Los Angeles\", \"Panama City\", 22),\n    (\"Rotterdam\", \"Mumbai\", 20),\n    (\"Tokyo\", \"Los Angeles\", 36),\n    (\"Sydney\", \"Singapore\", 18),\n    (\"Hamburg\", \"New York\", 24),\n    (\"Dubai\", \"Mumbai\", 33),\n    (\"Busan\", \"Shanghai\", 29),\n    (\"Hong Kong\", \"Hamburg\", 26),\n]\n\nflow_values = [f[2] for f in flows_data]\nmax_flow = max(flow_values)\nmin_flow = min(flow_values)\nline_widths = [2 + 8 * (v - min_flow) / (max_flow - min_flow) for v in flow_values]\n\n# Plot\nfig = go.Figure()\n\n# Add flow arcs using quadratic Bezier curves\nfor i, (origin, dest, volume) in enumerate(flows_data):\n    o_lat, o_lon = cities[origin]\n    d_lat, d_lon = cities[dest]\n\n    t = np.linspace(0, 1, 50)\n    mid_lat = (o_lat + d_lat) / 2\n    mid_lon = (o_lon + d_lon) / 2\n\n    dist = np.sqrt((d_lat - o_lat) ** 2 + (d_lon - o_lon) ** 2)\n    arc_height = dist * 0.2\n    dx = d_lon - o_lon\n    dy = d_lat - o_lat\n    perp_x = -dy / (dist + 0.001) * arc_height * 2\n    perp_y = dx / (dist + 0.001) * arc_height * 2\n    ctrl_lat = mid_lat + perp_y\n    ctrl_lon = mid_lon + perp_x\n\n    arc_lats = (1 - t) ** 2 * o_lat + 2 * (1 - t) * t * ctrl_lat + t**2 * d_lat\n    arc_lons = (1 - t) ** 2 * o_lon + 2 * (1 - t) * t * ctrl_lon + t**2 * d_lon\n\n    intensity = (volume - min_flow) / (max_flow - min_flow)\n    opacity = 0.35 + 0.40 * intensity\n\n    fig.add_trace(\n        go.Scattergeo(\n            lon=arc_lons,\n            lat=arc_lats,\n            mode=\"lines\",\n            line={\"width\": line_widths[i], \"color\": f\"rgba({ARC_RGB}, {opacity:.2f})\"},\n            hoverinfo=\"text\",\n            hovertext=f\"{origin} → {dest}<br>Volume: {volume}M tons\",\n            showlegend=False,\n        )\n    )\n\n    # Directional arrow at ~85% along arc, rotated to match the bezier tangent\n    t_a = 0.85\n    a_lat = (1 - t_a) ** 2 * o_lat + 2 * (1 - t_a) * t_a * ctrl_lat + t_a**2 * d_lat\n    a_lon = (1 - t_a) ** 2 * o_lon + 2 * (1 - t_a) * t_a * ctrl_lon + t_a**2 * d_lon\n    dir_lat = 2 * (1 - t_a) * (ctrl_lat - o_lat) + 2 * t_a * (d_lat - ctrl_lat)\n    dir_lon = 2 * (1 - t_a) * (ctrl_lon - o_lon) + 2 * t_a * (d_lon - ctrl_lon)\n    arrow_angle = 90 - math.degrees(math.atan2(dir_lat, dir_lon))\n    fig.add_trace(\n        go.Scattergeo(\n            lon=[a_lon],\n            lat=[a_lat],\n            mode=\"markers\",\n            marker={\n                \"symbol\": \"arrow\",\n                \"size\": max(6, int(line_widths[i] * 1.5)),\n                \"color\": f\"rgba({ARC_RGB}, {opacity:.2f})\",\n                \"angle\": arrow_angle,\n            },\n            hoverinfo=\"skip\",\n            showlegend=False,\n        )\n    )\n\n# City markers and labels\nall_cities = {o for o, _, _ in flows_data} | {d for _, d, _ in flows_data}\ncity_names = list(all_cities)\ncity_lats = [cities[c][0] for c in city_names]\ncity_lons = [cities[c][1] for c in city_names]\n\ncity_totals = {}\nfor o, d, v in flows_data:\n    city_totals[o] = city_totals.get(o, 0) + v\n    city_totals[d] = city_totals.get(d, 0) + v\n\nmax_total = max(city_totals.values())\nmarker_sizes = [10 + 15 * city_totals[c] / max_total for c in city_names]\ntext_pos = [label_positions.get(c, \"top center\") for c in city_names]\n\nfig.add_trace(\n    go.Scattergeo(\n        lon=city_lons,\n        lat=city_lats,\n        mode=\"markers+text\",\n        marker={\"size\": marker_sizes, \"color\": CITY_COLOR, \"line\": {\"width\": 2, \"color\": PAGE_BG}},\n        text=city_names,\n        textposition=text_pos,\n        textfont={\"size\": 11, \"color\": INK_SOFT},\n        hoverinfo=\"text\",\n        hovertext=[f\"{c}<br>Total: {city_totals[c]}M tons\" for c in city_names],\n        showlegend=False,\n    )\n)\n\n# Style\nfig.update_layout(\n    title={\n        \"text\": \"flowmap-origin-destination · python · plotly · anyplot.ai\",\n        \"font\": {\"size\": 16, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    geo={\n        \"projection_type\": \"natural earth\",\n        \"showland\": True,\n        \"landcolor\": LAND_COLOR,\n        \"showocean\": True,\n        \"oceancolor\": OCEAN_COLOR,\n        \"showcoastlines\": True,\n        \"coastlinecolor\": COAST_COLOR,\n        \"coastlinewidth\": 1,\n        \"showlakes\": True,\n        \"lakecolor\": OCEAN_COLOR,\n        \"showcountries\": True,\n        \"countrycolor\": COUNTRY_COLOR,\n        \"countrywidth\": 0.5,\n        \"bgcolor\": \"rgba(0,0,0,0)\",\n    },\n    paper_bgcolor=PAGE_BG,\n    margin={\"l\": 20, \"r\": 20, \"t\": 80, \"b\": 40},\n    annotations=[\n        {\n            \"text\": \"Line width proportional to trade volume (millions of tons)\",\n            \"x\": 0.5,\n            \"y\": -0.04,\n            \"xref\": \"paper\",\n            \"yref\": \"paper\",\n            \"showarrow\": False,\n            \"font\": {\"size\": 12, \"color\": INK_MUTED},\n        }\n    ],\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}