Matplotlib 全面使用指南 -- 转换教程 Transformations Tutorial

文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。

Matplotlib

转换教程

和所有图形软件包一样,Matplotlib 构建在一套变换框架之上,以便在不同坐标系之间轻松切换:用户空间的**数据(data)**坐标系、**坐标轴(axes)**坐标系、图形(figure)坐标系以及显示(display)坐标系。在日常绘图的 95% 场景中,你无需关心这些,因为框架会在内部自动完成变换;但当你开始定制复杂图形时,理解这些对象将大有裨益——你可以复用 Matplotlib 提供的现成变换,也可以自行创建(参见 matplotlib.transforms)。下表汇总了几种常用的坐标系、每种坐标系的描述以及从该坐标系映射到显示坐标系的变换对象。在“变换对象”列中,axAxes 实例,figFigure 实例,subfigureSubFigure 实例。

坐标系描述变换对象(到 display)
data用户空间的数据坐标系ax.transData
axes坐标轴坐标系;(0, 0) 为左下角,(1, 1) 为右上角ax.transAxes
figure图形坐标系;(0, 0) 为左下角,(1, 1) 为右上角fig.transFigure
subfigure子图坐标系;(0, 0) 为左下角,(1, 1) 为右上角subfigure.transSubfigure
display显示坐标系(像素)None

Transform 对象本身不关心源坐标系和目标坐标系,但上表提到的对象被构造为:接收其所在坐标系的输入,并将输入变换到显示坐标系。因此显示坐标系在“变换对象”列中为 None——它已经是显示坐标。命名和目的地约定是为了便于跟踪可用的“标准”坐标系与变换。

这些变换也知道如何反演自己(通过 Transform.inverted),从而生成从输出坐标系回到输入坐标系的变换。例如,ax.transData 将数据坐标值转换为显示坐标,而 ax.transData.inverted() 则是一个 matplotlib.transforms.Transform,可将显示坐标转回数据坐标。这在处理用户界面事件时特别有用——事件通常发生在显示空间,而你需要知道鼠标点击或按键在你的数据坐标系中的位置。

请注意,在显示坐标系中指定 Artist 的位置时,如果 dpi 或图形尺寸发生变化,它们的相对位置可能会改变。这可能会在打印或更改屏幕分辨率时引起困惑,因为对象的位置和大小可能会改变。因此,通常不会将坐标轴或图形中的 artist 的变换设置为 IdentityTransform();使用 add_artist 将 artist 添加到坐标轴时,默认的变换是 ax.transData,这样你就可以在数据坐标系中工作,而让 Matplotlib 负责变换到显示坐标。

数据坐标

我们从最常用的坐标系——数据坐标系开始说起。每当你向坐标轴添加数据时,Matplotlib 会更新数据限制,通常通过 set_xlim()set_ylim() 方法进行。例如,在下图中,数据限制在 x 轴上从 0 到 10,y 轴上从 -1 到 1。

import matplotlib.pyplot as plt
import numpy as np

import matplotlib.patches as mpatches

x = np.arange(0, 10, 0.005)
y = np.exp(-x/2.) * np.sin(2*np.pi*x)

fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_xlim(0, 10)
ax.set_ylim(-1, 1)

plt.show()

img

你可以使用 ax.transData 实例将数据坐标系中的点或点序列变换到显示坐标系,如下所示:

In [14]: type(ax.transData)
Out[14]: <class 'matplotlib.transforms.CompositeGenericTransform'>

In [15]: ax.transData.transform((5, 0))
Out[15]: array([ 335.175,  247.   ])

In [16]: ax.transData.transform([(5, 0), (1, 2)])
Out[16]:
array([[ 335.175,  247.   ],
       [ 132.435,  642.2  ]])

你也可以使用 inverted() 方法创建一个从显示坐标系回到数据坐标系的变换:

In [41]: inv = ax.transData.inverted()

In [42]: type(inv)
Out[42]: <class 'matplotlib.transforms.CompositeGenericTransform'>

In [43]: inv.transform((335.175,  247.))
Out[43]: array([ 5.,  0.])

如果你跟着本教程一起输入代码,由于窗口大小或 dpi 设置不同,显示坐标的精确值可能会有所不同。同样,在下图中,标注的显示点可能与 IPython 会话中的不同,因为文档默认的图形尺寸不同。

x = np.arange(0, 10, 0.005)
y = np.exp(-x/2.) * np.sin(2*np.pi*x)

fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_xlim(0, 10)
ax.set_ylim(-1, 1)

xdata, ydata = 5, 0
# 现在计算变换,如果任何因素(图形尺寸、dpi、坐标轴位置、数据限制、缩放等)发生变化,重新调用 transform 会得到不同的值。
xdisplay, ydisplay = ax.transData.transform((xdata, ydata))

bbox = dict(boxstyle="round", fc="0.8")
arrowprops = dict(
    arrowstyle="->",
    connectionstyle="angle,angleA=0,angleB=90,rad=10")

offset = 72
ax.annotate(f'data = ({xdata:.1f}, {ydata:.1f})',
            (xdata, ydata), xytext=(-2*offset, offset), textcoords='offset points',
            bbox=bbox, arrowprops=arrowprops)

disp = ax.annotate(f'display = ({xdisplay:.1f}, {ydisplay:.1f})',
                   (xdisplay, ydisplay), xytext=(0.5*offset, -offset),
                   xycoords='figure pixels',
                   textcoords='offset points',
                   bbox=bbox, arrowprops=arrowprops)

plt.show()

img

警告:如果你在 GUI 后端运行上面的示例代码,可能会发现数据显示标注的两个箭头没有精确指向同一点。这是因为显示点是在图形显示之前计算的,而 GUI 后端在创建图形时可能会略微调整图形尺寸。如果你自己调整图形大小,这种效果会更明显。这也是你很少想在显示空间工作的一个好理由,但你可以通过连接 'on_draw' Event 在图形绘制时更新图形坐标;参见事件处理与拾取

当你更改坐标轴的 x 或 y 限制时,数据限制会更新,从而使变换产生新的显示点。请注意,当我们只更改 ylim 时,只有 y 显示坐标会改变,而当我们同时更改 xlim 时,两个坐标都会改变。稍后讨论 Bbox 时会进一步说明。

In [54]: ax.transData.transform((5, 0))
Out[54]: array([ 335.175,  247.   ])

In [55]: ax.set_ylim(-1, 2)
Out[55]: (-1, 2)

In [56]: ax.transData.transform((5, 0))
Out[56]: array([ 335.175     ,  181.13333333])

In [57]: ax.set_xlim(10, 20)
Out[57]: (10, 20)

In [58]: ax.transData.transform((5, 0))
Out[58]: array([-171.675     ,  181.13333333])

坐标轴坐标

数据坐标系之后,**坐标轴(axes)**坐标系可能是第二有用的坐标系。在这里,点 (0, 0) 是坐标轴或子图的左下角,(0.5, 0.5) 是中心,(1.0, 1.0) 是右上角。你也可以引用范围之外的点,因此 (-0.1, 1.1) 位于坐标轴的左侧和上方。当你需要在坐标轴中放置文本时,这个坐标系非常有用,因为你通常希望文本气泡位于固定位置(例如坐标轴窗格的左上角),并且在平移或缩放时保持该位置不变。下面是一个简单示例,创建四个面板并按期刊常见方式标记为 ‘A’、‘B’、‘C’、‘D’。更复杂的子图标记方法见 Labelling subplots

fig = plt.figure()
for i, label in enumerate(('A', 'B', 'C', 'D')):
    ax = fig.add_subplot(2, 2, i+1)
    ax.text(0.05, 0.95, label, transform=ax.transAxes,
            fontsize=16, fontweight='bold', va='top')

plt.show()

img

