变换教程#

与任何图形包一样,Matplotlib 构建在一个变换框架之上,以便在坐标系之间轻松移动,包括用户域的 数据 坐标系、 坐标系、图形 坐标系和 显示 坐标系。在 95% 的绘图过程中,您不需要考虑这一点,因为它在幕后发生,但当您推动自定义图形生成的极限时,了解这些对象会有所帮助,这样您可以重用 Matplotlib 提供的现有变换,或者创建自己的变换(参见 matplotlib.transforms)。下表总结了一些有用的坐标系、每个系统的描述以及从每个坐标系到 显示 坐标的变换对象。在“变换对象”列中,axAxes 实例,figFigure 实例,subfigureSubFigure 实例。

坐标系

描述

从系统到显示的转换对象

“数据”

Axes 中数据的坐标系。

ax.transData

“轴”

Axes 的坐标系统;(0, 0) 是 Axes 的左下角,(1, 1) 是 Axes 的右上角。

ax.transAxes

subfigure

SubFigure 的坐标系统;(0, 0) 是子图的左下角,(1, 1) 是子图的右上角。如果一个图形没有子图,这与 transFigure 相同。

subfigure.transSubfigure

figure

Figure 的坐标系统;(0, 0) 是图的左下角,(1, 1) 是图的右上角。

fig.transFigure

figure-inches

Figure 的坐标系统以英寸为单位;(0, 0) 是图形的左下角,而 (width, height) 是图形的右上角,单位为英寸。

fig.dpi_scale_trans

“x轴”, “y轴”

混合坐标系,在一个方向上使用数据坐标,在另一个方向上使用轴坐标。

ax.get_xaxis_transform(), ax.get_yaxis_transform()

“显示”

输出原生坐标系的原点 (0, 0) 位于窗口的左下角,而 (width, height) 位于输出的右上角,单位为“显示单位”。

单位的精确解释取决于后端。例如,对于Agg来说是像素,而对于svg/pdf来说是点。

None,或 IdentityTransform()

Transform 对象对源坐标系和目标坐标系是不可知的,然而上表中提到的对象被构建为在其坐标系中接受输入,并将输入转换为 显示 坐标系。这就是为什么 显示 坐标系在“转换对象”列中为 None —— 它已经在 显示 坐标中了。命名和目标约定有助于跟踪可用的“标准”坐标系和转换。

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

请注意,在 显示 坐标中指定艺术家的位置可能会在 dpi 或图形大小改变时改变它们的相对位置。这可能会在打印或更改屏幕分辨率时造成混淆,因为对象的位置和大小可能会发生变化。因此,最常见的是将放置在 Axes 或图形中的艺术家的变换设置为 IdentityTransform() 以外的其他值;当使用 add_artist 将艺术家添加到 Axes 时,默认的变换是 ax.transData,这样你可以在 数据 坐标中工作并思考,而让 Matplotlib 负责转换到 显示 坐标。

数据坐标#

让我们从最常用的坐标系,即 数据 坐标系开始。每当你向 Axes 添加数据时,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()
transforms tutorial

你可以使用 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
# This computing the transform now, if anything
# (figure size, dpi, axes placement, data limits, scales..)
# changes re-calling transform will get a different value.
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()
transforms tutorial

警告

如果你在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])

轴坐标#

数据 坐标系之后, 可能是第二有用的坐标系。在这里,点 (0, 0) 是你的 Axes 或子图的左下角,(0.5, 0.5) 是中心,(1.0, 1.0) 是右上角。你也可以引用范围外的点,所以 (-0.1, 1.1) 在你的 Axes 的左边和上方。这个坐标系在放置 Axes 中的文本时非常有用,因为你通常希望文本气泡位于固定的位置,例如 Axes 窗格的左上角,并且希望该位置在平移或缩放时保持固定。这里有一个简单的例子,创建了四个面板并分别标记为 'A', 'B', 'C', 'D',这通常出现在期刊中。对于这种标记的更复杂的方法,请参见 子图标签

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()
transforms tutorial

你也可以在 axes 坐标系中绘制线条或补丁,但在我的经验中,这不如使用 ax.transAxes 来放置文本有用。尽管如此,这里有一个愚蠢的例子,它在数据空间中绘制了一些随机点,并在 Axes 的中心覆盖了一个半透明的 Circle,其半径为 Axes 的四分之一——如果你的 Axes 不保持纵横比(参见 set_aspect()),这看起来会像一个椭圆。使用平移/缩放工具移动,或手动更改数据 xlim 和 ylim,你会看到数据移动,但圆圈将保持固定,因为它不在 data 坐标中,并且将始终保持在 Axes 的中心。

fig, ax = plt.subplots()
x, y = 10*np.random.rand(2, 1000)
ax.plot(x, y, 'go', alpha=0.2)  # plot some data in data coordinates

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

混合变换#

在混合坐标空间中绘图,将*轴*与*数据*坐标混合使用,非常有用,例如创建一个水平跨度,突出显示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)

