What's new in Matplotlib 3.9.0 (May 15, 2024)#

For a list of all of the issues and pull requests since the last revision, see the GitHub statistics for 3.9.0 (May 15, 2024).

Plotting and Annotation improvements#

Axes.inset_axes is no longer experimental#

Axes.inset_axes is considered stable for use.

Legend support for Boxplot#

Boxplots now support a label parameter to create legend entries. Legend labels can be passed as a list of strings to label multiple boxes in a single Axes.boxplot call:

np.random.seed(19680801)
fruit_weights = [
    np.random.normal(130, 10, size=100),
    np.random.normal(125, 20, size=100),
    np.random.normal(120, 30, size=100),
]
labels = ['peaches', 'oranges', 'tomatoes']
colors = ['peachpuff', 'orange', 'tomato']

fig, ax = plt.subplots()
ax.set_ylabel('fruit weight (g)')

bplot = ax.boxplot(fruit_weights,
                   patch_artist=True,  # fill with color
                   label=labels)

# fill with colors
for patch, color in zip(bplot['boxes'], colors):
    patch.set_facecolor(color)

ax.set_xticks([])
ax.legend()

(Source code, 2x.png, png)

Example of creating 3 boxplots and assigning legend labels as a sequence.

Or as a single string to each individual Axes.boxplot:

fig, ax = plt.subplots()

data_A = np.random.random((100, 3))
data_B = np.random.random((100, 3)) + 0.2
pos = np.arange(3)

ax.boxplot(data_A, positions=pos - 0.2, patch_artist=True, label='Box A',
           boxprops={'facecolor': 'steelblue'})
ax.boxplot(data_B, positions=pos + 0.2, patch_artist=True, label='Box B',
           boxprops={'facecolor': 'lightblue'})

ax.legend()

(Source code, 2x.png, png)

Example of creating 2 boxplots and assigning each legend label as a string.

Percent sign in pie labels auto-escaped with usetex=True#

It is common, with Axes.pie, to specify labels that include a percent sign (%), which denotes a comment for LaTeX. When enabling LaTeX with rcParams["text.usetex"] (default: False) or passing textprops={"usetex": True}, this used to cause the percent sign to disappear.

Now, the percent sign is automatically escaped (by adding a preceding backslash) so that it appears regardless of the usetex setting. If you have pre-escaped the percent sign, this will be detected, and remain as is.

hatch parameter for stackplot#

The stackplot hatch parameter now accepts a list of strings describing hatching styles that will be applied sequentially to the layers in the stack:

fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(10,5))

cols = 10
rows = 4
data = (
np.reshape(np.arange(0, cols, 1), (1, -1)) ** 2
+ np.reshape(np.arange(0, rows), (-1, 1))
+ np.random.random((rows, cols))*5
)
x = range(data.shape[1])
ax1.stackplot(x, data, hatch="x")
ax2.stackplot(x, data, hatch=["//","\\","x","o"])

ax1.set_title("hatch='x'")
ax2.set_title("hatch=['//','\\\\','x','o']")

plt.show()

(Source code, 2x.png, png)

Two charts, identified as ax1 and ax2, showing

Add option to plot only one half of violin plot#

Setting the parameter side to 'low' or 'high' allows to only plot one half of the Axes.violinplot.

# Fake data with reproducible random state.
np.random.seed(19680801)
data = np.random.normal(0, 8, size=100)

fig, ax = plt.subplots()

ax.violinplot(data, [0], showmeans=True, showextrema=True)
ax.violinplot(data, [1], showmeans=True, showextrema=True, side='low')
ax.violinplot(data, [2], showmeans=True, showextrema=True, side='high')

ax.set_title('Violin Sides Example')
ax.set_xticks([0, 1, 2], ['Default', 'side="low"', 'side="high"'])
ax.set_yticklabels([])

(Source code, 2x.png, png)

Three copies of a vertical violin plot; first in blue showing the default of both sides, followed by an orange copy that only shows the

axhline and axhspan on polar axes#

