justanotherguy
justanotherguy

Reputation: 83

Overlaying traces in Mapbox for Python

I am currently working on a Geography project, for which I have to do some research on migration flows.

I want to represent migration flows using Python and Mapbox, based on a worldwide GeoJSON I previously downloaded. However, I am having some issues regarding the quality of the work, and can't find a proper solution.

I first uploaded the world GeoJSON:

countries = json.load(open("countries_without_antartica.geojson"))

I then extracted the coordinates with a function and grouped them into a list named countries_coords, with countries_lons, countries_lats = zip(*countries_coords).

I then start creating the figure.

Firstly, I initiate it:

fig = go.Figure()

Then, I put the information I extracted before into a ScatterMapbox environment:

fig.add_trace(go.Scattermapbox(
        mode='lines',
        name='Countries',
        fill='toself',
        fillcolor='lightgray',
        line=dict(color='black', width=1),
        lat=countries_lats,
        lon=countries_lons,
        opacity=1,
        showlegend=False,
        hoverinfo='skip',
))

I then specify the Mapbox style with: fig.update_layout(mapbox=dict(style='white-bg'))

That leaves the map with the GeoJSON data alone, as seen in this image: IMAGE 1

The problem, however, starts right here: I then try to add a line to the map, indicating the first migration flow (in this case, from Spain to Australia). I do this with the following code:

fig.add_trace(
        go.Scattermapbox(
            name='flow1',
            lon = [134.340916, -3.704239],
            lat = [-25.039402, 40.415887],
            mode = 'lines',
            line = dict(width = 8,color = 'green')
        )
)

However, the resulted figure is this:IMAGE 2

I have several problems with that, as the migration flow line should be a somewhat curved line and not a straight one.

I realized the solution to THAT (and only THAT) problem was to use go.Scattergeo instead of go.Scattermapbox to represent the line, and so I did:

fig.add_trace(
        go.Scattergeo(
            name='flow1',
            lon = [134.340916, -3.704239],
            lat = [-25.039402, 40.415887],
            mode = 'lines',
            line = dict(width = 8,color = 'green')
        )
)

BUT the line is now "behind" the map itself, so it is not visible (resulting in IMAGE 1 again).

The line with go.Scattergeo IS curved, and it DOES represent what I wanted it to represent, but it is not visible because it is "layered" behind the go.ScatterMapbox figure with the map.

How can I change the order of the traces? Is there a way to prevent the first trace from being "above" the second trace? I tried changing the order of appearance, but nothing worked.

EDIT 1 Following the solutions provided by @NikolasStevenson-Molnar and @BasvanderLinden, I rendered both the world and the migration flow by using go.Scattergeo. Code here:

fig.add_trace(go.Scattergeo(
        mode='lines',
        name='Countries',
        fill='toself',
        fillcolor='lightgray',
        line=dict(color='black', width=1),
        lat=countries_lats,
        lon=countries_lons,
        opacity=1,
        showlegend=False,
        hoverinfo='skip',
))

fig.add_trace(
        go.Scattergeo(
            name='flow1',
            lon = [134.340916, -3.704239],
            lat = [-25.039402, 40.415887],
            mode = 'lines',
            line = dict(width = 8,color = 'green')
        )
)

Here, the result:IMAGE 3

As you can see, the map is not as "great" as it should be. Some issues regarding it's quality are:

  1. The countries are filled with the same colors as the background (i.e. the oceans). I cannot find a way to fill only de countries. While using go.Scattermapbox this was easily done by specifying the desired style (fig.update_layout(mapbox=dict(style='white-bg'))). However, 'go.Scattergeo' does not have that functionality.
  2. The map seems to be outstretched horizontally (see how all the countries are way more wide in IMAGE 3 compared to IMAGE 1). This is particularly visible in the northern hemisphere.

It then occurred to me that issue 1 should be solved by "turning off" the filling atributes, so I coded:

fig.add_trace(go.Scattergeo(
        mode='lines',
        name='Countries',
        line=dict(color='black', width=1),
        lat=countries_lats,
        lon=countries_lons,
        opacity=1,
        showlegend=False,
        hoverinfo='skip',
))

