变换教程#

与任何图形软件包一样,Matplotlib 建立在转换框架之上,以便轻松地在坐标系之间移动,包括用户空间的 *数据* 坐标系、*轴* 坐标系、*图形* 坐标系和 *显示* 坐标系。在 95% 的绘图中,您不需要考虑这一点,因为它在后台自动完成,但当您突破自定义图形生成的限制时,了解这些对象将有所帮助,这样您就可以重用 Matplotlib 提供给您的现有转换,或者创建自己的转换(参见 matplotlib.transforms)。下表总结了一些有用的坐标系、每个系统的描述以及从每个坐标系到 *显示* 坐标系的转换对象。在“转换对象”列中,ax 是一个 Axes 实例,fig 是一个 Figure 实例,而 subfigure 是一个 SubFigure 实例。

坐标系

描述

从系统到显示的转换对象

"数据"

Axes 中数据的坐标系。

ax.transData

"轴"

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

ax.transAxes

"子图"

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

subfigure.transSubfigure

"图形"

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

fig.transFigure

"figure-inches"

图表以英寸为单位的坐标系;(0, 0) 是图表左下角,(宽度, 高度) 是图表右上角,以英寸为单位。

fig.dpi_scale_trans

"xaxis", "yaxis"

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

ax.get_xaxis_transform(), ax.get_yaxis_transform()

"display"

输出的原生坐标系;(0, 0) 是窗口左下角,(宽度, 高度) 是输出右上角,以“显示单位”为单位。

单位的具体解释取决于后端。例如,对于 Agg,它是像素,对于 svg/pdf,它是点。

None, 或 IdentityTransform()

变换对象对源坐标系和目标坐标系是无感知的,但是上表中提到的对象是专门构造的,用于接收其坐标系中的输入,并将输入转换为显示坐标系。这就是为什么显示坐标系的“变换对象”列为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) 是您的坐标轴或子图的左下角,(0.5, 0.5) 是中心,(1.0, 1.0) 是右上角。您还可以引用范围之外的点,因此 (-0.1, 1.1) 在您的坐标轴的左侧和上方。当在坐标轴中放置文本时,此坐标系非常有用,因为您通常希望文本气泡位于固定位置,例如坐标轴窗格的左上角,并且在您平移或缩放时该位置保持固定。这是一个简单的示例,它创建四个面板并分别标记为 '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,您将看到数据移动,但圆圈将保持固定,因为它不在数据坐标中,并且将始终保持在 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

混合变换#

在混合axesdata坐标的混合坐标空间中绘图非常有用,例如,创建一个水平跨度,它突出显示 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)

其中 *xt* 和 *yt* 是平移偏移量,*scale_trans* 是在变换时间应用偏移量之前缩放 *xt* 和 *yt* 的变换。

注意下面变换中加号运算符的使用。此代码表示:首先应用缩放变换 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)

我们在上面的 Axes 坐标 中介绍了 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 分钟 3.346 秒)

由 Sphinx-Gallery 生成的图库