本计划接着 Figure, Axes 对象,讨论 Axis 对象和 Ticks 对象。因为涉及到坐标变换,因此先把 matplotlib 的坐标变换总结一下。
示例代码有很多重复部分,文中仅贴出关键不同的部分,需要完整的代码,请到这里 https://download.csdn.net/download/sinat_32570141/14926207 下载完整的 .ipynb
文件。
与其它绘图包一样,Matplotlib 包含用于确定画布上绘制的所有元素的最终位置的任意几何变换的框架,以便在不同的坐标系之间定位、移动图形元素。
matplotlib.transforms
模块提供了转换框架的功能。
在绘图时,95% 的情况,我们不需要考虑这个问题,因为它发生在底层。
但是,在一些特殊情况下,我们必须指定图形元素的坐标系,才能实现我们的目标,我们希望在图形的中心绘制一个正圆(不是椭圆),如下图:
思维的惯性,一般都会先这样写:
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import matplotlib.transforms as transforms
x = np.linspace(0, 9, 100)
y = np.sin(x)
fig = plt.figure(figsize=(6.4,4.8),constrained_layout=True,facecolor='w')
ax = plt.axes(frameon=True)
ax.set_xticks([])
ax.set_yticks([])
circ = patches.Circle((0.5,0.5),0.4, color='r',fill=False)
ax.add_artist(circ)
plt.title(str(circ))
ax.plot(x, y)
plt.savefig('.\\assets\\trans_12.png',facecolor='w')
plt.show()
得到的远非你想要的,圆变成了椭圆,且不在图形的中心。
这时,我们会对代码做出改进,设置 axes.set_aspect(1)
:
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import matplotlib.transforms as transforms
x = np.linspace(0, 9, 100)
y = np.sin(x)
fig = plt.figure(constrained_layout=True,facecolor='w')
ax = plt.axes(frameon=True)
ax.set_xticks([])
ax.set_yticks([])
ax.set_aspect(1)
circ = patches.Circle((np.mean(x),np.mean(y)),0.4, color='r', fill=False)
ax.add_artist(circ)
plt.title('Not the expected circle yet')
ax.plot(x, y)
plt.savefig('.\\assets\\trans_13.png',facecolor='w')
plt.show()
得到了正圆,且在图形的中间,但整个 figure 被压扁了,这又不是我们想要的。
这时就需要通过坐标变换来实现我们的目标。
matplotlib 坐标系统有:
当你要挑战自定义创建图形的极限时,善用这些坐标系统有助于理解、控制对象。
甚至,你还可以创建自己的转换来实现特殊的效果。
下表总结了 matplotlib 绘图常用的坐标系统。
在Transformation Object 列中,ax是一个Axes实例,fig是一个Figure实例。
Coordinates | Transformation object | Description |
---|---|---|
“data” | ax.transData |
Data 坐标系,由 xlim 和 ylim 控制。 |
“axes” | ax.transAxes |
Axes 坐标系,(0, 0) 是 axes 的左下角, (1, 1) 是 axes 的右上角。 |
“figure” | fig.transFigure |
Figure 坐标系, (0, 0) 是 figure 的左下角, (1, 1) 是 figure 的右上角。这是相对数,包括对象在水平和垂直方向上的尺寸也是相对于 Figure 的宽和高的。 |
“figure-inches” | fig.dpi_scale_trans |
Figure 以 inches 为单位; (0, 0) 是 figure 的左下角, (width, height) 是 figure 的右上角。这是绝对数,包括对象在水平和垂直方向上的尺寸也是绝对值,以 inches 为单位。 |
“display” | None , or IdentityTransform() |
显示窗口的 pixel 坐标系统;(0, 0) 是显示窗口的左下角, (width, height) 是显示窗口的右上角,以 pixels 为单位。这是绝对数,包括对象在水平和垂直方向上的尺寸也是绝对值,但是以 pixels 为单位。对象的物理尺寸(以 inches 为单位)还与 dpi 有关。 |
“xaxis”, “yaxis” | ax.get_xaxis_transform() ,ax.get_yaxis_transform() |
混合坐标系;在一个 axis 上使用 data 坐标,在另一个上使用 axes 坐标系。 |
官方文档对上表有两段说明:
All of the transformation objects
in the table above take inputs in their coordinate
system, and transform the input to the display coordinate
system. That is why the display coordinate
system has None for the Transformation Object
column – it already is in display coordinates. The transformations also know how to invert themselves, to go from display back to the native coordinate system. This is particularly useful when processing events from the user interface, which typically occur in display space, and you want to know where the mouse click or key-press occurred in your data coordinate system.
单从字面看,这段描述并不难,很多人在开始时都是一读而过。网上的文章多数都没有说清其中的含义,导致我这样的草根花了很长的时间才完全理解这段文字的思想。
Note that specifying objects in display coordinates will change their location if the dpi
of the figure changes. This can cause confusion when printing or changing screen resolution, because the object can change location and size. Therefore it is most common for artists placed in an axes or figure to have their transform set to something other than the IdentityTransform()
; the default when an artist is placed on an axes using add_artist
is for the transform to be ax.transData
.
上表有三列:
display coordinate
系统的坐标;在 matplotlib 中,创建对象时,首先就要告诉 matplotlib 这个对象放在哪个位置,即提供一个坐标参数 (x, y)
。 由于 matplotlib 有多个坐标系统,所以你还需要告诉 matplotlib 这个坐标值是哪个坐标系统的坐标。在 matplotlib 中,默认是 data
坐标。
非常重要: 在创建 artist 时,一般都有一个 transform
参数,传递上表中的一个 Transformation object
给 transform 参数,实际上就是指定你提供的坐标 (x, y), size 值是基于该坐标系统的。
在前面的代码中,我们指定 Circle 的 transform = ax.transAxes
,得到的图形就是这样的:
circ = patches.Circle((0.5,0.5),0.4,color='r',fill=False, transform=ax.transAxes)
Axes 坐标系,(0, 0) 是 axes 的左下角, (1, 1) 是 axes 的右上角。
所以 (0.5, 0.5) 在 axes 的中间,radius = 0.4, diameter = 0.8, 占据了 axes 的 80% 空间。
下面,我们就逐个讨论上表中列出的坐标系,并给出示例。
指定 artist 的坐标、尺寸是 data
坐标系统的方法是将 ax.transData
实例传递给 tranform
参数。 matplotlib 默认为 data
坐标系。
data
坐标系,由 xlim 和 ylim 控制。 即提供的坐标值 (x,y)、size
值,在 xaxis, yaxis 方向上都是相对于 xlim, ylim 的,在前面的例子中,由于 xlim, ylim 的比例不一样(即 xaxis 的一个单位长度与 yaxis 一个单位的长度不等),所以圆看起来成了椭圆。
向坐标轴添加数据,Matplotlib 都会自动更新数据界限。注意: 当我们只更改ylim时,只有y-display坐标被更改,当我们也更改xlim时,两者都被更改。
也可以使用 set_xlim()
和 set_ylim()
方法,强制设置数据界限。
在向 axes 中添加数据前, xlime = ylime = (0,1), 但由于 figure, axes 的 width, height 可能不同,所以 xaxis 一个单位与 yaxis 一个单位的物理长度可能不同。
如下图,圆虽然看起来是椭圆,但它在 xaxis, yaxis 方向上的 diameter 都是 ‘0.5*2 = 1’。
circ = patches.Circle((0.5,0.5),0.5, color='r', fill=False) #默认是 *data* 坐标
下面手动设置 xlim, ylim, 让 xaxis, yaxis 的一个单位的物理长度相等,效果如下:
ax.set_xlim(0, 6.4/4.8)
ax.set_ylim(0,1.0)
ax.set_position((0.05,0.05,0.95,0.95))
circ = patches.Circle((0.5,0.5),0.5, color='r', fill=False)
print(ax.get_position())
Bbox(x0=0.05, y0=0.05, x1=1.0, y1=1.0)
Circle
是 Ellipse
子类:
class matplotlib.patches.Ellipse(xy, width, height, angle=0, **kwargs)
Bases: matplotlib.patches.Patch
class matplotlib.patches.Circle(xy, radius=5, **kwargs)
Bases: matplotlib.patches.Ellipse
但 Circle
的行为与 Ellipse
大不同。
下图展示了它们的不同,也能进一步说明 data
坐标系的特点。
图中绿色实心的是 Ellipse 对象, 红边空心的是 Circle 对象。
# Circle 的 radius 参数是半径;
# Ellips 的 width, height 是 total, 即 diameter,
# 在 'data' 坐标系统下,提供的坐标值和 size 值都是相对于 xlim, ylim 的,即相应 axis 的一个单位
circ = patches.Circle((5,0.0),1, color='r', fill=False)
ell = patches.Ellipse((5,0.0),1,1, color='g')
rect = patches.Rectangle((2,-1),2,0.5)
ax.add_artist(circ)
ax.add_artist(rect)
ax.add_artist(ell)
除了data
坐标系,axes
是第二有用的坐标系。
点(0,0)
是axes
或subplot
的左下角,(0.5,0.5)
是中心,(1.0,1.0)
是右上角。
还可以引用上述范围之外的点,因此(-0.1,1.1)
位于坐标轴的左侧和上方。
当将文本放置在axes中时,这个坐标系非常有用,因为我们通常希望在固定的位置 (例如axes
窗格的左上角) 中有一个文本气泡,并且在平移或缩放时该位置保持不变。
下面是在学术期刊中经常看到的一个简单的例子,它创建了四个 axes,并将它们标记为“A”、“B”、“C”和“D”:
# ax.transAxes 将整个 axes 看成一个([0,0],[1.0,1.0])的矩形;
# transform = ax.transAxes 就是将点 (0.05,0.95) 看成 ax 矩形区域中的点
# 添加数据,改变了 xlim, ylim, 但不会影响 ax.transAxes 的行为
for i, label in enumerate(('A', 'B', 'C', 'D')):
ax = fig.add_subplot(2, 2, i+1)
ax.plot(x,y)
ax.text(0.05, 0.95, label, transform=ax.transAxes,
fontsize=16, fontweight='bold', va='top')
可以看到,向 axes 中添加数据后, xlim, ylim 的变化没有改变 text 的位置。
下面的例子中,两个 axes 的 xlim, ylim 不同,在将 text 添加到 axes 中时,指定 transform = ax.transAxes
,text 很好地保持在相应的 axes 的中间。
#注意体会和比较两个axes中text的位置
txt_a = Text(0.5,0.5,'axa.transAxes',transform=axa.transAxes,fontsize=12,ha='center')
txt_b = Text(0.5,0.5,'axb.transAxes',transform=axb.transAxes,fontsize=12,ha='center')
transform = fig.transFigure
指定为 figure
坐标系, (0, 0) 是 figure 的左下角, (1, 1) 是 figure 的右上角。
这是相对数,包括对象在水平和垂直方向上的尺寸也是相对于 Figure 的宽和高的,这一点与 axes 非常相似。
下面的例子,在 fig.transFigure 坐标系统中绘制了 Circele, Ellipse, Rectangle,它们的坐标和尺寸不受 axes, data 的影响。
# Circle 的 radius 参数是半径;
# Ellips 的 width, height 是 total, 即 diameter,
# 在 'figure' 坐标系统下,提供的坐标值和 size 值都是相对于 Figure 的 width, height 的
circ = patches.Circle((0.5,0.5),0.3, color='r', fill=False, transform=fig.transFigure)
ell = patches.Ellipse((0.5,0.5),0.3,0.3, color='g',transform=fig.transFigure,fill=False)
rect = patches.Rectangle((0.5,0.5),0.2,0.2,transform=fig.transFigure, fill=False)
fig.add_artist(circ)
fig.add_artist(rect)
fig.add_artist(ell)
“figure-inches”
fig.dpi_scale_transFigure
以 inches 为单位, (0, 0) 是 figure 的左下角, (width, height) 是 figure 的右上角。
这是绝对数,即物理尺寸,包括对象在水平和垂直方向上的尺寸也是绝对值,以 inches 为单位。
下面的例子中,3 个 pathces 的定义代码与前面的区别仅仅是 transfor=transform=fig.dpi_scale_trans
,注意-它们的位置和尺寸变化(图的左下角):
# Circle 的 radius 参数是半径;
# Ellips 的 width, height 是 total, 即 diameter,
# 在 'figure-inches' 坐标系统下,提供的坐标值和 size 值都是以 inches 为单位的绝对值
circ = patches.Circle((0.5,0.5),0.3, color='r', fill=False, transform=fig.dpi_scale_trans)
ell = patches.Ellipse((0.5,0.5),0.3,0.3, color='g',transform=fig.dpi_scale_trans,fill=False)
rect = patches.Rectangle((0.5,0.5),0.2,0.2,transform=fig.dpi_scale_trans, fill=False)
fig.add_artist(circ)
fig.add_artist(rect)
fig.add_artist(ell)
None, or IdentityTransform()
一个特殊的类对象,什么都不做,快速的实现恒等变换。
显示窗口的 pixel 坐标系统;(0, 0) 是显示窗口的左下角, (width, height) 是显示窗口的右上角,以 pixels 为单位。
这是绝对数,包括对象在水平和垂直方向上的尺寸也是绝对值,但是以 pixels 为单位。注意:这时,对象以 inches 为单位的物理尺寸还与 dpi 有关。
display 坐标系统中的 artist 坐标(x,y),size 是以像素为单位的绝对值,不受 figure, axes, data 的长宽比影响。请看下例:
# 100dpi, 50 pixel = 50/100 inches
circ = patches.Circle(ax.transData.transform((0.5,0.5)),50, transform=None, color='r', fill=False)
ax.add_artist(circ)
混合坐标系:在一个 axis 上使用 data 坐标,在另一个上使用 axes 坐标系。
在混合 axes 和 data 坐标系的 blended 混合坐标系统中绘图非常有用,例如,创建一个水平跨距突出显示 y 数据的某些区域,但在 x-axis 轴上的跨距不受 x 数据的限制,移动和缩放等的影响。
事实上,混合坐标的 lines 和 spans 非常有用, matplotlib 已经内置了一些函数以便捷地绘制这样的图形元素(参见 axhline()
, axvline()
, axhspan()
, axvspan()
)
通常使用下面的工厂函数创建混合坐标系的 Transformation object
对象:
matplotlib.transforms.blended_transform_factory(x_transform, y_transform)
创建一个 “blended” 转换,用 x_transform
转换 x-axis,用 y_transform
转换 y-axis.
两个子变换都是 affine (仿射几何) 变换,快速返回混合变换。
为了说明混合坐标的用法,下面用 blend 转换实现一个水平跨距。这个技巧仅适用与可分离变换的坐标系,比如常见的笛卡尔坐标系。不适用于不可分离变换的坐标系,如 PolarTransform
.
# x-axis=transData, y-axis=transAxes
trans = transforms.blended_transform_factory(
ax.transData, ax.transAxes)
# 高亮 1..2 跨度范围.
# x-axis 方向上 始终保持 1 个数据单位, y-axis 方向上在 axes 坐标系统中 从 0..1.
rect = mpatches.Rectangle((1, 0), width=1, height=1, transform=trans,
color='yellow', alpha=0.5)
ax.add_patch(rect)
注意:
在 x 上应用 data 坐标,在 y 上应用 axes 坐标的 blended 转换非常有用,所以 matplotlib 还设计了一些辅助方法用于返回 matplotlib 内部用于绘制 ticks, ticklabels, 等的版本。这些方法有 matplotlib.axes.Axes.get_xaxis_transform()
和 matplotlib.axes.Axes.get_yaxis_transform()
. 所以,在上例中,调用 blended_transform_factory()
可以被替换为get_xaxis_transform
:
trans = ax.get_xaxis_transform() #实现同上的效果
# 高亮 1..2 跨度范围.
# x-axis 方向上 始终保持 1 个数据单位, y-axis 方向上在 axes 坐标系统中 从 0..1.
rect = mpatches.Rectangle((1, 0), width=1, height=1, transform=trans,
color='yellow', alpha=0.5)
# 实现横向的跨距
trans = ax.get_yaxis_transform()
# 高亮 1..2 跨度范围.
# x-axis 方向上 始终保持 1 个数据单位, y-axis 方向上在 axes 坐标系统中 从 0..1.
rect = mpatches.Rectangle((0, 30), width=20, height=20, transform=trans,
color='yellow', alpha=0.5)
ax.add_patch(rect)
有时我们需要求取不同坐标系中数据点的坐标值,即在两个相反方向上的坐标变换:
在开始的那张表中,Transformation object 列中的对象都是 Transform
类对象的一个实例,它们都有一个 transform
方法,接受其对应的坐标系中的输入(点坐标),并将输入转换为 display 坐标系(返回该点在display坐标系中的坐标值)。
这就是为什么 display 坐标系中没有Transformation object的原因,它已经在显示坐标系中了。
这些转换还知道如何反转 invert 它们自己,即从 display 坐标转回到原来的坐标系。这在处理来自用户界面的事件时特别有用,这些事件通常发生在 display 空间中,这时您想知道鼠标单击或按键在数据坐标系统中的位置。
inverted()方法可以创建一个反向的Transform对象,该对象的*transform()*方法用于将显示坐标转换为相应坐标系统中的坐标。
transform(self, values)
在给定的 values 数组上应用该转换。(返回该点的 display 坐标)。
inverted(self)
它通过 x == self.inverted().transform(self.transform(x))
,生成一个逆向转换的实例,再使用这个逆转换的transform
方法获取逆向坐标值。
用语言表述有点绕,看示例就很简单:
circ_data = patches.Circle((0.2,0.2),0.2, color='r', fill=False)
circ_axes = patches.Circle((0.4,0.4),0.1, color='g', fill=False, transform=ax.transAxes)
circ_fig = patches.Circle((0.7,0.7),0.1, color='b', transform=fig.transFigure)
ax.add_artist(circ_data)
ax.add_artist(circ_axes)
fig.add_artist(circ_fig)
上图中 3 个 Circle 的圆心指定了不同的坐标系,下面做两个方向上的变换:
# data --> display
#将 ax 中的数据点(0.2,0.2)转换为显示坐标
#单个坐标点以二元元组的形式提供
#返回一个array
ax.transData.transform((0.2,0.2))
output: array([ 68.13227919, 210.64898683])
# 生成一个逆转换实例
inv = ax.transData.inverted()
inv
output:
# 逆转换的 transform 将 display 坐标转换为 data 坐标
inv.transform((68.13227919, 210.64898683))
output: array([0.2, 0.2])
ax.transAxes.transform((0.4,0.4))
output: array([207.570048, 149.040048])
# 也可以这样写:
ax.transAxes.inverted().transform([207.570048, 149.040048])
output: array([0.4, 0.4])
fig.transFigure.transform((0.7,0.7))
output: array([322.56, 241.92])
fig.transFigure.inverted().transform([230.4, 172.8])
output: array([0.5, 0.5])
#多个点以list形式提供
#list的每个元素是一个二元元组
ax.transData.transform([(0.2, 0.2), (0.4, 0.4)])
output: array([[ 68.13227919, 210.64898683],
[ 76.55752202, 239.98254973]])
transformation object 对象是 matplotlib.transforms 模块中 Transform
类及其子类的实例:
ax.transData
output:
ax.transAxes
output:
fig.transFigure
output:
print(ax.transData)
CompositeGenericTransform(
TransformWrapper(
BlendedAffine2D(
IdentityTransform(),
IdentityTransform())),
CompositeGenericTransform(
BboxTransformFrom(
TransformedBbox(
Bbox(x0=-0.45, y0=-1.0998676467503805, x1=9.45, y1=1.0996573219097678),
TransformWrapper(
BlendedAffine2D(
IdentityTransform(),
IdentityTransform())))),
BboxTransformTo(
TransformedBbox(
Bbox(x0=0.08843368055555555, y0=0.05787106481481483, x1=0.9934890624999999, y1=0.99131875),
BboxTransformTo(
TransformedBbox(
Bbox(x0=0.0, y0=0.0, x1=6.4, y1=4.8),
Affine2D(
[[72. 0. 0.]
[ 0. 72. 0.]
[ 0. 0. 1.]])))))))
因为你窗口大小或dpi设置可能不同,则显示坐标的确切值可能会有所不同。同样,在不同的终端中,标记为 points 的显示可能与 ipython 会话中的显示不一样,因为文档的默认数字大小不同。
matplotlib.transforms
模块的 ScaledTranslation
类创建一个与另一个转换偏移的新转换,例如,放置一个相对于另一个对象移动了一点的对象。通常,你希望移动量是物理尺寸,如点或英寸,而不是数据坐标,这样移动效果在不同的缩放级别和dpi设置下是恒定的。
利用ScaledTranslation
转换的这一特性,可以实现阴影效果。
class matplotlib.transforms.ScaledTranslation(xt, yt, scale_trans, **kwargs)
#该示例详细对比演示了坐标变换偏移实现阴影的效果
dx, dy = 2/72., -2/72.
offset = transforms.ScaledTranslation(dx, dy, fig.dpi_scale_trans)
shadow_transform = axa.transData + offset
x = np.arange(0, 2*math.pi, 0.001)
y = np.sin(x)
line, = axa.plot(x, y, lw=3, color='blue')
#绘制一个相同,但偏移的曲线
#用灰色模拟阴影效果
axa.plot(x, y, lw=3, color='gray',
transform=shadow_transform,
zorder=0.5*line.get_zorder())
axb.plot(x*0.5, y*1.2, lw=3, color='blue')
上例,我们首先用数据单位(ax.transData)绘制,然后使用fig.dpi_scale_trans
移动dx
和dy
点。(在排版中,一个’point’是1/72英寸,通过指定以点为单位的偏移量,无论保存的dpi分辨率如何,您的图形看起来都是一样的。)
在官方文档中有一个使用坐标变换实现 Hammer projection (赤道投影,汉麦尔投影) 的示例:
matplotlib 基于 python 语言的先天优势,可以实现绝大多数你想要的绘图,问题仅仅在于你对他的掌控力和代码的复杂度!