{
  "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, 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.14.6"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}