你也可以在坐标轴坐标系中绘制线条或补丁,但根据我的经验,这不如使用 ax.transAxes 放置文本有用。尽管如此,下面是一个简单的示例,在数据空间中绘制一些随机点,并在坐标轴中心叠加一个半透明的 Circle,半径为坐标轴的四分之一——如果你的坐标轴未保持宽高比(参见 set_aspect()),它将看起来像一个椭圆。使用平移/缩放工具移动,或手动更改数据 xlim 和 ylim,你会看到数据移动,但圆保持固定,因为它不在数据坐标系中,始终位于坐标轴中心。

fig, ax = plt.subplots()
x, y = 10*np.random.rand(2, 1000)
ax.plot(x, y, 'go', alpha=0.2)  # 在数据坐标系中绘制一些数据

circ = mpatches.Circle((0.5, 0.5), 0.25, transform=ax.transAxes,
                       facecolor='blue', alpha=0.75)
ax.add_patch(circ)
plt.show()

img

混合变换

混合坐标空间中绘图,即混合坐标轴坐标与数据坐标,非常有用,例如创建一个水平跨度,突出显示 y 数据的某个区域,但该跨度在 x 轴方向上跨越整个坐标轴,无论数据限制、平移或缩放级别如何。事实上,这些混合线条和跨度非常有用,我们内置了便捷的绘图函数(参见 axhline()axvline()axhspan()axvspan()),但为了教学目的,我们将使用混合变换手动实现水平跨度。这个技巧仅适用于可分离变换,如常规笛卡尔坐标系,但不适用于不可分离变换如 PolarTransform

import matplotlib.transforms as transforms

fig, ax = plt.subplots()
x = np.random.randn(1000)

ax.hist(x, 30)
ax.set_title(r'$\sigma=1 \/ \dots \/ \sigma=2$', fontsize=16)

# 此变换的 x 坐标为数据坐标,y 坐标为坐标轴坐标
trans = transforms.blended_transform_factory(
    ax.transData, ax.transAxes)
# 用跨度突出 1..2 标准差区域。
# 我们希望 x 为数据坐标,y 从 0..1 为坐标轴坐标。
rect = mpatches.Rectangle((1, 0), width=1, height=1, transform=trans,
                          color='yellow', alpha=0.5)
ax.add_patch(rect)

plt.show()

img

注释

混合变换(其中 x 是数据坐标,y 是轴坐标)非常有用,因此我们提供了辅助方法来返回 Matplotlib 内部用于绘制刻度线、刻度线标签等的版本。这些方法是 matplotlib.axes.Axes.get_xaxis_transform() 和 matplotlib.axes.Axes.get_yaxis_transform()。因此,在上面的示例中,可以用 get_xaxis_transform 代替调用 blended_transform_factory():

trans = ax.get_xaxis_transform()

以物理坐标绘图

有时我们希望图形中的对象具有固定的物理尺寸。这里我们绘制与上面相同的圆,但以物理坐标绘制。如果以交互方式操作,你会发现更改图形尺寸不会改变圆与左下角的偏移量,也不会改变其大小,并且无论坐标轴的宽高比如何,圆始终保持圆形。

fig, ax = plt.subplots(figsize=(5, 4))
x, y = 10*np.random.rand(2, 1000)
ax.plot(x, y*10., 'go', alpha=0.2)  # 在数据坐标系中绘制一些数据
# 在固定坐标中添加一个圆
circ = mpatches.Circle((2.5, 2), 1.0, transform=fig.dpi_scale_trans,
                       facecolor='blue', alpha=0.75)
ax.add_patch(circ)
plt.show()

img

如果我们更改图形尺寸,圆不会更改其绝对位置,并且可能被裁剪。

fig, ax = plt.subplots(figsize=(7, 2))
x, y = 10*np.random.rand(2, 1000)
ax.plot(x, y*10., 'go', alpha=0.2)  # 在数据坐标系中绘制一些数据
# 在固定坐标中添加一个圆
circ = mpatches.Circle((2.5, 2), 1.0, transform=fig.dpi_scale_trans,
                       facecolor='blue', alpha=0.75)
