Note
Go to the endto download the full example code.
SkewT-logP diagram: using transforms and custom projections#
This serves as an intensive exercise of Matplotlib's transforms and customprojection API. This example produces a so-called SkewT-logP diagram, which isa common plot in meteorology for displaying vertical profiles of temperature.As far as Matplotlib is concerned, the complexity comes from having X and Yaxes that are not orthogonal. This is handled by including a skew component tothe basic Axes transforms. Additional complexity comes in handling the factthat the upper and lower X-axes have different data ranges, which necessitatesa bunch of custom classes for ticks, spines, and axis to handle this.
fromcontextlibimportExitStackfrommatplotlib.axesimportAxesimportmatplotlib.axisasmaxisfrommatplotlib.projectionsimportregister_projectionimportmatplotlib.spinesasmspinesimportmatplotlib.transformsastransforms# The sole purpose of this class is to look at the upper, lower, or total# interval as appropriate and see what parts of the tick to draw, if any.classSkewXTick(maxis.XTick):defdraw(self,renderer):# When adding the callbacks with `stack.callback`, we fetch the current# visibility state of the artist with `get_visible`; the ExitStack will# restore these states (`set_visible`) at the end of the block (after# the draw).withExitStack()asstack:forartistin[self.gridline,self.tick1line,self.tick2line,self.label1,self.label2]:stack.callback(artist.set_visible,artist.get_visible())needs_lower=transforms.interval_contains(self.axes.lower_xlim,self.get_loc())needs_upper=transforms.interval_contains(self.axes.upper_xlim,self.get_loc())self.tick1line.set_visible(self.tick1line.get_visible()andneeds_lower)self.label1.set_visible(self.label1.get_visible()andneeds_lower)self.tick2line.set_visible(self.tick2line.get_visible()andneeds_upper)self.label2.set_visible(self.label2.get_visible()andneeds_upper)super().draw(renderer)defget_view_interval(self):returnself.axes.xaxis.get_view_interval()# This class exists to provide two separate sets of intervals to the tick,# as well as create instances of the custom tickclassSkewXAxis(maxis.XAxis):def_get_tick(self,major):returnSkewXTick(self.axes,None,major=major)defget_view_interval(self):returnself.axes.upper_xlim[0],self.axes.lower_xlim[1]# This class exists to calculate the separate data range of the# upper X-axis and draw the spine there. It also provides this range# to the X-axis artist for ticking and gridlinesclassSkewSpine(mspines.Spine):def_adjust_location(self):pts=self._path.verticesifself.spine_type=='top':pts[:,0]=self.axes.upper_xlimelse:pts[:,0]=self.axes.lower_xlim# This class handles registration of the skew-xaxes as a projection as well# as setting up the appropriate transformations. It also overrides standard# spines and axes instances as appropriate.classSkewXAxes(Axes):# The projection must specify a name. This will be used be the# user to select the projection, i.e. ``subplot(projection='skewx')``.name='skewx'def_init_axis(self):# Taken from Axes and modified to use our modified X-axisself.xaxis=SkewXAxis(self)self.spines.top.register_axis(self.xaxis)self.spines.bottom.register_axis(self.xaxis)self.yaxis=maxis.YAxis(self)self.spines.left.register_axis(self.yaxis)self.spines.right.register_axis(self.yaxis)def_gen_axes_spines(self):spines={'top':SkewSpine.linear_spine(self,'top'),'bottom':mspines.Spine.linear_spine(self,'bottom'),'left':mspines.Spine.linear_spine(self,'left'),'right':mspines.Spine.linear_spine(self,'right')}returnspinesdef_set_lim_and_transforms(self):""" This is called once when the plot is created to set up all the transforms for the data, text and grids. """rot=30# Get the standard transform setup from the Axes base classsuper()._set_lim_and_transforms()# Need to put the skew in the middle, after the scale and limits,# but before the transAxes. This way, the skew is done in Axes# coordinates thus performing the transform around the proper origin# We keep the pre-transAxes transform around for other users, like the# spines for finding boundsself.transDataToAxes=(self.transScale+self.transLimits+transforms.Affine2D().skew_deg(rot,0))# Create the full transform from Data to Pixelsself.transData=self.transDataToAxes+self.transAxes# Blended transforms like this need to have the skewing applied using# both axes, in axes coords like before.self._xaxis_transform=(transforms.blended_transform_factory(self.transScale+self.transLimits,transforms.IdentityTransform())+transforms.Affine2D().skew_deg(rot,0)+self.transAxes)@propertydeflower_xlim(self):returnself.axes.viewLim.intervalx@propertydefupper_xlim(self):pts=[[0.,1.],[1.,1.]]returnself.transDataToAxes.inverted().transform(pts)[:,0]# Now register the projection with matplotlib so the user can select it.register_projection(SkewXAxes)if__name__=='__main__':# Now make a simple example using the custom projection.fromioimportStringIOimportmatplotlib.pyplotaspltimportnumpyasnpfrommatplotlib.tickerimportMultipleLocator,NullFormatter,ScalarFormatter# Some example data.data_txt=''' 978.0 345 7.8 0.8 971.0 404 7.2 0.2 946.7 610 5.2 -1.8 944.0 634 5.0 -2.0 925.0 798 3.4 -2.6 911.8 914 2.4 -2.7 906.0 966 2.0 -2.7 877.9 1219 0.4 -3.2 850.0 1478 -1.3 -3.7 841.0 1563 -1.9 -3.8 823.0 1736 1.4 -0.7 813.6 1829 4.5 1.2 809.0 1875 6.0 2.2 798.0 1988 7.4 -0.6 791.0 2061 7.6 -1.4 783.9 2134 7.0 -1.7 755.1 2438 4.8 -3.1 727.3 2743 2.5 -4.4 700.5 3048 0.2 -5.8 700.0 3054 0.2 -5.8 698.0 3077 0.0 -6.0 687.0 3204 -0.1 -7.1 648.9 3658 -3.2 -10.9 631.0 3881 -4.7 -12.7 600.7 4267 -6.4 -16.7 592.0 4381 -6.9 -17.9 577.6 4572 -8.1 -19.6 555.3 4877 -10.0 -22.3 536.0 5151 -11.7 -24.7 533.8 5182 -11.9 -25.0 500.0 5680 -15.9 -29.9 472.3 6096 -19.7 -33.4 453.0 6401 -22.4 -36.0 400.0 7310 -30.7 -43.7 399.7 7315 -30.8 -43.8 387.0 7543 -33.1 -46.1 382.7 7620 -33.8 -46.8 342.0 8398 -40.5 -53.5 320.4 8839 -43.7 -56.7 318.0 8890 -44.1 -57.1 310.0 9060 -44.7 -58.7 306.1 9144 -43.9 -57.9 305.0 9169 -43.7 -57.7 300.0 9280 -43.5 -57.5 292.0 9462 -43.7 -58.7 276.0 9838 -47.1 -62.1 264.0 10132 -47.5 -62.5 251.0 10464 -49.7 -64.7 250.0 10490 -49.7 -64.7 247.0 10569 -48.7 -63.7 244.0 10649 -48.9 -63.9 243.3 10668 -48.9 -63.9 220.0 11327 -50.3 -65.3 212.0 11569 -50.5 -65.5 210.0 11631 -49.7 -64.7 200.0 11950 -49.9 -64.9 194.0 12149 -49.9 -64.9 183.0 12529 -51.3 -66.3 164.0 13233 -55.3 -68.3 152.0 13716 -56.5 -69.5 150.0 13800 -57.1 -70.1 136.0 14414 -60.5 -72.5 132.0 14600 -60.1 -72.1 131.4 14630 -60.2 -72.2 128.0 14792 -60.9 -72.9 125.0 14939 -60.1 -72.1 119.0 15240 -62.2 -73.8 112.0 15616 -64.9 -75.9 108.0 15838 -64.1 -75.1 107.8 15850 -64.1 -75.1 105.0 16010 -64.7 -75.7 103.0 16128 -62.9 -73.9 100.0 16310 -62.5 -73.5 '''# Parse the datasound_data=StringIO(data_txt)p,h,T,Td=np.loadtxt(sound_data,unpack=True)# Create a new figure. The dimensions here give a good aspect ratiofig=plt.figure(figsize=(6.5875,6.2125))ax=fig.add_subplot(projection='skewx')plt.grid(True)# Plot the data using normal plotting functions, in this case using# log scaling in Y, as dictated by the typical meteorological plotax.semilogy(T,p,color='C3')ax.semilogy(Td,p,color='C2')# An example of a slanted line at constant Xl=ax.axvline(0,color='C0')# Disables the log-formatting that comes with semilogyax.yaxis.set_major_formatter(ScalarFormatter())ax.yaxis.set_minor_formatter(NullFormatter())ax.set_yticks(np.linspace(100,1000,10))ax.set_ylim(1050,100)ax.xaxis.set_major_locator(MultipleLocator(10))ax.set_xlim(-50,50)plt.show()

References
The use of the following functions, methods, classes and modules is shownin this example: