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')
)
)
此处,结果:
如您所见,地图并不像它应该的那样“好”。关于它的质量的一些问题是:
- 这些国家/地区填充了与背景(即海洋)相同的颜色。我找不到填充 only 个国家/地区的方法。在使用
go.Scattermapbox
时,这很容易通过指定所需的样式 (fig.update_layout(mapbox=dict(style='white-bg'))
) 来完成。但是,'go.Scattergeo' 没有该功能。
- 地图似乎被水平拉伸了(查看图像 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 中。
希望这些额外信息对找到合适的解决方案有价值。
提前感谢您给我的任何帮助、建议或解决方案。
- 你可以返璞归真计算出你自己的大圆线
- https://geographiclib.sourceforge.io/html/python/examples.html#basic-geodesic-calculations 有一个如何实现这个的例子
- 把它放在一起
- 源国家 GeoJSON 并创建一个 geopandas 数据框
- 使用 centroid 功能获取国家/地区中心的数据
- 构建效用函数来计算大圆轨迹
- 最终用三对国家/地区之间的线条展示它
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")
我目前正在做一个地理项目,为此我必须对迁移流进行一些研究。
我想使用 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')
)
)
此处,结果:
如您所见,地图并不像它应该的那样“好”。关于它的质量的一些问题是:
- 这些国家/地区填充了与背景(即海洋)相同的颜色。我找不到填充 only 个国家/地区的方法。在使用
go.Scattermapbox
时,这很容易通过指定所需的样式 (fig.update_layout(mapbox=dict(style='white-bg'))
) 来完成。但是,'go.Scattergeo' 没有该功能。 - 地图似乎被水平拉伸了(查看图像 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”提供的默认地图上方 绘制的。例如,当我放大到西班牙时,我得到:
希望这些额外信息对找到合适的解决方案有价值。
提前感谢您给我的任何帮助、建议或解决方案。
- 你可以返璞归真计算出你自己的大圆线
- https://geographiclib.sourceforge.io/html/python/examples.html#basic-geodesic-calculations 有一个如何实现这个的例子
- 把它放在一起
- 源国家 GeoJSON 并创建一个 geopandas 数据框
- 使用 centroid 功能获取国家/地区中心的数据
- 构建效用函数来计算大圆轨迹
- 最终用三对国家/地区之间的线条展示它
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")