Result is, again, not desirable, because the GeoJSON is plot above the default map that 'go.Scattergeo` provides. For example, when I zoom in into Spain, I get:IMAGE 4 Clearly, the two traces (default and GeoJSON) are operating at the same time, making the final result not-so-tidy. On top of that, the default trace just shows "territory", but not "political division", so -for example- Portugal is not drawn in the default trace but it is in the GeoJSON.

Hope this extra information is valuable to reach a proper solution.

Thank you in advance, for any help, advice, or solution you might give me.

Upvotes: 2

Views: 1920

Answers (1)

Rob Raymond
Rob Raymond

Reputation: 31226

  • you can go back to basics and calculate your own great circle line
  • https://geographiclib.sourceforge.io/html/python/examples.html#basic-geodesic-calculations has an example of how to achieve this
  • bringing it together
    • source countries GeoJSON and create a geopandas dataframe
    • use centroid capability to have data for the centre of a country
    • build utility function to calculate a great circle trace
    • finally show it in action with lines between three pairs of countries
import requests
import geopandas as gpd
import plotly.express as px
from pathlib import Path
from zipfile import ZipFile
import json, io
from geographiclib.geodesic import Geodesic
import math

# source geojson for country boundaries so we can calc centroids
geosrc = pd.json_normalize(
    requests.get(
        "https://pkgstore.datahub.io/core/geo-countries/7/datapackage.json"
    ).json()["resources"]
)
fn = Path(geosrc.loc[geosrc["name"].eq("geo-countries_zip"), "path"].values[0]).name

if not Path.cwd().joinpath(fn).exists():
    r = requests.get(
        geosrc.loc[geosrc["name"].eq("geo-countries_zip"), "path"].values[0],
        stream=True,
    )
    with open(fn, "wb") as fd:
        for chunk in r.iter_content(chunk_size=128):
            fd.write(chunk)

zfile = ZipFile(fn)
with zfile.open(zfile.infolist()[0]) as f:
    geojson = json.load(f)

gdf = gpd.GeoDataFrame.from_features(geojson).set_index("ISO_A3")
# centroids...
gdf["lon"] = gdf.apply(lambda r: r.geometry.centroid.x, axis=1)
gdf["lat"] = gdf.apply(lambda r: r.geometry.centroid.y, axis=1)


def worldcircleline(gdf, country1, country2, fig=None, color="blue"):
    geod = Geodesic.WGS84  # define the WGS84 ellipsoid

    l = geod.InverseLine(
        gdf.loc[country1, "lat"],
        gdf.loc[country1, "lon"],
        gdf.loc[country2, "lat"],
        gdf.loc[country2, "lon"],
        Geodesic.LATITUDE | Geodesic.LONGITUDE,
    )

    da = 1
    n = int(math.ceil(l.a13 / da))
    da = l.a13 / n

    lat = [
        l.ArcPosition(
            da * i, Geodesic.LATITUDE | Geodesic.LONGITUDE | Geodesic.LONG_UNROLL
        )["lat2"]
        for i in range(n + 1)
    ]
    lon = [
        l.ArcPosition(
            da * i, Geodesic.LATITUDE | Geodesic.LONGITUDE | Geodesic.LONG_UNROLL
        )["lon2"]
        for i in range(n + 1)
    ]

    tfig = px.line_mapbox(
        lat=lat,
        lon=lon,
        mapbox_style="carto-positron",
        zoom=1,
    ).update_traces(line={"color":color})

    if fig is None:
        return tfig.update_layout(margin={"l": 0, "r": 0, "b": 0, "t": 0})
    else:
        return fig.add_traces(tfig.data)


fig = worldcircleline(gdf, "ESP", "AUS")
worldcircleline(gdf, "GBR", "SGP", fig=fig, color="red")
worldcircleline(gdf, "IRL", "USA", fig=fig, color="green")

enter image description here

Upvotes: 1

Related Questions