... now draw circles and circular arcs (axhline) or annuli and wedges (axhspan).

fig = plt.figure()
ax = fig.add_subplot(projection="polar")
ax.set_rlim(0, 1.2)

ax.axhline(1, c="C0", alpha=.5)
ax.axhspan(.8, .9, fc="C1", alpha=.5)
ax.axhspan(.6, .7, .8, .9, fc="C2", alpha=.5)

(Source code, 2x.png, png)

A sample polar plot, that contains an axhline at radius 1, an axhspan annulus between radius 0.8 and 0.9, and an axhspan wedge between radius 0.6 and 0.7 and 288° and 324°.

Subplot titles can now be automatically aligned#

Subplot axes titles can be misaligned vertically if tick labels or xlabels are placed at the top of one subplot. The new align_titles method on the Figure class will now align the titles vertically.

fig, axs = plt.subplots(1, 2, layout='constrained')

axs[0].plot(np.arange(0, 1e6, 1000))
axs[0].set_title('Title 0')
axs[0].set_xlabel('XLabel 0')

axs[1].plot(np.arange(1, 0, -0.1) * 2000, np.arange(1, 0, -0.1))
axs[1].set_title('Title 1')
axs[1].set_xlabel('XLabel 1')
axs[1].xaxis.tick_top()
axs[1].tick_params(axis='x', rotation=55)

(Source code, 2x.png, png)

A figure with two Axes side-by-side, the second of which with ticks on top. The Axes titles and x-labels appear unaligned with each other due to these ticks.
fig, axs = plt.subplots(1, 2, layout='constrained')

axs[0].plot(np.arange(0, 1e6, 1000))
axs[0].set_title('Title 0')
axs[0].set_xlabel('XLabel 0')

axs[1].plot(np.arange(1, 0, -0.1) * 2000, np.arange(1, 0, -0.1))
axs[1].set_title('Title 1')
axs[1].set_xlabel('XLabel 1')
axs[1].xaxis.tick_top()
axs[1].tick_params(axis='x', rotation=55)

fig.align_labels()
fig.align_titles()

(Source code, 2x.png, png)

A figure with two Axes side-by-side, the second of which with ticks on top. Unlike the previous figure, the Axes titles and x-labels appear aligned.

axisartist can now be used together with standard Formatters#

... instead of being limited to axisartist-specific ones.

Toggle minorticks on Axis#

Minor ticks on an Axis can be displayed or removed using minorticks_on and minorticks_off; e.g., ax.xaxis.minorticks_on(). See also minorticks_on.

StrMethodFormatter now respects axes.unicode_minus#

When formatting negative values, StrMethodFormatter will now use unicode minus signs if rcParams["axes.unicode_minus"] (default: True) is set.

>>> from matplotlib.ticker import StrMethodFormatter
>>> with plt.rc_context({'axes.unicode_minus': False}):
...     formatter = StrMethodFormatter('{x}')
...     print(formatter.format_data(-10))
-10
>>> with plt.rc_context({'axes.unicode_minus': True}):
...     formatter = StrMethodFormatter('{x}')
...     print(formatter.format_data(-10))
−10

Figure, Axes, and Legend Layout#

Subfigures now have controllable zorders#

Previously, setting the zorder of a subfigure had no effect, and those were plotted on top of any figure-level artists (i.e for example on top of fig-level legends). Now, subfigures behave like any other artists, and their zorder can be controlled, with default a zorder of 0.

x = np.linspace(1, 10, 10)
y1, y2 = x, -x
fig = plt.figure(constrained_layout=True)
subfigs = fig.subfigures(nrows=1, ncols=2)
for subfig in subfigs:
    axarr = subfig.subplots(2, 1)
    for ax in axarr.flatten():
        (l1,) = ax.plot(x, y1, label="line1")
        (l2,) = ax.plot(x, y2, label="line2")
subfigs[0].set_zorder(6)
l = fig.legend(handles=[l1, l2], loc="upper center", ncol=2)

(Source code, 2x.png, png)

Example on controlling the zorder of a subfigure

