# 變換教程
> 原文:[Transformations Tutorial](http://matplotlib.org/users/transforms_tutorial.html)
> 譯者:[飛龍](https://github.com/)
> 協議:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
像任何圖形包一樣,matplotlib 建立在變換框架之上,以便在坐標系,用戶數據坐標系,軸域坐標系,圖形坐標系和顯示坐標系之間輕易變換。 在 95 %的繪圖中,你不需要考慮這一點,因為它發生在背后,但隨著你接近自定義圖形生成的極限,它有助于理解這些對象,以便可以重用 matplotlib 提供給你的現有變換,或者創建自己的變換(見`matplotlib.transforms`)。 下表總結了現有的坐標系,你應該在該坐標系中使用的變換對象,以及該系統的描述。 在『變換對象』一列中,`ax`是`Axes`實例,`fig`是一個圖形實例。
| 坐標系 | 變換對象 | 描述 |
| --- | --- | --- |
| 數據 | `ax.transData` | 用戶數據坐標系,由`xlim`和`ylim`控制 |
| 軸域 | `ax.transAxes` | 軸域坐標系;`(0,0)`是軸域左下角,`(1,1)`是軸域右上角 |
| 圖形 | `fig.transFigure` | 圖形坐標系;`(0,0)`是圖形左下角,`(1,1)`是圖形右上角 |
| 顯示 | `None` | 這是顯示器的像素坐標系; `(0,0)`是顯示器的左下角,`(width, height)`是顯示器的右上角,以像素為單位。 或者,可以使用恒等變換(`matplotlib.transforms.IdentityTransform()`)來代替`None`。 |
上表中的所有變換對象都接受以其坐標系為單位的輸入,并將輸入變換到顯示坐標系。 這就是為什么顯示坐標系沒有『變換對象』的原因 - 它已經以顯示坐標為單位了。 變換也知道如何反轉自身,從顯示返回自身的坐標系。 這在處理來自用戶界面的事件(通常發生在顯示空間中),并且你想知道數據坐標系中鼠標點擊或按鍵按下的位置時特別有用。
## 數據坐標
讓我們從最常用的坐標,數據坐標系開始。 每當向軸域添加數據時,matplotlib 會更新數據對象,`set_xlim()`和`set_ylim()`方法最常用于更新。 例如,在下圖中,數據的范圍在`x`軸上為從 0 到 10,在`y`軸上為從 -1 到 1。
```py
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(0, 10, 0.005)
y = np.exp(-x/2.) * np.sin(2*np.pi*x)
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x, y)
ax.set_xlim(0, 10)
ax.set_ylim(-1, 1)
plt.show()
```

你可以使用`ax.transData`實例將數據變換為顯示坐標系,無論是單個點或是一系列點,如下所示:
```py
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()`方法創建一個變換,從顯示坐標變換為數據坐標:
```py
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 會話中,由顯示標記的點可能并不相同,因為文檔圖形大小默認值是不同的。

> 注意
> 如果在 GUI 后端中運行上述示例中的源代碼,你還可能發現數據和顯示標注的兩個箭頭不會指向完全相同的點。 這是因為顯示點是在顯示圖形之前計算的,并且 GUI 后端可以在創建圖形時稍微調整圖形大小。 如果你自己調整圖的大小,效果更明顯。 這是你很少想要處理顯示空間的一個很好的原因,但是你可以連接到`'on_draw'`事件來更新圖上的圖坐標;請參閱[事件處理和選擇](http://matplotlib.org/users/event_handling.html#event-handling-tutorial)。
當你更改軸的`x`或`y`的范圍時,將更新數據范圍,以便變換生成新的顯示點。 注意,當我們只是改變`ylim`,只有`y`顯示坐標改變,當我們改變`xlim`也同理。 我們在談論 [Bbox](http://matplotlib.org/api/transformations.html#matplotlib.transforms.Bbox) 時會深入。
```py
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'`,你經常在期刊上看到它們。

你也可以在軸坐標系中創建線條或者補丁,但是以我的經驗,這比使用`ax.transAxes`放置文本更不實用。 盡管如此,這里是一個愚蠢的例子,它在數據空間中繪制了一些隨機點,并且覆蓋在一個半透明的圓上面,這個圓以軸域的中心為圓心,半徑為軸域的四分之一。 - 如果你的軸域不保留高寬比(見`set_aspect ()`),它將看起來像一個橢圓。 使用平移/縮放工具移動,或手動更改數據的`xlim`和`ylim`,你將看到數據移動,但圓將保持固定,因為它不在數據坐標中,并且將始終保持在軸域的中心 。

## 混合變換
在數據與軸域坐標混合的混合坐標空間中繪制是非常實用的,例如創建一個水平跨度,突出`y`數據的一些區域但橫跨`x`軸,而無論數據限制,平移或縮放級別等。實際上這些混合線條和跨度非常有用,我們已經內置了一些函數來使它們容易繪制(參見`axhline()`,`axvline()`,`axhspan()`,`axvspan()`),但是為了教學目的,我們使用混合變換實現這里的水平跨度。 這個技巧只適用于可分離的變換,就像你在正常的笛卡爾坐標系中看到的,但不能為不可分離的變換,如`PolarTransform`(極坐標變換)。
```py
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.transforms as transforms
fig = plt.figure()
ax = fig.add_subplot(111)
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 = patches.Rectangle((1,0), width=1, height=1,
transform=trans, color='yellow',
alpha=0.5)
ax.add_patch(rect)
plt.show()
```

> 注
> 混合變換非常有用,其中`x`為數據坐標而`y`為軸域坐標,我們擁有輔助方法來返回內部使用的版本 mpl ,用于繪制`ticks`,`ticklabels`以及其他。方法是`matplotlib.axes.Axes.get_xaxis_transform()`和`matplotlib.axes.Axes.get_yaxis_transform()`。 因此,在上面的示例中,`blended_transform_factory()`的調用可以替換為`get_xaxis_transform`:
> ```py
> trans = ax.get_xaxis_transform()
> ```
## 使用偏移變換來創建陰影效果
變換的一個用法,是創建偏離另一變換的新變換,例如,放置一個對象,相對于另一對象有一些偏移。 通常,你希望物理尺寸上有一些移位,例如以點或英寸,而不是數據坐標為單位,以便移位效果在不同的縮放級別和 dpi 設置下保持不變。
偏移的一個用途是創建一個陰影效果,其中你繪制一個與第一個相同的對象,剛好在它的右邊和下面,調整`zorder`來確保首先繪制陰影,然后繪制對象,陰影在它之上。 變換模塊具有輔助變換`ScaledTranslation`。 它可以這樣來實例化:
```py
trans = ScaledTranslation(xt, yt, scale_trans)
```
其中`xt`和`yt`是變換的偏移,`scale_trans`是變換,在應用偏移之前的變換期間縮放`xt`和`yt`。 一個典型的用例是,將圖形的`fig.dpi_scale_trans`變換用于`scale_trans`參數,來在實現最終的偏移之前,首先將以點為單位的`xt`和`yt`縮放到顯示空間。
DPI 和英寸偏移是常見的用例,我們擁有一個特殊的輔助函數,來在`matplotlib.transforms.offset_copy()`中創建它,它返回一個帶有附加偏移的新變換。 但在下面的示例中,我們將自己創建偏移變換。 注意使用加法運算符:
```py
offset = transforms.ScaledTranslation(dx, dy,
fig.dpi_scale_trans)
shadow_transform = ax.transData + offset
```
這里顯示了,可以使用加法運算符將變換鏈起來。 該代碼表示:首先應用數據變換`ax.transData`,然后由`dx`和`dy`點翻譯數據。 在[排版](https://en.wikipedia.org/wiki/Point_%28typography%29)中,一個點是 1/72 英寸,通過以點為單位指定偏移,你的圖形看起來是一樣的,無論所保存的 dpi 分辨率。
```py
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.transforms as transforms
fig = plt.figure()
ax = fig.add_subplot(111)
# 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()
```

## 變換流水線
我們在本教程中一直使用的`ax.transData`變換是三種不同變換的組合,它們構成從數據到顯示坐標的變換流水線。 Michael Droettboom 實現了變換框架,提供了一個干凈的 API,它隔離了在極坐標和對數坐標圖中發生的非線性投影和尺度,以及在平移和縮放時發生的線性仿射變換。 這里有一個效率問題,因為你可以平移和放大你的軸域,它會影響仿射變換,但你可能不需要計算潛在的昂貴的非線性比例或簡單的導航事件的投影。 也可以將仿射變換矩陣相乘在一起,然后在一步之中將它們應用于坐標。 這對所有可能的變換不都是有效的。
這里是在`ax.transData`實例在基本可分離的`Axes`類中的定義方式。
```py
self.transData = self.transScale + (self.transLimits + self.transAxes)
```
我們已經在`Axes`坐標中引入了上面的`transAxes`實例,它將軸或子圖邊界框的`(0,0)`,`(1,1)`角映射到顯示空間,所以讓我們看看這兩個部分。
`self.transLimits`是從數據到軸域坐標的變換; 也就是說,它將你的視圖`xlim`和`ylim`映射到軸域單位空間(然后`transAxes`將該單位空間用于顯示空間)。 我們可以在這里看到這一點:
```py
In [80]: ax = subplot(111)
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])
```
而且我們可以使用相同的反轉變換,從軸域單位坐標變換回數據坐標。
```py
In [90]: inv.transform((0.25, 0.25))
Out[90]: array([ 2.5, -0.5])
```
最后一個是`self.transScale`屬性,它負責數據的可選非線性縮放,例如對數軸域。 當`Axes`初始化時,這只是設置為恒等變換,因為基本的 matplotlib 軸域具有線性縮放,但是當你調用對數縮放函數如`semilogx()`或使用`set_xscale`顯式設置為對數時,`ax.transScale`屬性為處理非線性投影而設置。 縮放變換是相應`xaxis`和`yaxis` 的`Axis`實例的屬性。 例如,當調用`ax.set_xscale('log')`時,`xaxis`會將其縮放更新為`matplotlib.scale.LogScale`實例。
對于不可分離的軸域,`PolarAxes`,還有一個要考慮的部分,投影變換。 `matplotlib.projections.polar.PolarAxes`的`transData`類似于典型的可分離 matplotlib 軸域,帶有一個額外的部分,`transProjection`:
```py
self.transData = self.transScale + self.transProjection + \
(self.transProjectionAffine + self.transAxes)
```
`transProjection`將來自空間的投影,例如,地圖數據的緯度和經度,或極坐標數據的半徑和極角,處理為可分離的笛卡爾坐標系。 在`matplotlib.projections`包中有幾個投影示例,深入了解的最好方法是打開這些包的源代碼,看看如何自己制作它,因為 matplotlib 支持可擴展的軸域和投影。 Michael Droettboom 提供了一個創建一個錘投影軸域的很好的教程示例;請參閱 api 示例代碼:[`custom_projection_example.py`](http://matplotlib.org/examples/api/custom_projection_example.html#api-custom-projection-example)。