前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >绘图技巧 | 超详细的Colorbar定制化绘制教程

绘图技巧 | 超详细的Colorbar定制化绘制教程

作者头像
DataCharm
发布2021-04-16 11:56:36
7.5K0
发布2021-04-16 11:56:36
举报

本节提要:关于一些不常见的colorbar的仿制:弯曲与环形的colorbar、两端分离的colorbar、收缩colorbar的主副刻度、双刻度列colorbar、截取与拼接cmap、外部颜色引入cmaps与palettable库包、特别的格式定制、levels等距而colorbar刻度距离不等距、其他类型的伪colorbar、使刻度侧的框线与colorbar柱体分离。



上面提到的这些非常规的colorbar,主要依据的原样图是我平时偶然发现,或与其他大佬交流时,觉得还不错的图片,但是还没有在matplotlib中发现绘制技法,或对新手来说不太容易掌握的colorbar技巧。本期内容比较多,基本上掏空了我在colorbar方面的全部存货。希望各位读者喜欢,多多点赞转发。

一、弯曲与环形的colorbar

这是我很久之前在气象家园上看到一个朋友提的问题了。也不知他解决没有,我又去翻找了下,大概帖子已经沉底,只能在这里写一下了。

colorbar函数好像并没有给出能够改变其样式的参数,所以,想要直接简单的通过修改参数来让他变为弯曲的样式就非常困难了。(也不排除我眼瞎官网手册没看到

)所以我们只能取巧了,假造一个colorbar(以下省称cbar)。这一节推送里的很多cbar其实都是伪cbar,怎么理解呢?

我们通常生成cbar,一般要将等值线图的代号传进去,比如:

代码语言:javascript
复制
ac=ax.contourf(...)
fig.colorbar(ac)

这种直接传入的方法我称之为有源cbar,指的是颜色映射直接指向原图,不存在任何偏差,数值与颜色绝对与原图相匹配。但是显然,我们不能这样掰弯一个有源的cbar,原因见前。所以我们要按照原图的colormap和levels来伪造一个cbar,使其在视觉效果上变成一个弯曲的cbar。

首先前面的程序直到画等值线都是走流程。这里只撷取核心代码段:

代码语言:javascript
复制
from matplotlib import cm
ac=ax.contourf(levels=...,cmap='RdBu_r') #省略部分内容
ac.levels#获得等值线的等级
cmap=cm.get_cmap('RdBu_r',len(cs.levels)-1) #获得等值线填色图的色条对应分级
#划分极坐标系中的x坐标
angle=np.arange(0,0.5*np.pi,0.5*np.pi/(len(cs.levels)-1))
#划分极坐标系中的y坐标,由于我们要使cbar对其,所以高度都取2
radius=np.array([2]*(len(ac.levels)-1))
cmaps=cmap(range(len(cs.levels)-1))
#设置旋转偏移
ax1.set_theta_offset(np.pi/2)
#绘制极坐标中的bar
ax1.bar(angle,radius,width=0.5*np.pi/11,color=cmaps,align='center')
#标注文字
for i,x,y in zip(cs.levels,angle,radius):
    ax1.text(x-0.1,y+0.17,i,fontsize=3)
ax1.text(0.9,2.6,'气温:℃',fontsize=4)

ax1是个极坐标的子图,在最底层,我们叠加的地图是另一个ax,在上层,利用上层的地图遮住下面。然后在视觉上形成弯曲的cbar。在生成angle时我们只用了0.5π,一个圆的周长是2π,所以我们的弯曲cbar只有四分之一。

利用这个方式还可以完成下面这个图的cbar:

不过要修改x轴的划分区域,变为2π,还要限制ylim使其中心被掏空。

另外,还可以用刘大成《matplotlib精进》p28里提到的用楔形绘制圆环的办法完成弯曲cbar的绘制。

二、两端分离的colorbar

这个的仿制的缘起是另一个公号的编辑给我看了一张图,我觉得还比较好看,所以专门取出来看看。这个图的两端的尖角与主体是分离的。我尝试翻了官网文档,好像不能实现。(也有可能我过年猪油吃多了)那就是又在逼我伪造cbar了。

完整代码如下:

