{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n.. redirect-from:: /tutorials/intermediate/autoscale\n\n\n# Axis autoscaling\n\n## Basic concept\n\nAutoscaling ensures that data is visible within the Axes by automatically adjusting\nthe axis limits. When you plot data, Matplotlib's autoscaling mechanism updates the\naxis limits accordingly.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\nimport numpy as np\n\n\nx = np.linspace(-6, 6, 201)\ny = np.sinc(x)\n\nfig, ax = plt.subplots()\nax.plot(x, y)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n## Margins\nTo ensure that the data is not at the very edge of the plot, Matplotlib adds a\nmargin around the data limits. Note that the *x* data range in the above plot is\n[-6, 6], but the x-axis limits are slightly wider due to the margin.\n\nThe default margin is 5%, defined via\n\n- :rc:`axes.xmargin`\n- :rc:`axes.ymargin`\n- :rc:`axes.zmargin`\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "print(ax.get_xmargin(), ax.get_ymargin())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The margin size can be overridden to make them smaller or larger using\n`~matplotlib.axes.Axes.margins`:\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, ax = plt.subplots()\nax.plot(x, y)\nax.margins(0.2, 0.2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In general, margins can be in the range (-0.5, \u221e), where negative margins set\nthe axes limits to a subrange of the data range, i.e. they clip data.\nUsing a single number for margins affects both axes, a single margin can be\ncustomized using keyword arguments ``x`` or ``y``, but positional and keyword\ninterface cannot be combined.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, ax = plt.subplots()\nax.plot(x, y)\nax.margins(y=-0.2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n## Sticky edges\nThere are plot elements (`.Artist`\\s) that are usually used without margins.\nFor example, false-color images (e.g. created with `.Axes.imshow`) are not\nconsidered in the margins calculation.\n\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "xx, yy = np.meshgrid(x, x)\nzz = np.sinc(np.sqrt((xx - 1)**2 + (yy - 1)**2))\n\nfig, ax = plt.subplots(ncols=2, figsize=(12, 8))\nax[0].imshow(zz)\nax[0].set_title(\"default margins\")\nax[1].imshow(zz)\nax[1].margins(0.2)\nax[1].set_title(\"margins(0.2)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This override of margins is determined by \"sticky edges\", a\nproperty of `.Artist` class that can suppress adding margins to axis\nlimits. The effect of sticky edges can be disabled on an Axes by changing\n`~matplotlib.axes.Axes.use_sticky_edges`.\nArtists have a property `.Artist.sticky_edges`, and the values of\nsticky edges can be changed by writing to ``Artist.sticky_edges.x`` or\n``Artist.sticky_edges.y``.\n\nThe following example shows how overriding works and when it is needed.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, ax = plt.subplots(ncols=3, figsize=(16, 10))\nax[0].imshow(zz)\nax[0].margins(0.2)\nax[0].set_title(\"default use_sticky_edges\\nmargins(0.2)\")\nax[1].imshow(zz)\nax[1].margins(0.2)\nax[1].use_sticky_edges = False\nax[1].set_title(\"use_sticky_edges=False\\nmargins(0.2)\")\nax[2].imshow(zz)\nax[2].margins(-0.2)\nax[2].set_title(\"default use_sticky_edges\\nmargins(-0.2)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that setting ``use_sticky_edges`` to *False* renders the image\nwith requested margins.\n\nWhile sticky edges don't increase the axis limits through extra margins,\nnegative margins are still taken into account. This can be seen in\nthe reduced limits of the third image.\n\n## Controlling autoscale\n\nBy default, the limits are\nrecalculated every time you add a new curve to the plot:\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, ax = plt.subplots(ncols=2, figsize=(12, 8))\nax[0].plot(x, y)\nax[0].set_title(\"Single curve\")\nax[1].plot(x, y)\nax[1].plot(x * 2.0, y)\nax[1].set_title(\"Two curves\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you don't want automatic updates of the axis limits, either deactivate\nautoscaling with `~.axes.Axes.autoscale` or set the limits\nmanually with `~.axes.Axes.set_xlim` / `~.axes.Axes.set_ylim`.\n\nLet's say that we want to see only a part of the data in\ngreater detail. Setting the ``xlim`` persists even if we add more curves to\nthe data. Calling `.Axes.autoscale` will re-enable the autoscaling and\nrecalculate the limits to fit all the data.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, ax = plt.subplots(ncols=2, figsize=(12, 8))\nax[0].plot(x, y)\nax[0].set_xlim(left=-1, right=1)\nax[0].plot(x + np.pi * 0.5, y)\nax[0].set_title(\"set_xlim(left=-1, right=1)\\n\")\nax[1].plot(x, y)\nax[1].set_xlim(left=-1, right=1)\nax[1].plot(x + np.pi * 0.5, y)\nax[1].autoscale()\nax[1].set_title(\"set_xlim(left=-1, right=1)\\nautoscale()\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can check that the first plot has autoscale disabled and that the second\nplot has it enabled again by using `.Axes.get_autoscale_on()`:\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "print(ax[0].get_autoscale_on()) # False means disabled\nprint(ax[1].get_autoscale_on()) # True means enabled -> recalculated" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Arguments of the autoscale function give us precise control over the process\nof autoscaling. A combination of arguments ``enable``, and ``axis`` sets the\nautoscaling feature for the selected axis (or both). The argument ``tight``\nsets the margin of the selected axis to zero. To preserve settings of either\n``enable`` or ``tight`` you can set the opposite one to *None*, that way\nit should not be modified. However, setting ``enable`` to *None* and tight\nto *True* affects both axes regardless of the ``axis`` argument.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, ax = plt.subplots()\nax.plot(x, y)\nax.margins(0.2, 0.2)\nax.autoscale(enable=None, axis=\"x\", tight=True)\n\nprint(ax.margins())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Technical background\n\nThis section explains the internal pipeline that runs when autoscaling\ncomputes axis limits from data. Understanding the mechanics helps when\nyou encounter surprising behaviour or need to update limits manually.\n\n### Data limits and view limits\n\nMatplotlib maintains two sets of limits:\n\n- **Data limits** (`.Axes.dataLim`): the tight bounding box of the raw data.\n- **View limits** (`.Axes.viewLim`): the displayed axis limits. By default,\n computed from the data limits through the autoscaling mechanism outlined\n below, but they can be set independently. View limits can alternatively\n be set explicitly through `~.axes.Axes.set_xlim` / `~.axes.Axes.set_ylim`,\n which also disables autoscaling so that the set limits remain fixed.\n\nThe following shows the input and output of this process \u2014 ``dataLim`` holds\nthe raw data bounds, ``viewLim`` the final displayed axis limits.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, ax = plt.subplots()\nx = np.linspace(-6, 6, 201)\ny = np.sin(x)\nax.plot(x, y)\nprint(f\"dataLim x: ({ax.dataLim.x0:.3f}, {ax.dataLim.x1:.3f})\")\nprint(f\"dataLim y: ({ax.dataLim.y0:.3f}, {ax.dataLim.y1:.3f})\")\nprint(f\"viewLim x: ({ax.viewLim.x0:.3f}, {ax.viewLim.x1:.3f})\")\nprint(f\"viewLim y: ({ax.viewLim.y0:.3f}, {ax.viewLim.y1:.3f})\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The x data range is [-6, 6] and the default 5% margin adds roughly 0.6 on\neach side, widening the view to about [-6.6, 6.6]. The same applies to the\ny axis.\n\n### Update logic\n\nData and view limit updates are handled as separate stages.\n\n**Data limits**: When an artist is added to an Axes through one of the\nplotting methods, the data limits are updated through `.Axes.update_datalim`\nto include the new data. This only ever increases the data limits. It is\nalso possible to update `.Axes.dataLim` manually, but this is not common.\nRemoval of an artist or change of its data does not trigger any update of\nthe data limits, so they can become out of date. In such cases, it is\nnecessary to explicitly recompute the data limit through `.Axes.relim`.\n\n**View limits**: When autoscaling is enabled, the view limits are\nautomatically computed from the data limit. This update is lazy and only\ntriggered when the view limits are queried or drawn, so that they don't have\nto be recomputed for every added artist. This is transparent to the user.\nExplicit changes of the data limits through `.Axes.dataLim` or `.Axes.relim`\ndo not trigger an update of the view limits, so they can also become out of\ndate. In such cases, it is necessary to explicitly recompute the view limits\nthrough `.Axes.autoscale_view`.\n\n### View limit calculation\n\nGiven the data limits, the view limits are derived through these steps:\n\n- scale domain clamping\n- margin expansion\n- sticky edge clamping\n- optional limit rounding\n\n### Scale domain clamping\n\nBefore margins are applied, the data limits are clipped to the valid domain\nof the axis scale. This matters for scales like log (positive values only)\nand logit (values strictly between 0 and 1): if a bound lies outside the\ndomain, it is replaced with a value at the domain boundary.\n\nFor this purpose, `.Axes.dataLim` tracks not just the ordinary min/max of\nthe data but also ``minpos`` \u2014 the smallest strictly positive value seen.\nA log-scale lower bound of zero or less is replaced with ``minpos`` rather\nthan the actual minimum, because only positive values can be displayed.\n\nFor a logit scale, the upper bound is approximated as ``1 - minpos``, since\nthe largest data value below 1 is not tracked separately. This means the\nautoscaled upper limit may include slightly more headroom than necessary\nwhen the data maximum is well below 1.\n\n### Margin expansion\n\nThe first step is to apply the margins, i.e. widen the view limits beyond the\ndata limits so that data is not at the very edge of the plot. Margins are\nspecified as a fraction of the data span in screen coordinates so that\nthe data-free border area always has the same visual size, irrespective of\ndata ranges or axis scales. The margin is applied symmetrically to both sides\nof the data limits, so the view is expanded equally in both directions.\n\nThis is illustrated in the following example, where the data limits and\naxis scales are different, but the visual margin is the same in both cases.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4))\nfig.suptitle(\"Margins are visually constant, \"\n \"even with different data limits and axis scales\")\n\nax1.plot([0, 10], [0, 1])\nax1.margins(0.2)\n\nx = np.linspace(1, 20)\nax2.semilogy(x, np.exp(x))\nax2.margins(0.2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Sticky edges clamping\n\nSticky edges are axis values at which margin expansion is clamped. After\ncomputing the margin-expanded limits, if an expanded limit would extend\nbeyond a sticky edge, it is pulled back to that edge instead.\n\nArtists register sticky edges to prevent blank margins at natural data\nboundaries. `~.Axes.imshow`, for example, registers sticky edges at its\nfour pixel boundaries, which is why images fill the Axes by default without\nany surrounding margin (as shown in the `autoscale_sticky_edges`\nsection above). Sticky edges only suppress *outward expansion past the data\nboundary* \u2014 they never shrink limits into the data, and negative margins\nare not affected. Setting ``Axes.use_sticky_edges = False`` disables sticky\nedge clamping on that Axes.\n\n### Limit rounding\n\nAs a final step, the view limits can optionally be expanded outward to the\nnearest \"nice\" tick position, so that the axis edges coincide with tick\nmarks. This is disabled by default, but can be turned on with the\n\"round_numbers\" mode of :rc:`axes.autolimit_mode`:\n\n- ``'data'`` (default): keep the limits at the margin-expanded values.\n- ``'round_numbers'``: expand the limits outward to the nearest \"nice\" tick\n position, so the axis edges coincide with tick marks.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))\nax1.plot([0.3, 4.7], [0.3, 4.7])\nax1.set_title(\"autolimit_mode='data' (default)\")\nwith plt.rc_context({'axes.autolimit_mode': 'round_numbers'}):\n ax2.plot([0.3, 4.7], [0.3, 4.7])\n ax2.set_title(\"autolimit_mode='round_numbers'\")\n ax2.autoscale_view() # force autoscale while round_numbers is active" ] } ], "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.12.15" } }, "nbformat": 4, "nbformat_minor": 0 }