Python 在 Mapbox 中叠加轨迹

Overlaying traces in Mapbox for Python

我目前正在做一个地理项目,为此我必须对迁移流进行一些研究。

我想使用 Python 和 Mapbox 表示迁移流,基于我之前下载的全球 GeoJSON。但是,我在工作质量方面遇到了一些问题,找不到合适的解决方案。

我第一次上传世界GeoJSON:

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

然后我用一个函数提取坐标并将它们分组到一个名为 countries_coords 的列表中,其中 countries_lons, countries_lats = zip(*countries_coords).

然后我开始创建图形。

首先,我发起它:

fig = go.Figure()

然后,我将之前提取的信息放到一个ScatterMapbox环境中:

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',
))

然后我指定 Mapbox 样式:fig.update_layout(mapbox=dict(style='white-bg'))

这使得地图仅包含 GeoJSON 数据,如下图所示:

然而,问题就从这里开始:然后我尝试在地图上添加一条线,指示第一个移民流(在本例中,从西班牙到澳大利亚)。我使用以下代码执行此操作:

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')
        )
)

然而,结果图是这样的:

我有几个问题,因为迁移 flow 线应该有点弯曲而不是直线。

我意识到那个(而且只有那个)问题的解决方案是使用 go.Scattergeo 而不是 go.Scattermapbox 来表示直线,所以我做到了:

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')
        )
)

但是 该线现在位于地图本身的“后面”,因此不可见(再次导致图像 1)。

带go.Scattergeo 的线是弯曲的,它确实代表了我想要它代表的东西,但它是不可见,因为它在地图的 go.ScatterMapbox 图形后面“分层”。

如何更改轨迹的顺序?有没有办法防止第一条轨迹“高于”第二条轨迹?我试着改变出现的顺序,但没有任何效果。

编辑 1 按照@NikolasStevenson-Molnar 和@BasvanderLinden 提供的解决方案,我使用 go.Scattergeo 渲染了世界和迁移流程。代码在这里:

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')
        )
)

此处,结果:

如您所见,地图并不像它应该的那样“好”。关于它的质量的一些问题是:

  1. 这些国家/地区填充了与背景(即海洋)相同的颜色。我找不到填充 only 个国家/地区的方法。在使用 go.Scattermapbox 时,这很容易通过指定所需的样式 (fig.update_layout(mapbox=dict(style='white-bg'))) 来完成。但是,'go.Scattergeo' 没有该功能。
  2. 地图似乎被水平拉伸了(查看图像 3 中的所有国家与图像 1 相比如何更宽)。这在北半球尤为明显。

然后我想到问题1应该通过“关闭”填充属性来解决,所以我编码:

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',
))

结果同样是不可取的,因为 GeoJSON 是在“go.Scattergeo”提供的默认地图上方 绘制的。例如,当我放大到西班牙时,我得到: 显然,两个跟踪(default 和 GeoJSON)同时运行,使得最终结果不太整洁。最重要的是,默认轨迹只显示“领土”,而不是“政治分区”,因此 - 例如 - 葡萄牙未在默认轨迹中绘制,但它在 GeoJSON 中。

希望这些额外信息对找到合适的解决方案有价值。

提前感谢您给我的任何帮助、建议或解决方案。

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")