# the x coords of this transformation are data, and the y coord are axes
trans = transforms.blended_transform_factory(
    ax.transData, ax.transAxes)
# highlight the 1..2 stddev region with a span.
# We want x to be in data coordinates and y to span from 0..1 in axes coords.
rect = mpatches.Rectangle((1, 0), width=1, height=1, transform=trans,
                          color='yellow', alpha=0.5)
ax.add_patch(rect)

plt.show()
$\sigma=1 \/ \dots \/ \sigma=2$

备注

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

trans = ax.get_xaxis_transform()

在物理坐标中绘图#

有时我们希望对象在图上具有特定的物理尺寸。这里我们绘制了与上面相同的圆,但在物理坐标中。如果以交互方式完成,您可以看到改变图形的大小不会改变圆从左下角的偏移量,不会改变其大小,并且圆无论Axes的纵横比如何都保持为圆。

fig, ax = plt.subplots(figsize=(5, 4))
x, y = 10*np.random.rand(2, 1000)
ax.plot(x, y*10., 'go', alpha=0.2)  # plot some data in data coordinates
# add a circle in fixed-coordinates
circ = mpatches.Circle((2.5, 2), 1.0, transform=fig.dpi_scale_trans,
                       facecolor='blue', alpha=0.75)
ax.add_patch(circ)
plt.show()
transforms tutorial

如果我们改变图形的大小,圆的绝对位置不会改变,并且会被裁剪。

fig, ax = plt.subplots(figsize=(7, 2))
x, y = 10*np.random.rand(2, 1000)
ax.plot(x, y*10., 'go', alpha=0.2)  # plot some data in data coordinates
# add a circle in fixed-coordinates
circ = mpatches.Circle((2.5, 2), 1.0, transform=fig.dpi_scale_trans,
                       facecolor='blue', alpha=0.75)
ax.add_patch(circ)
plt.show()
transforms tutorial

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

trans = ScaledTranslation(xt, yt, scale_trans)

其中 xtyt 是平移偏移量,而 scale_trans 是一个变换,它在应用偏移量之前在变换时缩放 xtyt

注意下面变换中加号运算符的使用。这段代码表示:首先应用缩放变换 fig.dpi_scale_trans 使椭圆达到适当的大小,但仍以 (0, 0) 为中心,然后将数据平移到数据空间中的 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))

# plot an ellipse around the point that is 150 x 130 points in diameter...
circle = mpatches.Ellipse((0, 0), 150/72, 130/72, angle=40,
                          fill=None, transform=trans)
ax.add_patch(circle)
plt.show()
transforms tutorial

备注

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

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

另一个使用 ScaledTranslation 的例子是创建一个从另一个变换偏移的新变换,例如,将一个对象相对于另一个对象稍微移动。通常,您希望偏移量是某种物理维度,如点或英寸,而不是在 数据 坐标中,以便在不同的缩放级别和 dpi 设置下,偏移效果是恒定的。

偏移的一个用途是创建阴影效果,你可以在第一个对象的右侧和下方绘制一个与其完全相同的对象,调整zorder以确保阴影首先绘制,然后在其上方绘制被阴影覆盖的对象。

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

fig, ax = plt.subplots()

# make a simple sine wave
x = np.arange(0., 2., 0.01)
y = np.sin(2*np.pi*x)
line, = ax.plot(x, y, lw=3, color='blue')

# shift the object over 2 points, and down 2 points
dx, dy = 2/72., -2/72.
offset = transforms.ScaledTranslation(dx, dy, fig.dpi_scale_trans)
shadow_transform = ax.transData + offset

# now plot the same data with our offset transform;
# use the zorder to make sure we are below the line
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()
creating a shadow effect with an offset transform

备注

dpi 和英寸偏移是一个常见的需求,为此我们有一个专门的辅助函数来创建它,即 matplotlib.transforms.offset_copy(),它返回一个带有附加偏移的新变换。因此,上面我们可以这样做:

shadow_transform = transforms.offset_copy(ax.transData,
         fig, dx, dy, units='inches')

转换管道#

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

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

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

我们在 轴坐标 中介绍了 transAxes 实例,它将 Axes 或子图边界框的 (0, 0), (1, 1) 角映射到 显示 空间,所以让我们来看看其他这两部分。

self.transLimits 是将你从 数据 坐标转换到 坐标的变换;即,它将你的视图 xlim 和 ylim 映射到 Axes 的单位空间(然后 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 属性,它负责数据的可选非线性缩放,例如对数轴。当 Axes 最初设置时,这仅设置为恒等变换,因为基本的 Matplotlib 轴具有线性比例,但当你调用对数缩放函数如 semilogx() 或使用 set_xscale() 显式将对数设置为对数时,那么 ax.transScale 属性将被设置来处理非线性投影。缩放变换是各自 xaxisyaxis Axis 实例的属性。例如,当你调用 ax.set_xscale('log') 时,xaxis 将其比例更新为 matplotlib.scale.LogScale 实例。

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

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

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

脚本总运行时间: (0 分钟 1.030 秒)

由 Sphinx-Gallery 生成的图库