{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n# Secondary Axis\n\nSometimes we want a secondary axis on a plot, for instance to convert\nradians to degrees on the same plot. We can do this by making a child\naxes with only one axis visible via `.axes.Axes.secondary_xaxis` and\n`.axes.Axes.secondary_yaxis`. This secondary axis can have a different scale\nthan the main axis by providing both a forward and an inverse conversion\nfunction in a tuple to the *functions* keyword argument:\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import datetime\n\nimport matplotlib.pyplot as plt\nimport numpy as np\n\nimport matplotlib.dates as mdates\n\nfig, ax = plt.subplots(layout='constrained')\nx = np.arange(0, 360, 1)\ny = np.sin(2 * x * np.pi / 180)\nax.plot(x, y)\nax.set_xlabel('angle [degrees]')\nax.set_ylabel('signal')\nax.set_title('Sine wave')\n\n\ndef deg2rad(x):\n return x * np.pi / 180\n\n\ndef rad2deg(x):\n return x * 180 / np.pi\n\n\nsecax = ax.secondary_xaxis('top', functions=(deg2rad, rad2deg))\nsecax.set_xlabel('angle [rad]')\nplt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "By default, the secondary axis is drawn in the Axes coordinate space.\nWe can also provide a custom transform to place it in a different\ncoordinate space. Here we put the axis at Y = 0 in data coordinates.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, ax = plt.subplots(layout='constrained')\nx = np.arange(0, 10)\nnp.random.seed(19680801)\ny = np.random.randn(len(x))\nax.plot(x, y)\nax.set_xlabel('X')\nax.set_ylabel('Y')\nax.set_title('Random data')\n\n# Pass ax.transData as a transform to place the axis relative to our data\nsecax = ax.secondary_xaxis(0, transform=ax.transData)\nsecax.set_xlabel('Axis at Y = 0')\nplt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here is the case of converting from wavenumber to wavelength in a\nlog-log scale.\n\n

Note

In this case, the xscale of the parent is logarithmic, so the child is\n made logarithmic as well.

\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, ax = plt.subplots(layout='constrained')\nx = np.arange(0.02, 1, 0.02)\nnp.random.seed(19680801)\ny = np.random.randn(len(x)) ** 2\nax.loglog(x, y)\nax.set_xlabel('f [Hz]')\nax.set_ylabel('PSD')\nax.set_title('Random spectrum')\n\n\ndef one_over(x):\n \"\"\"Vectorized 1/x, treating x==0 manually\"\"\"\n x = np.array(x, float)\n near_zero = np.isclose(x, 0)\n x[near_zero] = np.inf\n x[~near_zero] = 1 / x[~near_zero]\n return x\n\n\n# the function \"1/x\" is its own inverse\ninverse = one_over\n\n\nsecax = ax.secondary_xaxis('top', functions=(one_over, inverse))\nsecax.set_xlabel('period [s]')\nplt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Sometime we want to relate the axes in a transform that is ad-hoc from the data, and\nis derived empirically. Or, one axis could be a complicated nonlinear function of the\nother. In these cases we can set the forward and inverse transform functions to be\nlinear interpolations from the one set of independent variables to the other.\n\n

Note

In order to properly handle the data margins, the mapping functions\n (``forward`` and ``inverse`` in this example) need to be defined beyond the\n nominal plot limits. This condition can be enforced by extending the\n interpolation beyond the plotted values, both to the left and the right,\n see ``x1n`` and ``x2n`` below.

\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, ax = plt.subplots(layout='constrained')\nx1_vals = np.arange(2, 11, 0.4)\n# second independent variable is a nonlinear function of the other.\nx2_vals = x1_vals ** 2\nydata = 50.0 + 20 * np.random.randn(len(x1_vals))\nax.plot(x1_vals, ydata, label='Plotted data')\nax.plot(x1_vals, x2_vals, label=r'$x_2 = x_1^2$')\nax.set_xlabel(r'$x_1$')\nax.legend()\n\n# the forward and inverse functions must be defined on the complete visible axis range\nx1n = np.linspace(0, 20, 201)\nx2n = x1n**2\n\n\ndef forward(x):\n return np.interp(x, x1n, x2n)\n\n\ndef inverse(x):\n return np.interp(x, x2n, x1n)\n\n# use axvline to prove that the derived secondary axis is correctly plotted\nax.axvline(np.sqrt(40), color=\"grey\", ls=\"--\")\nax.axvline(10, color=\"grey\", ls=\"--\")\nsecax = ax.secondary_xaxis('top', functions=(forward, inverse))\nsecax.set_xticks([10, 20, 40, 60, 80, 100])\nsecax.set_xlabel(r'$x_2$')\n\nplt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A final example translates np.datetime64 to yearday on the x axis and\nfrom Celsius to Fahrenheit on the y axis. Note the addition of a\nthird y axis, and that it can be placed using a float for the\nlocation argument\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "dates = [datetime.datetime(2018, 1, 1) + datetime.timedelta(hours=k * 6)\n for k in range(240)]\ntemperature = np.random.randn(len(dates)) * 4 + 6.7\nfig, ax = plt.subplots(layout='constrained')\n\nax.plot(dates, temperature)\nax.set_ylabel(r'$T\\ [^oC]$')\nax.xaxis.set_tick_params(rotation=70)\n\n\ndef date2yday(x):\n \"\"\"Convert matplotlib datenum to days since 2018-01-01.\"\"\"\n y = x - mdates.date2num(datetime.datetime(2018, 1, 1))\n return y\n\n\ndef yday2date(x):\n \"\"\"Return a matplotlib datenum for *x* days after 2018-01-01.\"\"\"\n y = x + mdates.date2num(datetime.datetime(2018, 1, 1))\n return y\n\n\nsecax_x = ax.secondary_xaxis('top', functions=(date2yday, yday2date))\nsecax_x.set_xlabel('yday [2018]')\n\n\ndef celsius_to_fahrenheit(x):\n return x * 1.8 + 32\n\n\ndef fahrenheit_to_celsius(x):\n return (x - 32) / 1.8\n\n\nsecax_y = ax.secondary_yaxis(\n 'right', functions=(celsius_to_fahrenheit, fahrenheit_to_celsius))\nsecax_y.set_ylabel(r'$T\\ [^oF]$')\n\n\ndef celsius_to_anomaly(x):\n return (x - np.mean(temperature))\n\n\ndef anomaly_to_celsius(x):\n return (x + np.mean(temperature))\n\n\n# use of a float for the position:\nsecax_y2 = ax.secondary_yaxis(\n 1.2, functions=(celsius_to_anomaly, anomaly_to_celsius))\nsecax_y2.set_ylabel(r'$T - \\overline{T}\\ [^oC]$')\n\n\nplt.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.axes.Axes.secondary_xaxis`\n - `matplotlib.axes.Axes.secondary_yaxis`\n\n.. tags::\n\n component: axis\n plot-type: line\n level: beginner\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 }