Reputation: 83
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:
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:
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')
)
)
As you can see, the map is not as "great" as it should be. Some issues regarding it's quality are:
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.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: 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
Reputation: 31226
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")
Upvotes: 1