Skip to content

[Bug]: QtAgg - "Configure subplots" button immediately moves subplots #28553

Open

Description

@rcomer

Bug summary

I was playing with @timhoffm's new example at #28546 and found that, with QtAgg, when I hit the "configure subplots" button the image subplots immediately move downwards.

Code for reproduction

import matplotlib.pyplot as plt
import numpy as np

from matplotlib import colors

np.random.seed(19680801)

datasets = [
    (i+1)/10 * np.random.rand(10, 20)
    for i in range(4)
]

fig, axs = plt.subplots(2, 2)
fig.suptitle('Multiple images')

# create a single norm to be shared across all images
norm = colors.Normalize(vmin=np.max(datasets), vmax=np.min(datasets))

images = []
for ax, data in zip(axs.flat, datasets):
    images.append(ax.imshow(data, norm=norm))

fig.colorbar(images[0], ax=axs, orientation='horizontal', fraction=.1)

plt.show()

Actual outcome

Initially I get the expected plot
image

but when I hit the "Configure subplots" button, without changing any subplot settings I have

image

Expected outcome

Subplots don't move until I tell them!

Additional information

I have reproduced this back to v3.6.3 with QtAgg (I haven't tested further back). I cannot reproduce with TkAgg.

Operating system

Ubuntu

Matplotlib Version

main, 3.8.4, 3.6.3

Matplotlib Backend

QtAgg

Python version

3.9.19, 3.12.3, 3.11.6

Jupyter version

N/A

Installation

conda

Activity

  1. rcomer commented on Jul 12, 2024

    @rcomer
    MemberAuthor

    I wanted to check how this would behave without the colorbar but got #28555.

  2. rcomer commented on Jul 12, 2024

    @rcomer
    MemberAuthor

    If I leave out the norm, the problem is the same. If I leave out the norm and the colorbar there is no problem.

  3. anntzer commented on Jul 10, 2025

    @anntzer
    Contributor

    One can "fix" the issue with something like

    diff --git i/lib/matplotlib/backends/backend_qt.py w/lib/matplotlib/backends/backend_qt.py
    index 9089e982ce..d3cbc349ea 100644
    --- i/lib/matplotlib/backends/backend_qt.py
    +++ w/lib/matplotlib/backends/backend_qt.py
    @@ -904,12 +904,11 @@ class SubplotToolQt(QtWidgets.QDialog):
             self._figure = targetfig
             self._defaults = {}
             self._export_values_dialog = None
    -        self.update_from_current_subplotpars()
     
         def update_from_current_subplotpars(self):
             self._defaults = {spinbox: getattr(self._figure.subplotpars, name)
                               for name, spinbox in self._spinboxes.items()}
    -        self._reset()  # Set spinbox current values without triggering signals.
    +        self._reset(subplots_adjust=False)  # Set spinbox current values without triggering signals.
     
         def _export_values(self):
             # Explicitly round to 3 decimals (which is also the spinbox precision)
    @@ -950,13 +949,14 @@ class SubplotToolQt(QtWidgets.QDialog):
                 spinbox.blockSignals(False)
             self._figure.canvas.draw_idle()
     
    -    def _reset(self):
    +    def _reset(self, *, subplots_adjust=True):
             for spinbox, value in self._defaults.items():
                 spinbox.setRange(0, 1)
                 spinbox.blockSignals(True)
                 spinbox.setValue(value)
                 spinbox.blockSignals(False)
    -        self._on_value_changed()
    +        if subplots_adjust:
    +            self._on_value_changed()
     
     
     class ToolbarQt(ToolContainerBase, QtWidgets.QToolBar):

    but that is really papering over a more serious issue (present for all backends), which is that the slider values don't actually match what's necessary to position the axes where desired. Indeed, if one slightly changes any of the slider (or, on qt, spinbox) values and then bring them back to the original value, then the axes again get messed up as in the original report here (whereas we would want such an action to not move the axes at all).
    So I think the proper fix is really to ensure that changing a subplot parameter and then bringing it back to the same value (whether via a slider on non-qt backends, or via a spinbox on qt) restores the original axes layout.

  4. anntzer commented on Jul 28, 2025

    @anntzer
    Contributor

    I looked a bit more into this (sorry for the slightly rambling writeup below). What happens is that when passing multiple axes as the ax kwarg of colorbar() (axes from which space will be stolen), then the colorbar will be created using make_axes, not make_axes_gridspec, and the original axes get repositioned in absolute (figure fraction) coordinates, overriding the subplot-based positioning. As a result, when configure_subplots tries to reposition them using subplotparams, they end up in the wrong position because they were never correctly associated with a nested SubplotSpec (which is what happens in the single axes case).

    I suspect it should be possible to still use make_axes_gridspec in that case, but this requires carefully working out what is the common SubplotSpec that is the union of the SubplotSpecs for each axes from which space is stolen, and then constructing the correct nested subgridspec from that from that...

  5. jklymak commented on Jul 28, 2025

    @jklymak
    Member

    I suspect it should be possible to still use make_axes_gridspec in that case...

    I think one could do that. I question adding that complexity to the code in service of making the subplots adjustable post-facto. If one is using the sliders, one can just slide "bottom" until it looks good again.

    If someone wanted to add code complexity, I'd have a different slider tool for constrained layout that adjusts the CL parameters instead of forcing the figure to drop CL. EDIT: we should actually disable that popup if the figure is on CL, because it doesn't do anything.

  6. anntzer commented on Jul 28, 2025

    @anntzer
    Contributor
    diff --git i/lib/matplotlib/backend_bases.py w/lib/matplotlib/backend_bases.py
    index 626852f2aa..07de6fb9a1 100644
    --- i/lib/matplotlib/backend_bases.py
    +++ w/lib/matplotlib/backend_bases.py
    @@ -3233,6 +3233,9 @@ class NavigationToolbar2:
             self.canvas.draw_idle()
     
         def configure_subplots(self, *args):
    +        if isinstance(self.canvas.figure.get_layout_engine(),
    +                      ConstrainedLayoutEngine):
    +            return
             if hasattr(self, "subplot_tool"):
                 self.subplot_tool.figure.canvas.manager.show()
                 return self.subplot_tool
    diff --git i/lib/matplotlib/backends/backend_qt.py w/lib/matplotlib/backends/backend_qt.py
    index 401ce0b0b7..02f46c6167 100644
    --- i/lib/matplotlib/backends/backend_qt.py
    +++ w/lib/matplotlib/backends/backend_qt.py
    @@ -12,6 +12,7 @@ from matplotlib.backend_bases import (
         CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent,
         _allow_interrupt)
     import matplotlib.backends.qt_editor.figureoptions as figureoptions
    +from matplotlib.layout_engine import ConstrainedLayoutEngine
     from . import qt_compat
     from .qt_compat import (
         QtCore, QtGui, QtWidgets, __version__, QT_API, _to_int, _isdeleted)
    @@ -818,6 +819,9 @@ class NavigationToolbar2QT(NavigationToolbar2, QtWidgets.QToolBar):
             self.canvas.drawRectangle(None)
     
         def configure_subplots(self):
    +        if isinstance(self.canvas.figure.get_layout_engine(),
    +                      ConstrainedLayoutEngine):
    +            return
             if self._subplot_dialog is None:
                 self._subplot_dialog = SubplotToolQt(
                     self.canvas.figure, self.canvas.parent())

    is perhaps a bit brutal but perhaps a good start?

  7. timhoffm commented on Jul 29, 2025

    @timhoffm
    Member

    An error message would be more user friendly rather than silently doing nothing.

  8. anntzer commented on Jul 29, 2025

    @anntzer
    Contributor

    Yes, I was thinking about that... probably easiest is to set a message in the toolbar; otherwise one needs to pop a small figure with the relevant figtext() (or even rely on gui-specific methods to raise real error message windows)...

  9. jklymak commented on Jul 29, 2025

    @jklymak
    Member

    Or just grey the button out so it's obvious it won't work? Maybe hard to keep checking if it should be active or not

  10. anntzer commented on Jul 29, 2025

    @anntzer
    Contributor

    The problem is that one may call set_layout_engine() and change that state, but the toolbar won't be notified about that.

  11. timhoffm commented on Jul 29, 2025

    @timhoffm
    Member

    or even rely on gui-specific methods to raise real error message windows

    At least for the Qt backend change that's straight forward. One could also add a popup_error() function to backends. The default would then only write to stderr, but backends could override this and use the GUI-specific messages.

  12. jklymak commented on Jul 29, 2025

    @jklymak
    Member

    The problem is that one may call set_layout_engine() and change that state, but the toolbar won't be notified about that.

    Sure but that is really mixing metaphors. If someone knows how to switch layout engine midstream it seems less likely they would be confused about this.

  13. anntzer commented on Jul 29, 2025

    @anntzer
    Contributor

    I'd probably just stick with setting an error message in the statusbar as the simplest way forward, we can always improve things later...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions