Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d1b13d9

Browse files
committed
Merge pull request matplotlib#1664 from dopplershift/skew
Support for skewed transforms.
2 parents 37dc468 + f3ba859 commit d1b13d9

File tree

14 files changed

+6213
-28
lines changed

14 files changed

+6213
-28
lines changed

doc/api/api_changes.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ original location:
144144
tuple-like if separate axis padding is required.
145145
The original behavior is preserved.
146146

147+
* Added support for skewed transforms to `matplotlib.transforms.Affine2D`,
148+
which can be created using the `skew` and `skew_deg` methods.
149+
147150

148151
.. _changes_in_1_3:
149152

doc/users/whats_new.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,17 @@ be a tuple if separate horizontal/vertical padding is needed.
121121
This is supposed to be very helpful when you have a labelled legend next to
122122
every subplot and you need to make some space for legend's labels.
123123

124+
Support for skewed transformations
125+
``````````````````````````````````
126+
The :class:`~matplotlib.transforms.Affine2D` gained additional methods
127+
`skew` and `skew_deg` to create skewed transformations. Additionally,
128+
matplotlib internals were cleaned up to support using such transforms in
129+
:class:`~matplotlib.Axes`. This transform is important for some plot types,
130+
specifically the Skew-T used in meteorology.
131+
132+
.. plot:: mpl_examples/api/skewt.py
133+
134+
124135
Date handling
125136
-------------
126137

