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