代码语言:javascript
复制
import numpy as np
import xarray as xr
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import matplotlib as mpl
from matplotlib import cm,colors
from matplotlib.patches import Rectangle
import cartopy.crs as ccrs
from cartopy.util import add_cyclic_point
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
import cartopy.feature as cf
import cartopy.io.shapereader as shpreader
plt.rcParams['font.sans-serif']=['SimHei']
file=r'E:\aaaa\datanc\fnl_20190620_00_00.grib2'
data=xr.open_dataset(file,engine='cfgrib',backend_kwargs={'filter_by_keys': {'typeOfLevel': 'surface'}})
t=data['t']-278.15
lat=data['latitude']
lon=data['longitude']
proj=ccrs.LambertConformal(central_longitude=105, central_latitude=35)
reader = shpreader.Reader( r'F:\BaiduNetdiskDownload\MeteoInfo\MeteoInfo_Software\MeteoInfo\map\china.shp')
provinces=cf.ShapelyFeature(reader.geometries(),crs=ccrs.PlateCarree(), edgecolor='k', facecolor='none',alpha=1,lw=0.5)
extent=[80,130,10,60]
fig=plt.figure(figsize=(1.5,1.5),dpi=800)
ax=fig.add_axes([0,0.1,1,0.85],projection=proj)
ax.add_feature(cf.COASTLINE.with_scale('50m'),lw=0.25)
ax.add_feature(cf.OCEAN.with_scale('50m'),lw=0.5)
ax.add_feature(cf.LAND.with_scale('50m'),lw=0.5,edgecolor='none')
ax.add_feature(cf.RIVERS.with_scale('50m'),lw=0.25)
ax.add_feature(provinces, linewidth=0.3)
ax.spines['geo'].set_linewidth(0.5)
ax.set_extent(extent)
ax2=fig.add_axes([0.054,0,0.892,0.1])
ax2.tick_params(axis='both',which='major',direction='in',width=0.5,length=0.5,labelsize=3)
for i in['top','bottom','left','right']:
    ax2.spines[i].set_linewidth(0.5)
ac=ax.contourf(lon,lat,t,cmap='Spectral_r',levels=np.arange(-20,40,2),transform=ccrs.PlateCarree())
ax2.set(xlim=(-3,30),ylim=(0,1))
#开始获取颜色条,产生关联
num=ac.levels
colormap=cm.get_cmap('Spectral_r',len(num)-1)#获得等值线填色图的色条对应分级
cmaps=colormap(range(len(num)-1))
camps=cmaps.tolist()
for i,x,y,color in zip(range(len(num)-2),range(len(num)-2),[0.25]*(len(num)-2),cmaps[1:-1]):
    rectangle=Rectangle([x,y],1,0.5,facecolor=color,
                    alpha=1,
                    edgecolor='none')
    ax2.add_patch(rectangle)
left=plt.Polygon(xy=[[-0.75,0.31], [-0.75, 0.69], [-2.5,0.5]], color=cmaps[0])
right=plt.Polygon(xy=[[27.75,0.31],[27.75,0.69],[29.5,0.5]],color=cmaps[-1])
ax2.add_patch(left)
ax2.add_patch(right)
ax2.axis('off')
ax.set_title('分离colorbar',fontsize=7) 

因为想这个办法只花了一个小时,肯定有些能简略的地方或者更好的办法,我这里只是一个造假cbar的思路。

通过中间的一个for循环,我们将每个等级的色条以颜色polygon的方法按顺序从左往右排列,变成一个视觉上的cbar,其实不是我们常规意义上的cbar。然后通过最后left、right两个polygon来添加分离的两头小三角。

在for循环中cmap[ 1:-1]表示颜色列表中的[ 0 ]和[ -1 ]即首尾两个颜色没有参与中间柱形polygon的添加工作。这两个颜色被赋予到left和right两个polygon中了。

三、收缩colorbar的主副刻度

这一节指的是对colorbar的刻度进行修饰,一般来说,自动生成的cbar是黑色边框的,刻度尺朝外的,字体大小也存在一定的问题。这里简单对其做个美化。我们以仿制plotnine库包的cbar为例子。

代码语言:javascript
复制
cax=fig.add_axes([0.95,0.6,0.05,0.4])
cmap=mpl.cm.viridis
bounds=np.arange(0,100,1)
norm=mpl.colors.BoundaryNorm(bounds, cmap.N)
cbar=fig.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap=cmap),cax=cax)
cbar.outline.set_color('none')
cbar.ax.tick_params(which='both',direction='in',length=1,width=0.25,color='white',labelsize=2,left=True,pad=0.5)