examples/api/skewt.py

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# This serves as an intensive exercise of matplotlib's transforms
2+
# and custom projection API. This example produces a so-called
3+
# SkewT-logP diagram, which is a common plot in meteorology for
4+
# displaying vertical profiles of temperature. As far as matplotlib is
5+
# concerned, the complexity comes from having X and Y axes that are
6+
# not orthogonal. This is handled by including a skew component to the
7+
# basic Axes transforms. Additional complexity comes in handling the
8+
# fact that the upper and lower X-axes have different data ranges, which
9+
# necessitates a bunch of custom classes for ticks,spines, and the axis
10+
# to handle this.
11+
12+
from matplotlib.axes import Axes
13+
import matplotlib.transforms as transforms
14+
import matplotlib.axis as maxis
15+
import matplotlib.spines as mspines
16+
import matplotlib.path as mpath
17+
from matplotlib.projections import register_projection
18+
19+
# The sole purpose of this class is to look at the upper, lower, or total
20+
# interval as appropriate and see what parts of the tick to draw, if any.
21+
class SkewXTick(maxis.XTick):
22+
def draw(self, renderer):
23+
if not self.get_visible(): return
24+
renderer.open_group(self.__name__)
25+
26+
lower_interval = self.axes.xaxis.lower_interval
27+
upper_interval = self.axes.xaxis.upper_interval
28+
29+
if self.gridOn and transforms.interval_contains(
30+
self.axes.xaxis.get_view_interval(), self.get_loc()):
31+
self.gridline.draw(renderer)
32+
33+
if transforms.interval_contains(lower_interval, self.get_loc()):
34+
if self.tick1On:
35+
self.tick1line.draw(renderer)
36+
if self.label1On:
37+
self.label1.draw(renderer)
38+
39+
if transforms.interval_contains(upper_interval, self.get_loc()):
40+
if self.tick2On:
41+
self.tick2line.draw(renderer)
42+
if self.label2On:
43+
self.label2.draw(renderer)
44+
45+
renderer.close_group(self.__name__)
46+
47+
48+
# This class exists to provide two separate sets of intervals to the tick,
49+
# as well as create instances of the custom tick
50+
class SkewXAxis(maxis.XAxis):
51+
def __init__(self, *args, **kwargs):
52+
maxis.XAxis.__init__(self, *args, **kwargs)
53+
self.upper_interval = 0.0, 1.0
54+
55+
def _get_tick(self, major):
56+
return SkewXTick(self.axes, 0, '', major=major)
57+
58+
@property
59+
def lower_interval(self):
60+
return self.axes.viewLim.intervalx
61+
62+
def get_view_interval(self):
63+
return self.upper_interval[0], self.axes.viewLim.intervalx[1]
64+
65+
66+
# This class exists to calculate the separate data range of the
67+
# upper X-axis and draw the spine there. It also provides this range
68+
# to the X-axis artist for ticking and gridlines
69+
class SkewSpine(mspines.Spine):
70+
def _adjust_location(self):
71+
trans = self.axes.transDataToAxes.inverted()
72+
if self.spine_type == 'top':
73+
yloc = 1.0
74+
else:
75+
yloc = 0.0
76+
left = trans.transform_point((0.0, yloc))[0]
77+
right = trans.transform_point((1.0, yloc))[0]
78+
79+
pts = self._path.vertices
80+
pts[0, 0] = left
81+
pts[1, 0] = right
82+
self.axis.upper_interval = (left, right)
83+
84+
85+
# This class handles registration of the skew-xaxes as a projection as well
86+
# as setting up the appropriate transformations. It also overrides standard
87+
# spines and axes instances as appropriate.
88+
class SkewXAxes(Axes):
89+
# The projection must specify a name. This will be used be the
90+
# user to select the projection, i.e. ``subplot(111,
91+
# projection='skewx')``.
92+
name = 'skewx'
93+
94+
def _init_axis(self):
95+
#Taken from Axes and modified to use our modified X-axis
96+
self.xaxis = SkewXAxis(self)
97+
self.spines['top'].register_axis(self.xaxis)
98+
self.spines['bottom'].register_axis(self.xaxis)
99+
self.yaxis = maxis.YAxis(self)
100+
self.spines['left'].register_axis(self.yaxis)
101+
self.spines['right'].register_axis(self.yaxis)
102+
103+
def _gen_axes_spines(self):
104+
spines = {'top':SkewSpine.linear_spine(self, 'top'),
105+
'bottom':mspines.Spine.linear_spine(self, 'bottom'),
106+
'left':mspines.Spine.linear_spine(self, 'left'),
107+
'right':mspines.Spine.linear_spine(self, 'right')}
108+
return spines
109+
110+
def _set_lim_and_transforms(self):
111+
"""
112+
This is called once when the plot is created to set up all the
113+
transforms for the data, text and grids.
114+
"""
115+
rot = 30
116+
117+
#Get the standard transform setup from the Axes base class
118+
Axes._set_lim_and_transforms(self)
119+
120+
# Need to put the skew in the middle, after the scale and limits,
121+
# but before the transAxes. This way, the skew is done in Axes
122+
# coordinates thus performing the transform around the proper origin
123+
# We keep the pre-transAxes transform around for other users, like the
124+
# spines for finding bounds
125+
self.transDataToAxes = self.transScale + (self.transLimits +
126+
transforms.Affine2D().skew_deg(rot, 0))
127+
128+
# Create the full transform from Data to Pixels
129+
self.transData = self.transDataToAxes + self.transAxes
130+
131+
# Blended transforms like this need to have the skewing applied using
132+
# both axes, in axes coords like before.
133+
self._xaxis_transform = (transforms.blended_transform_factory(
134+
self.transScale + self.transLimits,
135+
transforms.IdentityTransform()) +
136+
transforms.Affine2D().skew_deg(rot, 0)) + self.transAxes
137+
138+
# Now register the projection with matplotlib so the user can select
139+
# it.
140+
register_projection(SkewXAxes)
141+
142+
if __name__ == '__main__':
143+
# Now make a simple example using the custom projection.
144+
from matplotlib.ticker import ScalarFormatter, MultipleLocator
145+
from matplotlib.collections import LineCollection
146+
import matplotlib.pyplot as plt
147+
from StringIO import StringIO
148+
import numpy as np
149+
150+
#Some examples data
151+
data_txt = '''
152+
978.0 345 7.8 0.8 61 4.16 325 14 282.7 294.6 283.4
153+
971.0 404 7.2 0.2 61 4.01 327 17 282.7 294.2 283.4
154+
946.7 610 5.2 -1.8 61 3.56 335 26 282.8 293.0 283.4
155+
944.0 634 5.0 -2.0 61 3.51 336 27 282.8 292.9 283.4
156+
925.0 798 3.4 -2.6 65 3.43 340 32 282.8 292.7 283.4
157+
911.8 914 2.4 -2.7 69 3.46 345 37 282.9 292.9 283.5
158+
906.0 966 2.0 -2.7 71 3.47 348 39 283.0 293.0 283.6
159+
877.9 1219 0.4 -3.2 77 3.46 0 48 283.9 293.9 284.5
160+
850.0 1478 -1.3 -3.7 84 3.44 0 47 284.8 294.8 285.4
161+
841.0 1563 -1.9 -3.8 87 3.45 358 45 285.0 295.0 285.6
162+
823.0 1736 1.4 -0.7 86 4.44 353 42 290.3 303.3 291.0
163+
813.6 1829 4.5 1.2 80 5.17 350 40 294.5 309.8 295.4
164+
809.0 1875 6.0 2.2 77 5.57 347 39 296.6 313.2 297.6
165+
798.0 1988 7.4 -0.6 57 4.61 340 35 299.2 313.3 300.1
166+
791.0 2061 7.6 -1.4 53 4.39 335 33 300.2 313.6 301.0
167+
783.9 2134 7.0 -1.7 54 4.32 330 31 300.4 313.6 301.2
168+
755.1 2438 4.8 -3.1 57 4.06 300 24 301.2 313.7 301.9
169+
727.3 2743 2.5 -4.4 60 3.81 285 29 301.9 313.8 302.6
170+
700.5 3048 0.2 -5.8 64 3.57 275 31 302.7 313.8 303.3
171+
700.0 3054 0.2 -5.8 64 3.56 280 31 302.7 313.8 303.3
172+
698.0 3077 0.0 -6.0 64 3.52 280 31 302.7 313.7 303.4
173+
687.0 3204 -0.1 -7.1 59 3.28 281 31 304.0 314.3 304.6
174+
648.9 3658 -3.2 -10.9 55 2.59 285 30 305.5 313.8 305.9
175+
631.0 3881 -4.7 -12.7 54 2.29 289 33 306.2 313.6 306.6
176+
600.7 4267 -6.4 -16.7 44 1.73 295 39 308.6 314.3 308.9
177+
592.0 4381 -6.9 -17.9 41 1.59 297 41 309.3 314.6 309.6
178+
577.6 4572 -8.1 -19.6 39 1.41 300 44 310.1 314.9 310.3
179+
555.3 4877 -10.0 -22.3 36 1.16 295 39 311.3 315.3 311.5
180+
536.0 5151 -11.7 -24.7 33 0.97 304 39 312.4 315.8 312.6
181+
533.8 5182 -11.9 -25.0 33 0.95 305 39 312.5 315.8 312.7
182+
500.0 5680 -15.9 -29.9 29 0.64 290 44 313.6 315.9 313.7
183+
472.3 6096 -19.7 -33.4 28 0.49 285 46 314.1 315.8 314.1
184+
453.0 6401 -22.4 -36.0 28 0.39 300 50 314.4 315.8 314.4
185+
400.0 7310 -30.7 -43.7 27 0.20 285 44 315.0 315.8 315.0
186+
399.7 7315 -30.8 -43.8 27 0.20 285 44 315.0 315.8 315.0
187+
387.0 7543 -33.1 -46.1 26 0.16 281 47 314.9 315.5 314.9
188+
382.7 7620 -33.8 -46.8 26 0.15 280 48 315.0 315.6 315.0
189+
342.0 8398 -40.5 -53.5 23 0.08 293 52 316.1 316.4 316.1
190+
320.4 8839 -43.7 -56.7 22 0.06 300 54 317.6 317.8 317.6
191+
318.0 8890 -44.1 -57.1 22 0.05 301 55 317.8 318.0 317.8
192+
310.0 9060 -44.7 -58.7 19 0.04 304 61 319.2 319.4 319.2
193+
306.1 9144 -43.9 -57.9 20 0.05 305 63 321.5 321.7 321.5
194+
305.0 9169 -43.7 -57.7 20 0.05 303 63 322.1 322.4 322.1
195+
300.0 9280 -43.5 -57.5 20 0.05 295 64 323.9 324.2 323.9
196+
292.0 9462 -43.7 -58.7 17 0.05 293 67 326.2 326.4 326.2
197+
276.0 9838 -47.1 -62.1 16 0.03 290 74 326.6 326.7 326.6
198+
264.0 10132 -47.5 -62.5 16 0.03 288 79 330.1 330.3 330.1
199+
251.0 10464 -49.7 -64.7 16 0.03 285 85 331.7 331.8 331.7
200+
250.0 10490 -49.7 -64.7 16 0.03 285 85 332.1 332.2 332.1
201+
247.0 10569 -48.7 -63.7 16 0.03 283 88 334.7 334.8 334.7
202+
244.0 10649 -48.9 -63.9 16 0.03 280 91 335.6 335.7 335.6
203+
243.3 10668 -48.9 -63.9 16 0.03 280 91 335.8 335.9 335.8
204+
220.0 11327 -50.3 -65.3 15 0.03 280 85 343.5 343.6 343.5
205+
212.0 11569 -50.5 -65.5 15 0.03 280 83 346.8 346.9 346.8
206+
210.0 11631 -49.7 -64.7 16 0.03 280 83 349.0 349.1 349.0
207+
200.0 11950 -49.9 -64.9 15 0.03 280 80 353.6 353.7 353.6
208+
194.0 12149 -49.9 -64.9 15 0.03 279 78 356.7 356.8 356.7
209+
183.0 12529 -51.3 -66.3 15 0.03 278 75 360.4 360.5 360.4
210+
164.0 13233 -55.3 -68.3 18 0.02 277 69 365.2 365.3 365.2
211+
152.0 13716 -56.5 -69.5 18 0.02 275 65 371.1 371.2 371.1
212+
150.0 13800 -57.1 -70.1 18 0.02 275 64 371.5 371.6 371.5
213+
136.0 14414 -60.5 -72.5 19 0.02 268 54 376.0 376.1 376.0
214+
132.0 14600 -60.1 -72.1 19 0.02 265 51 380.0 380.1 380.0
215+
131.4 14630 -60.2 -72.2 19 0.02 265 51 380.3 380.4 380.3
216+
128.0 14792 -60.9 -72.9 19 0.02 266 50 381.9 382.0 381.9
217+
125.0 14939 -60.1 -72.1 19 0.02 268 49 385.9 386.0 385.9
218+
119.0 15240 -62.2 -73.8 20 0.01 270 48 387.4 387.5 387.4
219+
112.0 15616 -64.9 -75.9 21 0.01 265 53 389.3 389.3 389.3
220+
108.0 15838 -64.1 -75.1 21 0.01 265 58 394.8 394.9 394.8
221+
107.8 15850 -64.1 -75.1 21 0.01 265 58 395.0 395.1 395.0
222+
105.0 16010 -64.7 -75.7 21 0.01 272 50 396.9 396.9 396.9
223+
103.0 16128 -62.9 -73.9 21 0.02 277 45 402.5 402.6 402.5
224+
100.0 16310 -62.5 -73.5 21 0.02 285 36 406.7 406.8 406.7'''
225+
226+
# Parse the data
227+
sound_data = StringIO(data_txt)
228+
p,h,T,Td = np.loadtxt(sound_data, usecols=range(0,4), unpack=True)
229+
230+
# Create a new figure. The dimensions here give a good aspect ratio
231+
fig = plt.figure(figsize=(6.5875, 6.2125))
232+
ax = fig.add_subplot(111, projection='skewx')
233+
234+
plt.grid(True)
235+
236+
# Plot the data using normal plotting functions, in this case using
237+
# log scaling in Y, as dicatated by the typical meteorological plot
238+
ax.semilogy(T, p, 'r')
239+
ax.semilogy(Td, p, 'g')
240+
241+
# An example of a slanted line at constant X
242+
l = ax.axvline(0, color='b')
243+
244+
# Disables the log-formatting that comes with semilogy
245+
ax.yaxis.set_major_formatter(ScalarFormatter())
246+
ax.set_yticks(np.linspace(100,1000,10))
247+
ax.set_ylim(1050,100)
248+
249+
ax.xaxis.set_major_locator(MultipleLocator(10))
250+
ax.set_xlim(-50,50)
251+
252+
plt.show()

