变换教程#

像任何图形包一样,Matplotlib 构建在变换框架之上,以便轻松地在坐标系之间移动,用户空间的数据坐标系,坐标轴坐标系,图形坐标系和显示坐标系。在 95% 的绘图中,您无需考虑这一点,因为它在后台发生,但是当您突破自定义图形生成的限制时,了解这些对象会有所帮助,这样您就可以重用 Matplotlib 提供给您的现有变换,或创建您自己的变换(请参阅matplotlib.transforms)。下表总结了一些有用的坐标系,每个系统的描述,以及从每个坐标系到显示坐标系的变换对象。在“变换对象”列中,axAxes 实例,figFigure 实例,而 subfigureSubFigure 实例。

坐标系

描述

从系统到显示的变换对象

“数据”

坐标轴中数据的坐标系。

ax.transData

“坐标轴”

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

ax.transAxes

“子图形”

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

subfigure.transSubfigure

“图形”

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

fig.transFigure

“图形英寸”

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

fig.dpi_scale_trans

“xaxis”,“yaxis”

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

ax.get_xaxis_transform(), ax.get_yaxis_transform()

“显示”

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

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

None, 或 IdentityTransform()

Transform 对象对源和目标坐标系很陌生,但是上表中引用的对象被构建为在其坐标系中获取输入,并将输入转换为显示坐标系。这就是为什么显示坐标系的“变换对象”列为 None,因为它已经在显示坐标中。命名和目标约定有助于跟踪可用的“标准”坐标系和变换。

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

请注意,如果图形的 dpi 或大小发生变化,则在显示坐标中指定艺术家(Artist)的位置可能会更改其相对位置。这可能会在打印或更改屏幕分辨率时引起混淆,因为对象可能会更改位置和大小。因此,放置在坐标轴或图形中的艺术家最常见的做法是将其变换设置为 IdentityTransform();当使用 add_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()
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 后端运行上面示例中的源代码,您可能还会发现 datadisplay 注释的两个箭头没有指向完全相同的点。这是因为显示点是在图形显示之前计算的,并且 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

您也可以在坐标系中绘制线条或补丁,但在我的经验中,这不如使用 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)  # 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()

在物理坐标中绘图#

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

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

另一个用途是将具有设定物理尺寸的补丁放置在轴上的数据点周围。在这里,我们将两个转换相加。第一个设置椭圆应该有多大的缩放比例,第二个设置其位置。然后,将椭圆放置在原点,然后我们使用辅助转换 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 中平移和缩放,这会影响仿射变换,但您可能不需要在简单的导航事件中计算可能开销很大的非线性缩放或投影。也可以将仿射变换矩阵相乘,然后一步应用于坐标。并非所有可能的变换都是如此。

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

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

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

由 Sphinx-Gallery 生成的图库