Getters for xmargin, ymargin and zmargin#

Axes.get_xmargin, Axes.get_ymargin and Axes3D.get_zmargin methods have been added to return the margin values set by Axes.set_xmargin, Axes.set_ymargin and Axes3D.set_zmargin, respectively.

Mathtext improvements#

mathtext documentation improvements#

The documentation is updated to take information directly from the parser. This means that (almost) all supported symbols, operators, etc. are shown at Writing mathematical expressions.

mathtext spacing corrections#

As consequence of the updated documentation, the spacing on a number of relational and operator symbols were correctly classified and therefore will be spaced properly.

Widget Improvements#

Check and Radio Button widgets support clearing#

The CheckButtons and RadioButtons widgets now support clearing their state by calling their .clear method. Note that it is not possible to have no selected radio buttons, so the selected option at construction time is selected.

3D plotting improvements#

Setting 3D axis limits now set the limits exactly#

Previously, setting the limits of a 3D axis would always add a small margin to the limits. Limits are now set exactly by default. The newly introduced rcparam axes3d.automargin can be used to revert to the old behavior where margin is automatically added.

fig, axs = plt.subplots(1, 2, subplot_kw={'projection': '3d'})

plt.rcParams['axes3d.automargin'] = True
axs[0].set(xlim=(0, 1), ylim=(0, 1), zlim=(0, 1), title='Old Behavior')

plt.rcParams['axes3d.automargin'] = False  # the default in 3.9.0
axs[1].set(xlim=(0, 1), ylim=(0, 1), zlim=(0, 1), title='New Behavior')

(Source code, 2x.png, png)

Example of the new behavior of 3D axis limits, and how setting the rcParam reverts to the old behavior.

Other improvements#

BackendRegistry#

New BackendRegistry class is the single source of truth for available backends. The singleton instance is matplotlib.backends.backend_registry. It is used internally by Matplotlib, and also IPython (and therefore Jupyter) starting with IPython 8.24.0.

There are three sources of backends: built-in (source code is within the Matplotlib repository), explicit module://some.backend syntax (backend is obtained by loading the module), or via an entry point (self-registering backend in an external package).

To obtain a list of all registered backends use:

>>> from matplotlib.backends import backend_registry
>>> backend_registry.list_all()

Add widths, heights and angles setter to EllipseCollection#

The widths, heights and angles values of the EllipseCollection can now be changed after the collection has been created.

from matplotlib.collections import EllipseCollection

rng = np.random.default_rng(0)

widths = (2, )
heights = (3, )
angles = (45, )
offsets = rng.random((10, 2)) * 10

fig, ax = plt.subplots()

ec = EllipseCollection(
    widths=widths,
    heights=heights,
    angles=angles,
    offsets=offsets,
    units='x',
    offset_transform=ax.transData,
    )

ax.add_collection(ec)
ax.set_xlim(-2, 12)
ax.set_ylim(-2, 12)

new_widths = rng.random((10, 2)) * 2
new_heights = rng.random((10, 2)) * 3
new_angles = rng.random((10, 2)) * 180

ec.set(widths=new_widths, heights=new_heights, angles=new_angles)

(Source code, 2x.png, png)

image.interpolation_stage rcParam#

This new rcParam controls whether image interpolation occurs in "data" space or in "rgba" space.

Arrow patch position is now modifiable#

A setter method has been added that allows updating the position of the patches.Arrow object without requiring a full re-draw.

from matplotlib import animation
from matplotlib.patches import Arrow

fig, ax = plt.subplots()
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)

a = Arrow(2, 0, 0, 10)
ax.add_patch(a)


# code for modifying the arrow
def update(i):
    a.set_data(x=.5, dx=i, dy=6, width=2)


ani = animation.FuncAnimation(fig, update, frames=15, interval=90, blit=False)

plt.show()

(Source code, 2x.png, png)

Example of changing the position of the arrow with the new ``set_data`` method.

NonUniformImage now has mouseover support#

When mousing over a NonUniformImage, the data values are now displayed.