lib/matplotlib/axes/_axes.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -720,8 +720,7 @@ def axhline(self, y=0, xmin=0, xmax=1, **kwargs):
720720
yy = self.convert_yunits(y)
721721
scaley = (yy < ymin) or (yy > ymax)
722722

723-
trans = mtransforms.blended_transform_factory(
724-
self.transAxes, self.transData)
723+
trans = self.get_yaxis_transform(which='grid')
725724
l = mlines.Line2D([xmin, xmax], [y, y], transform=trans, **kwargs)
726725
self.add_line(l)
727726
self.autoscale_view(scalex=False, scaley=scaley)
@@ -787,8 +786,7 @@ def axvline(self, x=0, ymin=0, ymax=1, **kwargs):
787786
xx = self.convert_xunits(x)
788787
scalex = (xx < xmin) or (xx > xmax)
789788

790-
trans = mtransforms.blended_transform_factory(
791-
self.transData, self.transAxes)
789+
trans = self.get_xaxis_transform(which='grid')
792790
l = mlines.Line2D([x, x], [ymin, ymax], transform=trans, **kwargs)
793791
self.add_line(l)
794792
self.autoscale_view(scalex=scalex, scaley=False)
@@ -833,8 +831,7 @@ def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs):
833831
.. plot:: mpl_examples/pylab_examples/axhspan_demo.py
834832
835833
"""
836-
trans = mtransforms.blended_transform_factory(
837-
self.transAxes, self.transData)
834+
trans = self.get_yaxis_transform(which='grid')
838835

839836
# process the unit information
840837
self._process_unit_info([xmin, xmax], [ymin, ymax], kwargs=kwargs)
@@ -889,8 +886,7 @@ def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs):
889886
:meth:`axhspan`
890887
for example plot and source code
891888
"""
892-
trans = mtransforms.blended_transform_factory(
893-
self.transData, self.transAxes)
889+
trans = self.get_xaxis_transform(which='grid')
894890

