Search code examples
pythonmatplotlibtextboxwidget

matplotlib.widgets.TextBox interaction is slow when figure contains several subplots


Below is python code to demonstrate the problem.
If there are 2 rows and 2 columns of images, for example, typing/erasing in the textbox is reasonably fast. However, if there are 5 rows and 5 columns, typing/erasing in the textbox is quite slow. If the xticks and yticks are drawn, interaction is even slower. So, it seems as if the entire figure is redrawn after every keystroke.
Is there a solution for this (apart from putting the textbox on a separate figure)?
(My development platform is MacOS Mojave, Python 3.7.5.)

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.widgets import TextBox

class Textbox_Demo(object):
    def __init__(self):
        self.fig = plt.figure(figsize=(8,8))
        self.string = 'label'

        self.rows = 5   # reducing rows speeds up textbox interaction
        self.cols = 5   # reducing cols speeds up textbox interaction
        self.plot_count = self.rows * self.cols
        self.gs = gridspec.GridSpec(self.rows, self.cols,
            left=0.05, right=1-0.02, top=1-.02,  bottom=0.10, wspace=0.3, hspace=0.4)
        for k in range(self.plot_count):
            ax = self.fig.add_subplot(self.gs[k])
            #ax.set_xticks([])  # showing axes slows textbox interaction
            #ax.set_yticks([])  # showing axes slows textbox interaction
            data = np.atleast_2d(np.sin(np.linspace(1,255,255) * 50))
            ax.imshow(data, aspect="auto", cmap='ocean')

        # this is the user-input textbox
        tb_axis = plt.axes([0.125, 0.02, 0.8, 0.05])
        self.tb = TextBox(tb_axis, 'Enter label:', initial=self.string, label_pad=0.01)
        self.tb.on_submit(self.on_submit)

        plt.show()

    def on_submit(self, text):
        pass

if __name__ == "__main__":
    Textbox_Demo()

Solution

  • Matplotlib's TextBox is inherently slow, because it uses the drawing tools provided by matplotlib itself and hence redraws the complete figure upon changes.

    I would propose to use a text box of a GUI kit instead. For example for PyQt this might look like:

    enter image description here

    import numpy as np
    import sys
    from matplotlib.backends.backend_qt5agg import (
            FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
    from matplotlib.backends.qt_compat import QtCore, QtWidgets
    import matplotlib.gridspec as gridspec
    from matplotlib.figure import Figure
    
    
    class Textbox_Demo(QtWidgets.QMainWindow):
        def __init__(self):
            super().__init__()
            self._main = QtWidgets.QWidget()
            self.setCentralWidget(self._main)
            layout = QtWidgets.QVBoxLayout(self._main)
            layout.setContentsMargins(0,0,0,0)
            layout.setSpacing(0)
    
            self.fig = Figure(figsize=(8,8))
            self.canvas = FigureCanvas(self.fig)
            layout.addWidget(self.canvas)
            self.addToolBar(NavigationToolbar(self.canvas, self))
    
            self._textwidget = QtWidgets.QWidget()
            textlayout = QtWidgets.QHBoxLayout(self._textwidget)
            self.textbox = QtWidgets.QLineEdit(self)
            self.textbox.editingFinished.connect(self.on_submit)
            # or, if wanting to have changed apply directly:
            # self.textbox.textEdited.connect(self.on_submit)
            textlayout.addWidget(QtWidgets.QLabel("Enter Text: "))
            textlayout.addWidget(self.textbox)
            layout.addWidget(self._textwidget)
    
            self.fill_figure()
    
        def fill_figure(self):
            self.string = 'label'
    
            self.rows = 5   # reducing rows speeds up textbox interaction
            self.cols = 5   # reducing cols speeds up textbox interaction
            self.plot_count = self.rows * self.cols
            self.gs = gridspec.GridSpec(self.rows, self.cols,
                left=0.05, right=1-0.02, top=1-.02,  bottom=0.10, wspace=0.3, hspace=0.4)
            for k in range(self.plot_count):
                ax = self.fig.add_subplot(self.gs[k])
                #ax.set_xticks([])  # showing axes slows textbox interaction
                #ax.set_yticks([])  # showing axes slows textbox interaction
                data = np.atleast_2d(np.sin(np.linspace(1,255,255) * 50))
                ax.imshow(data, aspect="auto", cmap='ocean')
    
        def on_submit(self):
            text = self.textbox.text()
            print(text)
            pass
    
    
    if __name__ == "__main__":
        qapp = QtWidgets.QApplication(sys.argv)
        app = Textbox_Demo()
        app.show()
        qapp.exec_()