cax这一步代表强制指定cbar摆放的位置,在传入参数后,cbar只能摆放在这个子图上,不会调参时东跑西跑了。

cbar.outline.set_color('none')表示将cbar的边框颜色变为无。

ax.tick_parmas这句话,可以参考前面的推送文章中的关于刻度轴的内容:气象绘图加强版(四)—坐标名、刻度、轴。通过which这个参数,我们可以将主副刻度分别设置为相反的朝向等。这里就不在赘述。

四、双刻度列colorbar

这里提到了前面文章推送过的双刻度列的cbar,上次推送很多地方没有讲完,这里再提一次。并且用我们常用的雨量量级来做一个双刻度cbar。首先制作一个单侧刻度列的cbar:

代码语言:javascript
复制
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
##################第一步步,制作雨量色条
fig=plt.figure(figsize=(1.5,0.2),dpi=500)
ax=fig.add_axes([0,0,1,0.5])
colorlevel=[0.1,10.0,25.0,50.0,100.0,250.0,500.0]#雨量等级
colordict=['#A6F28F','#3DBA3D','#61BBFF','#0000FF','#FA00FA','#800040']#颜色列表
cmap=mcolors.ListedColormap(colordict)#产生颜色映射
norm=mcolors.BoundaryNorm(colorlevel,cmap.N)#生成索引
fc=fig.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap=cmap),
             cax=ax, orientation='horizontal',extend='both')
fc.ax.tick_params(which='major',labelsize=3,direction='out',width=0.5,length=1)
fc.outline.set_linewidth(0.3)

生成一个颜色条之后,进入下一步:

代码语言:javascript
复制
ax2=fc.ax#召唤出fc的ax属性并省称为ax2,这时ax2即视为一个子图
ax2.xaxis.set_ticks_position('top')#将数值刻度移动到上边
ax2.tick_params(labelsize=3,top=True,width=0.5,length=1)#修改刻度样式,并使上有刻度
ax3=ax2.secondary_xaxis('bottom')#新建ax3,使ax3与ax2完全相同,但是是处于下部
ax3.tick_params(labelsize=3,width=0.5,length=1)
ax3.spines['bottom'].set_bounds(0.1,500)#截去多余的部分
ax3.set_xticks([40,120,210,290,380,460])
ax3.set_xticklabels(['小雨','中雨','大雨','暴雨','大暴雨','特大暴雨'])#将ax3上的定量数值转化为定性文字
ax3.spines['bottom'].set_linewidth(0.3)#修改底部到框线粗细

这样我们一方面就可以定量的分析各个雨级的mm值,又可以定性的分析各个雨级。还是比较实用的,当然,不一定只到500mm,这只是生成cbar时的限制的,你也可以改到1000。

请注意,在自己定制降水量色条时,间距是不相等的,但是新生成的ax3间距是相等的,所以会出现错位,不过这不重要,只要视觉上过关即可。如果是自动生成的cbar,则不存在这样的问题。

五、截取与拼接colormap

这个不是我想出来的,但也是一个比较少用的功能。即提取一个colormap的某一部分,或者将两个colormap拼到一起。主要基于Creating Colormaps in Matplotlib这个Tutorials里的一些demo。还参考了公众号DQS小王的文章,以及气象家园论坛ID为f_idled的用户总结的经验。

这里只讲讲为什么能截取和拼接。在第一和第二小节中,我们就提取过colormap,并将其划分为levels的对应片段,并对每个polygon填色,实际上colormap就是一系列的色号拼接而成的一个数组。而在我们学习python中,列表list是切片操作的始祖。是数组就可以切片。数组可以切片,那么colormap就可以截取与拼接。colormap的操作转化为列表的操作。

这里用DQS的例子做说明:

代码语言:javascript
复制
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
cmap=mpl.cm.jet_r#获取色条
newcolors=cmap(np.linspace(0,1,256))#分片操作
newcmap=ListedColormap(newcolors[125:])#只取125之后的颜色列表,前面的舍去
fig=plt.figure(figsize=(1.5,0.3),dpi=500)
ax1=fig.add_axes([0,0,1,0.45])
ax2=fig.add_axes([0,0.5,1,0.45])
norm = mpl.colors.Normalize(vmin=0, vmax=10)
fc1=fig.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap='jet_r'),cax=ax1, orientation='horizontal',extend='both')
fc2=fig.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap=newcmap),cax=ax2, orientation='horizontal',extend='both')
for i in [fc1,fc2]:
    i.ax.tick_params(labelsize=3,width=0.5,length=0.5)
    i.outline.set_linewidth(0.5)

可以看出,暖色部分基本被截去了。更多的内容大家可以参考提到的作者的原文章,这里不再展开(其实是我实在打不动字了

)。

六、外部颜色引入cmaps与palettable库包

cmaps与palettable不是matplotlib里的东西,所以要先conda安装。cmaps他可以使你在matplotlib中使用NCL里的颜色条。matplotlib中自带的颜色条实在是比较少的,也难看。NCL中有许多经典的大气科学绘图配色可供使用。大牛写个包实在是嘉惠学林。palettable也差不多,可以让你拥有超出matplotlib的享受。这里简单写个实例:

代码语言:javascript
复制
import cmaps#首先导入这个包
cmap=cmaps.CBR_wet_r
#CBR_wet_r#这个颜色条就不是matplotlib里的,而是通过cmaps转引ncl的
bounds=np.arange(0,100,10)
norm=mpl.colors.BoundaryNorm(bounds, cmap.N)
cbar=fig.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap=cmap),cax=cax)

而且请注意,导入之后,你就可以对ncl的颜色条进行matplotlib里的操作了,比如截取和拼接等。(你敢信第五第六节几百个字打了两天

七、特别的格式定制

在matplotlib中可以使用format参数对cbar的刻度的格式修改,但是有时候会有些不一样的需求。比如下面一个色条:

上面数字格式使用的是Times New Roman,下面中文的字体是SimHei。

代码语言:javascript
复制
fc=fig.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap=cmap),
             cax=ax, orientation='horizontal',extend='both')
labels=fc.ax.get_xticklabels()
[label.set_fontname('Times New Roman')for label in labels]

八、levels等距而colorbar刻度距离不等距

参照FuncNorm: Arbitrary function normalization这个例子,实现levels等距而cbar不等距。但是有个问题是我现在安装的matplotlib中的源码中,该FuncNorm类下面只有注释,没有内容。打开本地的matplotlib文件下的colors.py,我从头读到尾,确实没有这个功能的定义。应该是开发者更新的时候出问题了,所以即便你粘贴demo过去运行也不行。

九、其他类型的伪colorbar

主要是使用legend函数来仿制colorbar。参见Python气象绘图教程(十四)来看如下这张图的制作:

十、使刻度侧的框线与colorbar柱体分离

这是为了仿制前面提到的一张图里的cbar时涉及到的问题。可以看出,一侧的标签框线是和柱体分离的。

但是当我使用cbar.ax.spines['right'].set_position(('outward',10))来修改时,却会只有刻度尺离开柱体的样子。所以我尝试第四节双刻度列的办法提到的新建一根坐标轴进行处理。

代码语言:javascript
复制
cmap = mpl.cm.cool
norm = mpl.colors.Normalize(vmin=5, vmax=10)
fc=fig.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap=cmap),
             cax=ax, orientation='horizontal')
fc.ax.tick_params(which='major',labelsize=3,direction='out',width=0.5,length=1)
fc.outline.set_linewidth(0.3)
#################第二步,生成双刻度列
ax2=fc.ax#召唤出fc的ax属性并省称为ax2,这时ax2即视为一个子图
ax2.xaxis.set_ticks_position('top')#将数值刻度移动到左侧
ax2.tick_params(labelsize=3,top=True,width=0.5,length=1)#修改刻度样式,并使上下都有刻度
ax3=ax2.secondary_xaxis('bottom')#新建ax3,使ax3与ax2完全相同
ax3.tick_params(labelsize=3,width=0.5,length=3)
ax3.spines['bottom'].set_bounds(5,10)#截去多余的部分
ax3.set_xticks([5,10])
ax3.set_xticklabels(['0.1mm','500mm'])#将ax3上的定量数值转化为定性文字
ax3.spines['bottom'].set_linewidth(0.3)#修改底部到框线粗细
labels=fc.ax.get_xticklabels()
ax2.axis('off')
fc.outline.set_color('none')
ax3.spines['bottom'].set_position(('outward',10))

当当当,还是比较像的,后续可以通过设置label和rotation的角度来美化。不过大头已经解决了,其他的细节我就不再赘述。(其实是打不动字了

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-04-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DataCharm 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档