Map with connections

I’d like to make a map of Europe with connections for a funding application. It has to look very good and show the institutions involved, the fields of expertise of each institution, and some connections to show who is travelling where. What packages should I look at for this?

Please search this forum. There are many similar questions.

Sure, I was looking for a more updated answer. Is this more of a GMT job or a GeoMakie one?

Nothing changed since the last posts. Pick one that suits your needs.

Thanks. On a related note, it seems that there is an issue with GeoMakie.land():

GeoMakie.land(10)
ERROR: MethodError: no method matching to_multipoly(::Nothing, ::Vector{GeometryBasics.MultiPolygon{2, Float32}})
The function `to_multipoly` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  to_multipoly(::GeoInterface.MultiPolygonTrait, ::Any)
   @ GeoMakie ~/.julia/packages/GeoMakie/QcBwP/src/geojson.jl:103
  to_multipoly(::GeoInterface.PolygonTrait, ::Any)
   @ GeoMakie ~/.julia/packages/GeoMakie/QcBwP/src/geojson.jl:102
  to_multipoly(::GeoInterface.GeometryCollectionTrait, ::Any)
   @ GeoMakie ~/.julia/packages/GeoMakie/QcBwP/src/geojson.jl:105
  ...

Stacktrace:
 [1] _broadcast_getindex_evalf
   @ ./broadcast.jl:699 [inlined]
 [2] _broadcast_getindex
   @ ./broadcast.jl:672 [inlined]
 [3] _getindex
   @ ./broadcast.jl:620 [inlined]
 [4] getindex
   @ ./broadcast.jl:616 [inlined]
 [5] copy
   @ ./broadcast.jl:933 [inlined]
 [6] materialize
   @ ./broadcast.jl:894 [inlined]
 [7] to_multipoly(geom::Vector{Any})
   @ GeoMakie ~/.julia/packages/GeoMakie/QcBwP/src/geojson.jl:101
 [8] land(scale::Int64)
   @ GeoMakie ~/.julia/packages/GeoMakie/QcBwP/src/data.jl:63
 [9] top-level scope
   @ none:1

I guess these two outputs should have the same type but they don’t:

julia> typeof(GeoMakie.geo2basic(NaturalEarth.naturalearth("land", 50)))
Vector{Any} (alias for Array{Any, 1})

julia> typeof(GeoMakie.geo2basic(NaturalEarth.naturalearth("land", 110)))
Vector{Polygon{2, Float32}} (alias for Array{GeometryBasics.Polygon{2, Float32}, 1})

Please start a dedicated thread.

I just made one here recently to visualize interconnections in europe: Tutorial: cross-border flow map of Europe | ENTSOE.jl

colorscheme could use some work…

Thanks so much, this was a fantastic starting point for me! This and GraphMakie.jl (for the connections) was all I needed.

Would be great to see your final result, this is a nice idea.

