{"spec_id":"flowmap-origin-destination","library":"makie","language":"julia","code":"# anyplot.ai\n# flowmap-origin-destination: Origin-Destination Flow Map\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 89/100 | Created: 2026-09-02\n\nusing CairoMakie\nusing Colors\nusing ColorSchemes\n\n# --- Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\") -\nconst THEME     = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG   = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst INK       = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT  = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst LAND_FILL = RGBAf(INK.r, INK.g, INK.b, THEME == \"light\" ? 0.06 : 0.10)\nconst LAND_LINE = RGBAf(INK.r, INK.g, INK.b, 0.20)\nconst BRAND     = colorant\"#009E73\"  # Imprint palette position 1 — location markers\nconst FLOW_CMAP = cgrad([colorant\"#009E73\", colorant\"#4467A3\"])  # imprint_seq — flow magnitude\n\n# --- Data: migration corridors between world cities (thousands / year) -----\ncities = Dict(\n    \"New York\" => (-74.0, 40.7), \"London\" => (-0.1, 51.5), \"Paris\" => (2.35, 48.85),\n    \"Berlin\" => (13.4, 52.5), \"Moscow\" => (37.6, 55.75), \"Beijing\" => (116.4, 39.9),\n    \"Tokyo\" => (139.7, 35.7), \"Delhi\" => (77.2, 28.6), \"Dubai\" => (55.3, 25.2),\n    \"Lagos\" => (3.4, 6.5), \"Cairo\" => (31.2, 30.0), \"Nairobi\" => (36.8, -1.3),\n    \"Johannesburg\" => (28.0, -26.2), \"Sao Paulo\" => (-46.6, -23.5),\n    \"Mexico City\" => (-99.1, 19.4), \"Los Angeles\" => (-118.2, 34.0),\n    \"Toronto\" => (-79.4, 43.7), \"Sydney\" => (151.2, -33.9), \"Singapore\" => (103.8, 1.35),\n    \"Mumbai\" => (72.8, 19.1), \"Istanbul\" => (28.98, 41.0), \"Seoul\" => (126.98, 37.57),\n    \"Bangkok\" => (100.5, 13.75), \"Jakarta\" => (106.8, -6.2),\n)\n\nflows = [\n    (\"Mexico City\", \"Los Angeles\", 180), (\"Mumbai\", \"Dubai\", 150),\n    (\"Beijing\", \"Toronto\", 90), (\"Lagos\", \"London\", 70), (\"Cairo\", \"Dubai\", 60),\n    (\"Jakarta\", \"Singapore\", 130), (\"Istanbul\", \"Berlin\", 85), (\"Delhi\", \"London\", 95),\n    (\"Sao Paulo\", \"New York\", 55), (\"Moscow\", \"Berlin\", 40), (\"Seoul\", \"Los Angeles\", 65),\n    (\"Bangkok\", \"Tokyo\", 30), (\"Nairobi\", \"London\", 45), (\"Johannesburg\", \"London\", 50),\n    (\"Mexico City\", \"New York\", 75), (\"Mumbai\", \"New York\", 60), (\"Lagos\", \"New York\", 35),\n    (\"Cairo\", \"Paris\", 40), (\"Istanbul\", \"Paris\", 55), (\"Delhi\", \"Dubai\", 200),\n    (\"Jakarta\", \"Sydney\", 25), (\"Beijing\", \"Los Angeles\", 100), (\"Seoul\", \"Tokyo\", 20),\n    (\"Toronto\", \"New York\", 15), (\"Nairobi\", \"Dubai\", 30), (\"Johannesburg\", \"Beijing\", 20),\n    (\"Sao Paulo\", \"Toronto\", 25), (\"Bangkok\", \"Singapore\", 40), (\"Mumbai\", \"Singapore\", 45),\n    (\"Lagos\", \"Dubai\", 28),\n]\n\nmin_flow = minimum(f[3] for f in flows)\nmax_flow = maximum(f[3] for f in flows)\n\nnode_totals = Dict{String,Int}()\nfor (o, d, f) in flows\n    node_totals[o] = get(node_totals, o, 0) + f\n    node_totals[d] = get(node_totals, d, 0) + f\nend\n\n# --- Simplified world landmass outlines (stylized silhouette, not survey-grade) -\nnorth_america = Point2f[\n    (-165, 68), (-140, 70), (-100, 75), (-80, 72), (-60, 50), (-52, 47),\n    (-65, 45), (-75, 35), (-80, 25), (-97, 18), (-105, 20), (-115, 30),\n    (-124, 40), (-124, 49), (-130, 55), (-140, 60), (-165, 68),\n]\nsouth_america = Point2f[\n    (-80, 10), (-77, 0), (-70, -18), (-70, -30), (-72, -45), (-68, -55),\n    (-65, -55), (-58, -38), (-48, -25), (-35, -8), (-50, 0), (-60, 5), (-80, 10),\n]\nafrica = Point2f[\n    (-17, 15), (-10, 5), (10, 4), (20, -5), (35, -15), (40, -25),\n    (32, -35), (18, -35), (12, -18), (10, 0), (-5, 5), (-17, 15),\n]\neurope = Point2f[\n    (-10, 36), (-9, 43), (0, 49), (10, 54), (20, 55), (30, 60),\n    (40, 65), (30, 45), (20, 40), (10, 38), (-10, 36),\n]\nasia = Point2f[\n    (30, 45), (40, 65), (60, 70), (90, 75), (140, 73), (160, 65),\n    (150, 45), (130, 35), (120, 25), (100, 10), (80, 10), (68, 25),\n    (55, 25), (45, 30), (35, 35), (30, 45),\n]\naustralia = Point2f[\n    (113, -22), (125, -15), (135, -12), (145, -15), (153, -28), (150, -38),\n    (140, -38), (130, -32), (115, -35), (113, -22),\n]\ncontinents = [north_america, south_america, africa, europe, asia, australia]\n\n# --- Plot -------------------------------------------------------------------\nfig = Figure(resolution = (1600, 900), fontsize = 14, backgroundcolor = PAGE_BG)\n\nax = Axis(\n    fig[1, 1];\n    title = \"flowmap-origin-destination · julia · makie · anyplot.ai\",\n    titlesize = 20,\n    titlecolor = INK,\n    backgroundcolor = PAGE_BG,\n    aspect = DataAspect(),\n)\nhidedecorations!(ax)\nhidespines!(ax)\nxlims!(ax, -172, 172)\nylims!(ax, -60, 80)\n\nfor continent in continents\n    poly!(ax, continent; color = LAND_FILL, strokecolor = LAND_LINE, strokewidth = 1.2)\nend\n\n# The Middle East / South Asia corridor cluster crosses over itself the most\n# densely (Delhi/Mumbai/Dubai/Cairo/Nairobi/Istanbul), so those arcs get a\n# wider curvature spread and lower alpha to stay disentangled.\ncrowded_hubs = Set([\"Delhi\", \"Mumbai\", \"Dubai\", \"Cairo\", \"Nairobi\", \"Istanbul\"])\n\nfor (origin, dest, flow) in flows\n    x0, y0 = cities[origin]\n    x1, y1 = cities[dest]\n    dx, dy = x1 - x0, y1 - y0\n    dist = sqrt(dx^2 + dy^2)\n    is_crowded = origin in crowded_hubs && dest in crowded_hubs\n    curvature = is_crowded ? 0.22 : 0.15\n    cx = (x0 + x1) / 2 - dy / dist * dist * curvature\n    cy = (y0 + y1) / 2 + dx / dist * dist * curvature\n    t = range(0, 1; length = 40)\n    arc_x = @. (1 - t)^2 * x0 + 2 * (1 - t) * t * cx + t^2 * x1\n    arc_y = @. (1 - t)^2 * y0 + 2 * (1 - t) * t * cy + t^2 * y1\n    norm_flow = (flow - min_flow) / (max_flow - min_flow)\n    lines!(\n        ax, arc_x, arc_y;\n        color = (get(FLOW_CMAP, norm_flow), is_crowded ? 0.5 : 0.6),\n        linewidth = 1.5 + 7.5 * norm_flow,\n    )\nend\n\nnode_names = collect(keys(node_totals))\nnode_x = [cities[n][1] for n in node_names]\nnode_y = [cities[n][2] for n in node_names]\npeak_total = maximum(values(node_totals))\nnode_size = [8 + 14 * (node_totals[n] / peak_total) for n in node_names]\nscatter!(\n    ax, node_x, node_y;\n    color = BRAND, markersize = node_size,\n    strokecolor = PAGE_BG, strokewidth = 1.5,\n)\n\n# Label the top hub cities by total flow so major corridors are identifiable\n# without an external reference.\ntop_hubs = first(sort(collect(node_totals); by = last, rev = true), 5)\nfor (name, _) in top_hubs\n    x, y = cities[name]\n    text!(\n        ax, x, y + 4;\n        text = name, color = INK, fontsize = 13,\n        align = (:center, :bottom), font = :bold,\n    )\nend\n\nColorbar(\n    fig[1, 2];\n    colormap = FLOW_CMAP,\n    limits = (min_flow, max_flow),\n    label = \"Flow volume (thousands / year)\",\n    labelcolor = INK,\n    ticklabelcolor = INK_SOFT,\n    ticklabelsize = 12,\n    labelsize = 14,\n)\ncolsize!(fig.layout, 2, Relative(0.05))\n\n# --- Save ---------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}