ax.add_patch(circ)
plt.show()

img

另一种用途是在坐标轴上的数据点周围放置一个具有固定物理尺寸的补丁。这里我们将两个变换组合在一起。第一个变换设置椭圆的大小,第二个变换设置其位置。椭圆最初位于原点,然后使用辅助变换 ScaledTranslation 将其移动到 ax.transData 坐标系中的正确位置。
该辅助变换的实例化方式为:

trans = ScaledTranslation(xt, yt, scale_trans)

其中 xtyt 是平移偏移量,scale_trans 是一个变换,在应用偏移量之前,于变换时对 xtyt 进行缩放。

请注意下面变换的加法运算符的使用。这段代码的意思是:首先应用缩放变换 fig.dpi_scale_trans 使椭圆具有正确的大小,但仍以 (0, 0) 为中心,然后通过 ax.transData 坐标系中的 xdata[0]ydata[0] 将数据平移到正确位置。

在交互使用中,即使通过缩放更改坐标轴限制,椭圆仍保持相同大小。

fig, ax = plt.subplots()
xdata, ydata = (0.2, 0.7), (0.5, 0.5)
ax.plot(xdata, ydata, "o")
ax.set_xlim((0, 1))

trans = (fig.dpi_scale_trans +
         transforms.ScaledTranslation(xdata[0], ydata[0], ax.transData))

# 在点周围绘制一个直径为 150 x 130 点的椭圆...
circle = mpatches.Ellipse((0, 0), 150/72, 130/72, angle=40,
                          fill=None, transform=trans)
ax.add_patch(circle)
plt.show()

img

注意

变换顺序很重要。这里椭圆首先在显示空间中获得正确尺寸,然后在数据空间中移动到正确位置。如果我们先做 ScaledTranslation,那么 xdata[0]ydata[0] 将首先被变换到显示坐标(在 200 dpi 显示器上为 [ 358.4 475.2]),然后这些坐标会被 fig.dpi_scale_trans 缩放,将椭圆中心推到屏幕外(即 [ 71680. 95040.])。

使用偏移变换创建阴影效果

ScaledTranslation 的另一个用途是创建相对于另一个变换偏移的新变换,例如将一个对象相对于另一个对象稍微偏移。通常,你希望偏移量以某种物理单位(如点或英寸)而非数据坐标表示,这样在不同缩放级别和 dpi 设置下,偏移效果保持一致。

一种用途是创建阴影效果,即绘制一个与第一个对象相同的对象,但向右和向下稍微偏移,并调整 zorder 以确保阴影先绘制,然后在其上方绘制被阴影覆盖的对象。

这里我们按与上面 ScaledTranslation 相反的顺序应用变换。绘图首先在数据坐标系(ax.transData)中完成,然后通过 dxdy 点使用 fig.dpi_scale_trans 进行偏移。(在排版中,一个点是 1/72 英寸,通过以点为单位指定偏移量,无论保存图形的 dpi 分辨率如何,图形看起来都相同。)

fig, ax = plt.subplots()

# 绘制一个简单的正弦波
x = np.arange(0., 2., 0.01)
y = np.sin(2*np.pi*x)
line, = ax.plot(x, y, lw=3, color='blue')

# 将对象向右移动 2 点,向下移动 2 点
dx, dy = 2/72., -2/72.
offset = transforms.ScaledTranslation(dx, dy, fig.dpi_scale_trans)
shadow_transform = ax.transData + offset

# 现在使用偏移变换绘制相同的数据;
# 使用 zorder 确保我们位于线条下方
ax.plot(x, y, lw=3, color='gray',
        transform=shadow_transform,
        zorder=0.5*line.get_zorder())

ax.set_title('creating a shadow effect with an offset transform')
plt.show()

