前序博文Python气象数据可视化学习笔记5——基于cartopy绘制contour并对中国地区进行白化(包含南海) 的阅读和收藏量都很高,感觉大家还是有很多地图白化裁剪的需求。但是在上述博文中,需要自己找mask文件和地图shp数据,对很多人来说也是个麻烦事,近期发现了一个在线的中国地图python扩展包,试了下,非常好用,写博客记录下学习过程,也希望对看到这篇博文的人有所帮助。
官网介绍,cnmaps是一个致力于让中国地图的获取和使用更丝滑的python扩展包,主要功能包括:
(1)自带合规地图边界,数据源来自于高德等测绘机构,让你无需再额外寻找地图边界文件。
(2)支持地图边界之间的加减、交并集等常规操作,让你可以自由地组合想要的地图形状。
(3)具有易于使用的地图裁剪功能,且裁剪效果好,平滑无锯齿。
(4)与cartopy集成,可以自动转换地图边界的投影。
这4点简直是气象er们的福音,这样的扩展包怎么能不支持。而且cnmaps目前包括了省级到县级地图的绘制,简直太给力了。
主要参考网站:
(1)cnmaps使用指南
(2)cnmaps安装
(3)cnmaps github
安装这里提示大家一定要用最新版本,并且cartopy版本要在20以上,老版本有些函数都用不了。
绘制效果图主要使用到4个库:
(1) cnmaps: 主要用到get_adm_maps, draw_maps,clip_contours_by_map, draw_map 四个函数,后面会介绍
(2) cartopy: 用地图的投影转换,经纬度坐标的设置等
(3) matplotlib: 不用介绍了,cartopy都是基于matplotlib的,一定要导入的
(4) xarray: 效果图因为用到了WRF的静态地形数据geo_em_d01.nc的海拔高度做填色,所以需要使用xarray读取数据。
import xarray as xr
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
from cnmaps import get_adm_maps, draw_maps,clip_contours_by_map, draw_map
mpl.rcParams["font.size"] = 13
利用cnmaps绘制地图和白化主要包含三个步骤:
(1)使用get_adm_maps返回地图边界。
(2)ax.contourf绘制填色图
(3)clip_contours_by_map基于填色图和地图边界进行裁剪和白化处理。
(4)draw_map 或者 draw_maps绘制地图边界
在以上四个步骤的基础上,还有一些细节处理。本文为了呈现多种地图和白化效果,定义了map_plot函数,下面介绍该函数的具体思路。
def map_plot(fig,ax,grid_lat,grid_lon,data, is_mask, is_province_boundary,title):
#调用地图,返回多条边界
beijing1 = get_adm_maps(province='北京市')
tianjin1 = get_adm_maps(province='天津市')
hebei1 = get_adm_maps(province='河北省')
#仅返回地图边界对象(MapPolygon), 一条边界
beijing2 = get_adm_maps(province='北京市', only_polygon=True, record='first')
tianjin2 = get_adm_maps(province='天津市', only_polygon=True, record='first')
hebei2 = get_adm_maps(province='河北省', only_polygon=True, record='first')
jingjinji1 = beijing1 + tianjin1 + hebei1
jingjinji2 = beijing2 + tianjin2 + hebei2
#设置显示区域
ax.set_extent([113,120,36.2,42.5], crs=ccrs.PlateCarree())
这里使用了geo_em_d01.nc中HGT的数据,绘图原理不再重复,比较简单
#绘制填色图
cf = ax.contourf(grid_lon, grid_lat,data, cmap=plt.cm.terrain,levels=np.linspace(0, 3000, 11),transform=ccrs.PlateCarree())
填色图白化使用的函数是clip_contours_by_map,输入数据为contour返回对象cf和地图返回对象jingjinji2. 注意白化时无论最后地图边界呈现如何,这里要选择仅包含一条边界的地图对象(Jingjinji2),不然会报错. 这样能够得到白化后的效果,图(b)和图(d)。
#设置是否白化,白化必须基于一条边界(jingjinji2)
if is_mask:
clip_contours_by_map(cf, jingjinji2)
根据官方文档 draw_map 和draw_maps的区别是前者是使用map_polygon地图边界对象,后者使用maps (list or GeoDataFrame) ,对应的分别是jingjinji2和jingjinji1,代表仅有一条边界还是全部省份边界,图(a)和图 ©的差别。如果只有一个省份,那用draw_map就可以了。
#绘制地图
if is_province_boundary:
draw_maps(jingjinji1,linewidth=0.8, color='k') #绘制全部省份地图
else:
draw_map(jingjinji2,linewidth=0.8, color='k') #仅绘制所有省份的一条边界
#设置标题
ax.set_title(title)
#添加经纬度格网 兰伯特
gl=ax.gridlines(draw_labels=True,linestyle=":",linewidth=0.3 ,x_inline=False, y_inline=False,color='k')
gl.top_labels=False #关闭上部经纬标签
gl.right_labels=False#关闭右边经纬标签
gl.rotate_labels=None#关闭兰伯特经纬标签旋转
gl.xformatter = LONGITUDE_FORMATTER #使横坐标转化为经纬度格式
gl.yformatter = LATITUDE_FORMATTER
#添加coloarbar
fig.colorbar(cf,ax=ax, shrink=0.9, extendfrac='auto',extendrect=True,location='right',fraction=0.05, pad=0.05)
使用了经度grid_lon, 纬度grid_lat和data(grid_lat, grid_lon)二维数据,并使用了lambert投影(中心经纬度37N,105E,切割纬度纬30N和60N),这里可根据自己的数据选择不同的投影。
例子里通过前面定义好的map_plot函数绘制了四幅图,具体见效果图。
# read data
grid_path = "../air_quality/geo_em.d01.nc"
grid_data = xr.open_dataset(grid_path)
grid_lat = grid_data["XLAT_M"].values[0]
grid_lon = grid_data["XLONG_M"].values[0]
data = grid_data['HGT_M'].values[0]
# %%
# make plot
proj_lambert = ccrs.LambertConformal(central_latitude=37, central_longitude=105,standard_parallels=(30,60))
#(a) 仅包含一条边界
fig1, ax1 = plt.subplots(1,1,figsize=(6,6), subplot_kw=dict(projection=proj_lambert))
map_plot(fig1, ax1, grid_lat,grid_lon,data,False,False,'(a) Without province boundary')
#(b)京津冀白化
fig2, ax2 = plt.subplots(1,1,figsize=(6,6), subplot_kw=dict(projection=proj_lambert))
map_plot(fig2, ax2, grid_lat,grid_lon,data,True,False,'(b) Clip contours')
#(c)包含全部省份边界
fig3, ax3 = plt.subplots(1,1,figsize=(6,6), subplot_kw=dict(projection=proj_lambert))
map_plot(fig3, ax3, grid_lat,grid_lon,data,False,True,'(c) With province boundary')
#(d)包含全部省份边界
fig4, ax4 = plt.subplots(1,1,figsize=(6,6), subplot_kw=dict(projection=proj_lambert))
map_plot(fig4, ax4, grid_lat,grid_lon,data,True,True,'(d) Clip contours with province boundary')
plt.show()
大功告成!
import xarray as xr
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
from cnmaps import get_adm_maps, draw_maps,clip_contours_by_map, draw_map
mpl.rcParams["font.size"] = 13
# %%
def map_plot(fig,ax,grid_lat,grid_lon,data, is_mask, is_province_boundary,title):
#调用地图,返回多条边界
beijing1 = get_adm_maps(province='北京市')
tianjin1 = get_adm_maps(province='天津市')
hebei1 = get_adm_maps(province='河北省')
#仅返回地图边界对象(MapPolygon), 一条边界
beijing2 = get_adm_maps(province='北京市', only_polygon=True, record='first')
tianjin2 = get_adm_maps(province='天津市', only_polygon=True, record='first')
hebei2 = get_adm_maps(province='河北省', only_polygon=True, record='first')
jingjinji1 = beijing1 + tianjin1 + hebei1
jingjinji2 = beijing2 + tianjin2 + hebei2
#设置显示区域
ax.set_extent([113,120,36.2,42.5], crs=ccrs.PlateCarree())
#绘制填色图
cf = ax.contourf(grid_lon, grid_lat,data, cmap=plt.cm.terrain,levels=np.linspace(0, 3000, 11),transform=ccrs.PlateCarree())
#设置是否白化,白化必须基于一条边界(jingjinji2)
if is_mask:
clip_contours_by_map(cf, jingjinji2)
#绘制地图
if is_province_boundary:
draw_maps(jingjinji1,linewidth=0.8, color='k') #绘制全部省份地图
else:
draw_map(jingjinji2,linewidth=0.8, color='k') #仅绘制所有省份的一条边界
#设置标题
ax.set_title(title)
#添加经纬度格网 兰伯特
gl=ax.gridlines(draw_labels=True,linestyle=":",linewidth=0.3 ,x_inline=False, y_inline=False,color='k')
gl.top_labels=False #关闭上部经纬标签
gl.right_labels=False#关闭右边经纬标签
gl.rotate_labels=None#关闭兰伯特经纬标签旋转
gl.xformatter = LONGITUDE_FORMATTER #使横坐标转化为经纬度格式
gl.yformatter = LATITUDE_FORMATTER
#添加coloarbar
fig.colorbar(cf,ax=ax, shrink=0.9, extendfrac='auto',extendrect=True,location='right',fraction=0.05, pad=0.05)
# %%
# read data
grid_path = "../air_quality/geo_em.d01.nc"
grid_data = xr.open_dataset(grid_path)
grid_lat = grid_data["XLAT_M"].values[0]
grid_lon = grid_data["XLONG_M"].values[0]
data = grid_data['HGT_M'].values[0]
# %%
# make plot
proj_lambert = ccrs.LambertConformal(central_latitude=37, central_longitude=105,standard_parallels=(30,60))
#(a) 仅包含一条边界
fig1, ax1 = plt.subplots(1,1,figsize=(6,6), subplot_kw=dict(projection=proj_lambert))
map_plot(fig1, ax1, grid_lat,grid_lon,data,False,False,'(a) Without province boundary')
#(b)京津冀白化
fig2, ax2 = plt.subplots(1,1,figsize=(6,6), subplot_kw=dict(projection=proj_lambert))
map_plot(fig2, ax2, grid_lat,grid_lon,data,True,False,'(b) Clip contours')
#(c)包含全部省份边界
fig3, ax3 = plt.subplots(1,1,figsize=(6,6), subplot_kw=dict(projection=proj_lambert))
map_plot(fig3, ax3, grid_lat,grid_lon,data,False,True,'(c) With province boundary')
#(d)包含全部省份边界
fig4, ax4 = plt.subplots(1,1,figsize=(6,6), subplot_kw=dict(projection=proj_lambert))
map_plot(fig4, ax4, grid_lat,grid_lon,data,True,True,'(d) Clip contours with province boundary')
plt.show()
# %%