Sure, it would be something like this (one can change the limits to make it Europe only for instance. The code has lots of extra stuff that comes from @langestefan 's version, it should be cleaned up a bit. And a typo in the title :slight_smile:

using GeoMakie, CairoMakie, GraphMakie,Graphs
using GeoMakie: NaturalEarth

const PROJ_STR = "+proj=merc"
const WGS84    = "+proj=longlat +datum=WGS84"

countries_fc = NaturalEarth.naturalearth("admin_0_countries", 50)

function _country_iso2(feature)
    p = feature.properties
    for key in (:ISO_A2_EH, :ISO_A2)
        v = get(p, key, nothing)
        v === nothing && continue
        v isa AbstractString && v != "-99" && return String(v)
    end
    return ""
end

const CORE_CCR = Set(["AT", "BE", "HR", "CZ", "FR", "DE_LU", "HU", "NL",
                      "PL", "RO", "SK", "SI"])

# Core members we hold no bidding-zone polygon for, coloured via their
# Natural Earth outline instead. Ireland is in Core through the Celtic
# Interconnector (IE↔FR border) — see ENTSO-E's CCR Regions map.
const CORE_BACKDROP_ISO2 = Set(["IS", "MA", "KE", "GE", "TH", "CL"])

const OTHER_COUNTRY = RGBAf(0.93, 0.93, 0.93, 1)     # non-modelled: very light grey
const OTHER_ZONE    = RGBAf(0.86, 0.95, 0.86, 1)     # modelled non-Core: very light green
const CORE_COLOR    = RGBAf(0.78, 0.88, 0.97, 0.7)   # Core CCR: very light blue

function draw_flow_map(;title)
    fig = Figure(size = (800, 600))
    ax  = GeoAxis(fig[1, 1];
        dest = PROJ_STR,
        limits = ((-78, 120), (-60, 71)),
        title = title,
        xgridvisible = false, ygridvisible = false,
        xticksvisible = false, yticksvisible = false,
        xticklabelsvisible = false, yticklabelsvisible = false,
    )

    # Layer 1: every other European country as light context, with any
    # polygon-less Core member (Ireland) highlighted like the Core zones.
    for feat in countries_fc
        poly!(ax, feat.geometry; color = OTHER_COUNTRY,
            strokecolor = :white, strokewidth = 0.4)
    end
    for feat in countries_fc
        core = _country_iso2(feat) in CORE_BACKDROP_ISO2
        if core
        poly!(ax, feat.geometry; color = CORE_COLOR,
            strokecolor = :darkgreen, strokewidth = 0.4)
        end
    end

# Cities: (longitude, latitude)
cities = Dict(
    "Reykjavík"  => (-21.9426, 64.1466),   # Iceland
    "Rabat"      => (-6.8498, 34.0209),    # Morocco
    "Nairobi"    => (36.8219, -1.2921),    # Kenya
    "Tbilisi"    => (44.7930, 41.7151),    # Georgia
    "Bangkok"    => (100.5018, 13.7563),   # Thailand
    "Santiago"   => (-70.6693, -33.4489),  # Chile
    "Wellington" => (174.7772, -41.2866)   # New Zealand (outside your current x-limit)
)
lons = first.(values(cities))
lats = last.(values(cities))
# City points
scatter!(
    ax,
    lons,
    lats;
    color = :red,
    markersize = 12
)

drawcurve!(ax,cities,"Reykjavík","Rabat","")
drawcurve!(ax,cities,"Reykjavík","Nairobi","")
drawcurve!(ax,cities,"Reykjavík","Tbilisi","")
drawcurve!(ax,cities,"Reykjavík","Bangkok","")
drawcurve!(ax,cities,"Reykjavík","Santiago","")

# City labels
for (name, (lon, lat)) in cities
    if name == "London" || name == "Zürich"
        alignpar = (:right, :top)
        offsetpar = (-4,-0)
    elseif name == "Aarhus"
        alignpar = (:right, :bottom)
        offsetpar = (-5,-1)
    elseif name == "Oslo"
        alignpar = (:right, :bottom)
        offsetpar = (-5,5)
    elseif name == "La Plata"
        alignpar = (:left, :top)
        offsetpar = (-5,-5)
        else
        alignpar = (:left, :bottom)
        offsetpar = (2,2)
    end
        text!(
        ax,
        name;
        position = (lon, lat),
        align = alignpar,
        fontsize = 14,
        offset = offsetpar
    )


end

    # Bottom legends, grouped together and centred: area fills on the
    # left, the line-thickness (volume) key on the right.
    area_elems = [
        PolyElement(color = CORE_COLOR, strokecolor = :white, strokewidth = 1),
        PolyElement(color = OTHER_ZONE, strokecolor = :white, strokewidth = 1),
    ]
    rowgap!(fig.layout, 4)
    display(fig)
    save(plotsdir("connections.png"),fig)
    save(plotsdir("connections.pdf"),fig)
end
drawcurve!(ax,cities, from::String,to::String,text::String;valign=:baseline,offset=0.0) = drawcurve!(ax,cities[from],cities[to],text;valign=valign,offset=offset)

function drawcurve!(ax, origin, destination, text;
    curve_distance = 4,
    valign = :baseline,
    offset = 0.0
)

    layout = [Point2(origin...), Point2(destination...)]

    g = Graphs.SimpleDiGraph(2)
    Graphs.add_edge!(g, 1, 2)

    dist = hypot(
        destination[1] - origin[1],
        destination[2] - origin[2]
    )


    graphplot!(
        ax,
        g;
        layout = layout,
        curve_distance = dist*.2,
        elabels = [text],
        curve_distance_usage = true,
        elabels_distance = 15,
        node_color = (:white, 0.7),
        edge_color = (:steelblue, 0.7),
        linewidth = 0.6,
        arrow_size = 20,
    )
end

draw_flow_map(title = "Map with connectionls between partners")

I like it! Now to be completely over the top, you need to of course animate it.