Search code examples
pythonmatplotlibfigureaxes

What is the proper way of re-adding axes to a matplotlib figure while maintaining a constrained layout?


I have a program where I would like to swap between different axes in the same figure in order to plot different datasets against eachother, either in a polar or rectilinear coordinate system depending on the dataset. The change_axes method implements this functionality, while the swap_axes, clf_swap, hide_swap and new_subplot_swap methods show different ways of trying to swap between axes, although swap_axes is the only one that produces the desired result. The code is based on this post.

Is there a better way of doing this? Using axes.set_visible or axes.set_alpha does nothing for me and even produces an Error for some reason (AttributeError: 'NoneType' object has no attribute 'get_points'). I don't understand why this doesn't work, but it would have been a much easier way of 'adding' and 'removing' the axes.

import numpy as np
import sys
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import \
    FigureCanvasQTAgg as FigureCanvas

from PyQt5.QtWidgets import (
    QApplication, QDialog, QGridLayout, QComboBox, QPushButton
)


class Test(QDialog):
    def __init__(self):
        super().__init__()
        self.lay = QGridLayout(self)
        self.fig, self.ax = plt.subplots(constrained_layout=True)

        self.ax2 = self.fig.add_subplot(111, projection='polar')
        # self.ax2 = plt.subplot(111, projection='polar')

        # uncomment this for 'hide_swap'
        # self.ax2.set_visible(False)

        self.canvas = FigureCanvas(self.fig)
        self.lay.addWidget(self.canvas)
        self.data = {
            'Dataset 1': np.random.normal(2, 0.5, 10000),
            'Dataset 2': np.random.binomial(10000, 0.3, 10000),
            'Dataset 3': np.random.uniform(0, 2*np.pi, 10000)
        }
        _, _, self.artists = self.ax.hist(self.data['Dataset 1'], 100)

        self.swapBtn = QPushButton('Swap')
        self.lay.addWidget(self.swapBtn, 1, 0)
        self.swapBtn.clicked.connect(self.swap_axes)
        self.clfSwapBtn = QPushButton('fig.clf() swap')
        self.clfSwapBtn.clicked.connect(self.clf_swap)
        self.lay.addWidget(self.clfSwapBtn, 2, 0)
        self.hideSwapBtn = QPushButton('Hide swap')
        self.hideSwapBtn.clicked.connect(self.hide_swap)
        self.lay.addWidget(self.hideSwapBtn, 3, 0)
        self.subplotSwapBtn = QPushButton('New subplot swap')
        self.subplotSwapBtn.clicked.connect(self.new_subplot_swap)
        self.lay.addWidget(self.subplotSwapBtn, 4, 0)
        self.xParam = QComboBox()
        self.xParam.addItem('Dataset 1')
        self.xParam.addItem('Dataset 2')
        self.xParam.addItem('Dataset 3')
        self.xParam.currentTextChanged.connect(self.change_axes)
        self.lay.addWidget(self.xParam)
        self.yParam = QComboBox()
        self.yParam.addItem('Distribution')
        self.yParam.addItem('Dataset 1')
        self.yParam.addItem('Dataset 2')
        self.yParam.addItem('Dataset 3')
        self.yParam.currentTextChanged.connect(self.change_axes)
        self.lay.addWidget(self.yParam)
        self.canvas.draw()

        # this is neccessary for
        # "self.ax2 = self.fig.add_subplot(111, projection='polar')", and for
        # some reason has to be called after 'canvas.draw', otherwise,
        # the constrained layout cannot be applied. comment this if using
        # "self.ax2 = plt.subplot(111, projection='polar')" or "hide_swap"
        self.ax2.remove()

    def change_axes(self):
        if self.yParam.currentText() == 'Distribution':
            if self.xParam.currentText() == 'Dataset 3':
                if self.fig.axes[0] == self.ax:
                    self.ax.remove()
                    self.ax2.figure = self.fig
                    self.fig.axes.append(self.ax2)
                    self.fig.add_axes(self.ax2)
                radii, theta = np.histogram(self.data['Dataset 3'], 100)
                width = np.diff(theta)
                self.fig.axes[0].cla()
                self.artists = self.ax2.bar(theta[:-1], radii, width=width)
            else:
                if self.fig.axes[0] == self.ax2:
                    self.ax2.remove()
                    self.ax.figure = self.fig
                    self.fig.axes.append(self.ax)
                    self.fig.add_axes(self.ax)
                self.fig.axes[0].cla()
                _, _, self.artists = self.ax.hist(
                    self.data[self.xParam.currentText()], 100
                )
        else:
            if (
                self.xParam.currentText() == 'Dataset 3'
                and self.fig.axes[0] == self.ax
            ):
                self.ax.remove()
                self.ax2.figure = self.fig
                self.fig.axes.append(self.ax2)
                self.fig.add_axes(self.ax2)
            elif (
                self.xParam.currentText() != 'Dataset 3'
                and self.fig.axes[0] == self.ax2
            ):
                self.ax2.remove()
                self.ax.figure = self.fig
                self.fig.axes.append(self.ax)
                self.fig.add_axes(self.ax)
            self.fig.axes[0].cla()
            self.artists = self.fig.axes[0].plot(
                self.data[self.xParam.currentText()],
                self.data[self.yParam.currentText()], 'o'
            )
        self.canvas.draw()

    def swap_axes(self):
        if self.fig.axes[0] == self.ax:
            self.ax.remove()
            self.ax2.figure = self.fig
            self.fig.axes.append(self.ax2)
            self.fig.add_axes(self.ax2)
        else:
            self.ax2.remove()
            self.ax.figure = self.fig
            self.fig.axes.append(self.ax)
            self.fig.add_axes(self.ax)
        self.canvas.draw()

    def clf_swap(self):
        if self.fig.axes[0] == self.ax:
            self.fig.clf()
            self.ax2.figure = self.fig
            self.fig.add_axes(self.ax2)
        else:
            self.fig.clf()
            self.ax.figure = self.fig
            _, _, self.artists = self.ax.hist(self.data['Dataset 1'], 100)
            self.fig.add_axes(self.ax)
        self.canvas.draw()

    def hide_swap(self):
        if self.ax.get_visible():
            self.ax.set_visible(False)
            self.ax2.set_visible(True)
            # self.ax.set_alpha(0)
            # self.ax2.set_alpha(1)
        else:
            self.ax.set_visible(True)
            self.ax2.set_visible(False)
            # self.ax.set_alpha(1)
            # self.ax2.set_alpha(0)
        self.canvas.draw()

    def new_subplot_swap(self):
        if self.fig.axes[0].name == 'rectilinear':
            self.ax.remove()
            self.ax2 = self.fig.add_subplot(111, projection='polar')
        else:
            self.ax2.remove()
            self.ax = self.fig.add_subplot(111)
            _, _, self.artists = self.ax.hist(self.data['Dataset 1'], 100)
        self.canvas.draw()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    test = Test()
    test.show()
    sys.exit(app.exec_())