895891
# process the unit information
896892
self._process_unit_info([xmin, xmax], [ymin, ymax], kwargs=kwargs)
@@ -3949,8 +3945,7 @@ def coarse_bin(x, y, coarse):
39493945
values.append(val)
39503946

39513947
values = np.array(values)
3952-
trans = mtransforms.blended_transform_factory(
3953-
self.transData, self.transAxes)
3948+
trans = self.get_xaxis_transform(which='grid')
39543949

39553950
hbar = mcoll.PolyCollection(verts, transform=trans, edgecolors='face')
39563951

@@ -3979,8 +3974,7 @@ def coarse_bin(x, y, coarse):
39793974

39803975
values = np.array(values)
39813976

3982-
trans = mtransforms.blended_transform_factory(
3983-
self.transAxes, self.transData)
3977+
trans = self.get_yaxis_transform(which='grid')
39843978

39853979
vbar = mcoll.PolyCollection(verts, transform=trans, edgecolors='face')
39863980
vbar.set_array(values)

lib/matplotlib/axes/_base.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1977,6 +1977,14 @@ def draw(self, renderer=None, inframe=False):
19771977
artists.extend(self.lines)
19781978
artists.extend(self.texts)
19791979
artists.extend(self.artists)
1980+
1981+
# the frame draws the edges around the axes patch -- we
1982+
# decouple these so the patch can be in the background and the
1983+
# frame in the foreground. Do this before drawing the axis
1984+
# objects so that the spine has the opportunity to update them.
1985+
if self.axison and self._frameon:
1986+
artists.extend(six.itervalues(self.spines))
1987+
19801988
if self.axison and not inframe:
19811989
if self._axisbelow:
19821990
self.xaxis.set_zorder(0.5)
@@ -1993,12 +2001,6 @@ def draw(self, renderer=None, inframe=False):
19932001
if self.legend_ is not None:
19942002
artists.append(self.legend_)
19952003

1996-
# the frame draws the edges around the axes patch -- we
1997-
# decouple these so the patch can be in the background and the
1998-
# frame in the foreground.
1999-
if self.axison and self._frameon:
2000-
artists.extend(six.itervalues(self.spines))
2001-
20022004
if self.figure.canvas.is_saving():
20032005
dsu = [(a.zorder, a) for a in artists]
20042006
else:

lib/matplotlib/spines.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -376,14 +376,7 @@ def set_position(self, position):
376376
self._position = position
377377
self._calc_offset_transform()
378378

379-
t = self.get_spine_transform()
380-
if self.spine_type in ['left', 'right']:
381-
t2 = mtransforms.blended_transform_factory(t,
382-
self.axes.transData)
383-
elif self.spine_type in ['bottom', 'top']:
384-
t2 = mtransforms.blended_transform_factory(self.axes.transData,
385-
t)
386-
self.set_transform(t2)
379+
self.set_transform(self.get_spine_transform())
387380

388381
if self.axis is not None:
389382
self.axis.cla()
Binary file not shown.

0 commit comments

Comments
 (0)