{"spec_id":"flowmap-origin-destination","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nflowmap-origin-destination: Origin-Destination Flow Map\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 82/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_rect,\n    element_text,\n    geom_curve,\n    geom_point,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    scale_color_viridis,\n    scale_size,\n    theme,\n    theme_void,\n    xlim,\n    ylim,\n)\n\n\nLetsPlot.setup_html()\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\"\nCONTINENT_FILL = \"#E0DDD6\" if THEME == \"light\" else \"#2E2E2B\"\nCONTINENT_BORDER = \"#C8C5BE\" if THEME == \"light\" else \"#4A4A47\"\nHUB_COLOR = \"#4467A3\"\n\n# Data\nnp.random.seed(42)\n\nhubs = {\n    \"Los Angeles\": (-118.24, 34.05),\n    \"New York\": (-74.01, 40.71),\n    \"London\": (-0.13, 51.51),\n    \"Rotterdam\": (4.48, 51.92),\n    \"Dubai\": (55.27, 25.20),\n    \"Singapore\": (103.82, 1.35),\n    \"Shanghai\": (121.47, 31.23),\n    \"Tokyo\": (139.69, 35.69),\n    \"Sydney\": (151.21, -33.87),\n    \"Sao Paulo\": (-46.63, -23.55),\n}\n\nlabel_offsets = {\n    \"Los Angeles\": (-5, 4),\n    \"New York\": (4, 2),\n    \"London\": (-20, 4),\n    \"Rotterdam\": (4, 2),\n    \"Dubai\": (4, 2),\n    \"Singapore\": (4, -5),\n    \"Shanghai\": (4, 2),\n    \"Tokyo\": (4, 2),\n    \"Sydney\": (4, -5),\n    \"Sao Paulo\": (-14, -5),\n}\n\nflows = [\n    (\"Shanghai\", \"Los Angeles\", 85),\n    (\"Shanghai\", \"Rotterdam\", 72),\n    (\"Singapore\", \"Rotterdam\", 65),\n    (\"Tokyo\", \"Los Angeles\", 58),\n    (\"Rotterdam\", \"New York\", 52),\n    (\"Dubai\", \"London\", 48),\n    (\"Shanghai\", \"Singapore\", 45),\n    (\"Los Angeles\", \"Tokyo\", 42),\n    (\"Singapore\", \"Sydney\", 38),\n    (\"Sao Paulo\", \"Rotterdam\", 35),\n    (\"New York\", \"London\", 32),\n    (\"Dubai\", \"Singapore\", 30),\n    (\"Shanghai\", \"Dubai\", 28),\n    (\"Rotterdam\", \"Dubai\", 25),\n    (\"London\", \"New York\", 22),\n    (\"Sydney\", \"Singapore\", 20),\n    (\"Tokyo\", \"Shanghai\", 18),\n    (\"Los Angeles\", \"Shanghai\", 15),\n]\n\nflow_data = []\nfor origin, dest, volume in flows:\n    o_lon, o_lat = hubs[origin]\n    d_lon, d_lat = hubs[dest]\n    flow_data.append(\n        {\n            \"origin_name\": origin,\n            \"dest_name\": dest,\n            \"origin_lon\": o_lon,\n            \"origin_lat\": o_lat,\n            \"dest_lon\": d_lon,\n            \"dest_lat\": d_lat,\n            \"flow\": volume,\n        }\n    )\n\ndf_flows = pd.DataFrame(flow_data)\n\nhub_data = []\nfor name, (lon, lat) in hubs.items():\n    lx, ly = label_offsets.get(name, (4, 2))\n    hub_data.append({\"name\": name, \"lon\": lon, \"lat\": lat, \"label_lon\": lon + lx, \"label_lat\": lat + ly})\ndf_hubs = pd.DataFrame(hub_data)\n\n# Simplified world polygons\nworld_coords = [\n    # North America\n    (-170, 70),\n    (-140, 70),\n    (-120, 60),\n    (-100, 50),\n    (-80, 45),\n    (-70, 45),\n    (-60, 50),\n    (-55, 50),\n    (-55, 45),\n    (-80, 25),\n    (-100, 20),\n    (-120, 30),\n    (-130, 50),\n    (-170, 60),\n    (-170, 70),\n    (None, None),\n    # South America\n    (-80, 10),\n    (-60, 5),\n    (-35, -5),\n    (-40, -20),\n    (-55, -25),\n    (-70, -55),\n    (-75, -45),\n    (-80, -5),\n    (-80, 10),\n    (None, None),\n    # Europe/Africa\n    (-10, 60),\n    (30, 70),\n    (40, 65),\n    (30, 45),\n    (10, 35),\n    (-10, 35),\n    (-20, 15),\n    (50, 10),\n    (45, -35),\n    (20, -35),\n    (10, 5),\n    (-20, 10),\n    (-10, 60),\n    (None, None),\n    # Asia\n    (30, 70),\n    (70, 75),\n    (180, 70),\n    (160, 60),\n    (140, 50),\n    (130, 45),\n    (120, 30),\n    (105, 20),\n    (90, 25),\n    (70, 25),\n    (55, 25),\n    (45, 30),\n    (35, 35),\n    (30, 45),\n    (30, 70),\n    (None, None),\n    # Australia\n    (115, -20),\n    (150, -10),\n    (155, -25),\n    (150, -40),\n    (135, -35),\n    (115, -35),\n    (115, -20),\n]\n\npolygons = []\ncurrent_poly = []\nfor lon, lat in world_coords:\n    if lon is None:\n        if current_poly:\n            polygons.append(current_poly)\n            current_poly = []\n    else:\n        current_poly.append((lon, lat))\nif current_poly:\n    polygons.append(current_poly)\n\nworld_data = []\nfor i, poly in enumerate(polygons):\n    for lon, lat in poly:\n        world_data.append({\"x\": lon, \"y\": lat, \"group\": i})\ndf_world = pd.DataFrame(world_data)\n\n# Plot\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    plot_title=element_text(size=16, hjust=0.5, color=INK),\n    legend_title=element_text(size=12, color=INK),\n    legend_text=element_text(size=10, color=INK_SOFT),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_position=\"right\",\n)\n\nplot = (\n    ggplot()\n    + geom_polygon(\n        data=df_world, mapping=aes(x=\"x\", y=\"y\", group=\"group\"), fill=CONTINENT_FILL, color=CONTINENT_BORDER, size=0.3\n    )\n    + geom_curve(\n        data=df_flows,\n        mapping=aes(x=\"origin_lon\", y=\"origin_lat\", xend=\"dest_lon\", yend=\"dest_lat\", size=\"flow\", color=\"flow\"),\n        curvature=-0.3,\n        alpha=0.6,\n    )\n    + geom_point(data=df_hubs, mapping=aes(x=\"lon\", y=\"lat\"), size=7, color=HUB_COLOR, fill=HUB_COLOR)\n    + geom_text(data=df_hubs, mapping=aes(x=\"label_lon\", y=\"label_lat\", label=\"name\"), size=8, color=INK_SOFT)\n    + scale_size(range=[1, 6], name=\"Trade Volume\")\n    + scale_color_viridis(option=\"viridis\", name=\"Trade Volume\")\n    + labs(title=\"flowmap-origin-destination · python · letsplot · anyplot.ai\")\n    + theme_void()\n    + anyplot_theme\n    + ggsize(800, 450)\n    + xlim(-180, 180)\n    + ylim(-60, 85)\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}