img

注意

dpi 和英寸偏移是足够常见的用例,因此我们提供了一个专门的辅助函数 matplotlib.transforms.offset_copy() 来创建它,它返回一个添加了偏移量的新变换。因此,上面我们可以这样做:

变换流水线

我们在本教程中使用的 ax.transData 变换是由三个不同变换组合而成的,它们构成了从数据 -> 显示坐标的变换流水线。Michael Droettboom 实现了变换框架,精心提供了一个干净的 API,将极坐标和对数坐标图中发生的非线性投影和缩放,与平移和缩放时发生的线性仿射变换分离开来。这里有一个效率优势,因为你可以在坐标轴中平移和缩放,这会影响仿射变换,但可能不需要在简单的导航事件中计算潜在昂贵的非线性缩放或投影。还可以将仿射变换矩阵相乘,然后一步应用于坐标。并非所有可能的变换都支持这一点。

以下是在基本可分离坐标轴 Axes 类中 ax.transData 实例的定义方式:

self.transData = self.transScale + (self.transLimits + self.transAxes)

我们之前已经介绍了 transAxes 实例(在坐标轴坐标系中),它将坐标轴或子图边界框的 (0, 0)、(1, 1) 角映射到显示空间,让我们看看另外两个部分。

self.transLimits 是从数据坐标轴坐标的变换;即,它将你的视图 xlim 和 ylim 映射到坐标轴的单位空间(然后 transAxes 将该单位空间映射到显示空间)。我们可以在这里看到它的实际效果:

In [80]: ax = plt.subplot()

In [81]: ax.set_xlim(0, 10)
Out[81]: (0, 10)

In [82]: ax.set_ylim(-1, 1)
Out[82]: (-1, 1)

In [84]: ax.transLimits.transform((0, -1))
Out[84]: array([ 0.,  0.])

In [85]: ax.transLimits.transform((10, -1))
Out[85]: array([ 1.,  0.])

In [86]: ax.transLimits.transform((10, 1))
Out[86]: array([ 1.,  1.])

In [87]: ax.transLimits.transform((5, 0))
Out[87]: array([ 0.5,  0.5])

我们可以使用相同的逆变换从单位坐标轴坐标返回数据坐标。

In [90]: inv.transform((0.25, 0.25))
Out[90]: array([ 2.5, -0.5])

最后一部分是 self.transScale 属性,它负责数据的非线性缩放,例如对数坐标轴。当坐标轴最初设置时,这只是一个恒等变换,因为基本的 Matplotlib 坐标轴具有线性缩放,但当你调用对数缩放函数如 semilogx() 或通过 set_xscale() 显式将对数设置为缩放时,ax.transScale 属性会被设置为处理非线性投影。缩放变换是各自 xaxisyaxis Axis 实例的属性。例如,当你调用 ax.set_xscale('log') 时,xaxis 会将其缩放更新为 matplotlib.scale.LogScale 实例。

对于非可分离坐标轴 PolarAxes,还有需要考虑的另一部分,即投影变换。transData matplotlib.projections.polar.PolarAxes 类似于典型的可分离 matplotlib 坐标轴,但多了一个 transProjection 部分:

self.transData = (
    self.transScale + self.transShift + self.transProjection +
    (self.transProjectionAffine + self.transWedge + self.transAxes))

transProjection 处理从空间的投影,例如地图数据的纬度和经度,或极坐标数据的半径和 theta,到可分离笛卡尔坐标系。matplotlib.projections 包中有几个投影示例,了解更多最好的方法是打开这些包的源代码,看看如何制作自己的投影,因为 Matplotlib 支持可扩展的坐标轴和投影。Michael Droettboom 提供了一个创建 Hammer 投影坐标轴的不错的教程示例;参见自定义投影

Download Jupyter notebook: transforms_tutorial.ipynb

Download Python source code: transforms_tutorial.py

Download zipped: transforms_tutorial.zip

风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

船长Q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值