freidrichen
freidrichen

Reputation: 2576

How to get constant distance between legend and axes even when the figure is resized?

When placing the legend outside of the axes using bbox_to_anchor as in this answer, the space between the axes and the legend changes when the figure is resized. For static exported plots this is fine; you can simply tweak the numbers until you get it right. But for interactive plots that you might want to resize, this is a problem. As can be seen in this example:

import numpy as np
from matplotlib import pyplot as plt

x = np.arange(5)
y = np.random.randn(5)

fig, ax = plt.subplots(tight_layout=True)
ax.plot(x, y, label='data1')
ax.plot(x, y-1, label='data2')
legend = ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.05), ncol=2)
plt.show()

Result:

legend placement ruined by resize

How can I make sure that the legend keeps the same distance from the axes even when the figure is resized?

Upvotes: 2

Views: 2170

Answers (2)

ImportanceOfBeingErnest
ImportanceOfBeingErnest

Reputation: 339660

The distance of a legend from the bounding box edge is set by the borderaxespad argument. The borderaxespad is in units of multiples of the fontsize - making it automatically independent of the axes size. So in this case,

import matplotlib.pyplot as plt
import numpy as np
x = np.arange(5)
y = np.random.randn(5)

fig, ax = plt.subplots(constrained_layout=True)
ax.plot(x, y, label='data1')
ax.plot(x, y-1, label='data2')
legend = ax.legend(loc="upper center", bbox_to_anchor=(0.5,0), borderaxespad=2)

plt.show()

enter image description here enter image description here


A similar question about showing a title below the axes at a constant distance is being asked in Place title at the bottom of the figure of an axes?

Upvotes: 3

freidrichen
freidrichen

Reputation: 2576

You can use the resize events of the canvas to update the values in bbox_to_anchor with each update. To calculate the new values you can use the inverse of the axes transformation (Bbox.inverse_transformed(ax.transAxes)), which translates from screen coordinates in pixels to the axes coordinates which are normally used in bbox_to_anchor.

Here is an example with support for putting the legend on all four sides of the axes:

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.transforms import Bbox


class FixedOutsideLegend:
    """A legend placed at a fixed offset (in pixels) from the axes."""

    def __init__(self, ax, location, pixel_offset, **kwargs):
        self._pixel_offset = pixel_offset

        self.location = location
        if location == 'right':
            self._loc = 'center left'
        elif location == 'left':
            self._loc = 'center right'
        elif location == 'upper':
            self._loc = 'lower center'
        elif location == 'lower':
            self._loc = 'upper center'
        else:
            raise ValueError('Unknown location: {}'.format(location))

        self.legend = ax.legend(
            loc=self._loc, bbox_to_anchor=self._get_bbox_to_anchor(), **kwargs)
        ax.figure.canvas.mpl_connect('resize_event', self.on_resize)

    def on_resize(self, event):
        self.legend.set_bbox_to_anchor(self._get_bbox_to_anchor())

    def _get_bbox_to_anchor(self):
        """
        Find the lengths in axes units that correspond to the specified
        pixel_offset.
        """
        screen_bbox = Bbox.from_bounds(
            0, 0, self._pixel_offset, self._pixel_offset)
        try:
            ax_bbox = screen_bbox.inverse_transformed(ax.transAxes)
        except np.linagl.LinAlgError:
            ax_width = 0
            ax_height = 0
        else:
            ax_width = ax_bbox.width
            ax_height = ax_bbox.height

        if self.location == 'right':
            return (1 + ax_width, 0.5)
        elif self.location == 'left':
            return (-ax_width, 0.5)
        elif self.location == 'upper':
            return (0.5, 1 + ax_height)
        elif self.location == 'lower':
            return (0.5, -ax_height)


x = np.arange(5)
y = np.random.randn(5)

fig, ax = plt.subplots(tight_layout=True)
ax.plot(x, y, label='data1')
ax.plot(x, y-1, label='data2')
legend = FixedOutsideLegend(ax, 'lower', 20, ncol=2)
plt.show()

Result:

legend spacing still good after resize

Upvotes: 1

Related Questions