Solution

  • Using the current development version of matplotlib the error would not appear. But one would need to make sure to exclude the respective axes from the constrained layout mechanism.

    import matplotlib.pyplot as plt
    
    fig, ax = plt.subplots(constrained_layout=True)
    ax2 = fig.add_subplot(111, projection="polar")
    ax2.set_visible(False)
    ax2.set_in_layout(False)
    
    def swap(evt):
        if evt.key == "h":
            b = ax.get_visible()
            ax.set_visible(not b)
            ax.set_in_layout(not b)
            ax2.set_visible(b)
            ax2.set_in_layout(b)
            fig.canvas.draw_idle()
    
    cid = fig.canvas.mpl_connect("key_press_event", swap)
    
    plt.show()
    

    With any current matplotlib version, one could use tight_layout instead of constrained layout, and call it manually at each resize event.

    import matplotlib.pyplot as plt
    
    fig, ax = plt.subplots()
    ax2 = fig.add_subplot(111, projection="polar")
    ax2.set_visible(False)
    
    
    def swap(evt):
        if evt.key == "h":
            b = ax.get_visible()
            ax.set_visible(not b)
            ax2.set_visible(b)
            fig.tight_layout()
            fig.canvas.draw_idle()
    
    def onresize(evt):
        fig.tight_layout()
    
    cid = fig.canvas.mpl_connect("key_press_event", swap)
    cid = fig.canvas.mpl_connect("resize_event", onresize)
    fig.tight_layout()    
    plt.show()