pythonmatplotlibrangesubplot

Move Span center to click position not showing range when clicking in subplot with shown range


I have created a 2 plot subplot. Selecting the range in both subplots works fine. Clicking into the upper subplot (the zoomed plot) shifts both views fine, but when clicking in the lower (total view) plot, the selected range (SpanSelector) region vanishes. What am I missing.

As a next step I intend to ass a cross-hair cursor to the top (zoomed) plot.

Attaches the code-example:

import sys
import numpy as np
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib.pyplot as plt
from matplotlib.widgets import MultiCursor
from PyQt5.QtWidgets import QMainWindow,QVBoxLayout
from PyQt5.QtWidgets import QApplication
from PyQt5 import QtCore, QtGui, QtWidgets
from matplotlib.ticker import FuncFormatter
from matplotlib.widgets import SpanSelector
import matplotlib.ticker as ticker

class MainWindow_code_serarch(object):

    def setup_code_serarch(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(1024, 800)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayoutWidget = QtWidgets.QWidget(self.centralwidget)
        self.verticalLayoutWidget.setGeometry(QtCore.QRect(0, 0, 1024, 800))
        self.verticalLayoutWidget.setObjectName("verticalLayoutWidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget)
        self.verticalLayout.setContentsMargins(0,0,0,0)
        self.verticalLayout.setObjectName("verticalLayout")


        self.figure = plt.figure()
        self.canvas = FigureCanvas(self.figure)
        self.verticalLayout.addWidget(self.canvas)
        axes, axes2 = self.figure.subplots(nrows=2, sharex=True)
    
        datacount = 100000
    
        data_x = []
        data_y = []
    
        for i in range(1, datacount):
            data_x.append(i)
            if i % 250 <= 50:
                data_y.append(np.nan)
            else:
                data_y.append(np.sin(i/100)+0.05*np.sin(i))
    
        #** matplotlib subplot *********************************************
        self.figure.subplots_adjust(left=0.03, right=0.97, top=0.975, bottom=0.075, wspace=0, hspace=0.2)
    
        ax1, ax2 = self.figure.subplots(2, height_ratios=[0.8,0.2])
        ax1.grid()
        ax1.tick_params(axis = 'x', length=0)
        ax1.tick_params(axis = 'y', length=0)

        ax2.tick_params(axis = 'x', length=0)
        ax2.tick_params(axis = 'y', length=0)
        ax2.grid()
    
        def format_tick_labels(x, pos):
            return '{0:2.0e}'.format(x)

        ax2.xaxis.set_major_formatter(FuncFormatter(format_tick_labels))
        
        ax2.plot(data_x, data_y, linewidth = 0.5)
        ax2.set_xlim(0, datacount)
        ax2.set_ylim(-1.2, 1.2)
    
        self.line2, = ax1.plot([], [], linewidth=0.5)
    
        def onselect_span(xmin, xmax):
            print('onselect_span')
            indmin, indmax = np.searchsorted(data_x, (xmin, xmax))
        
            print(f'Span: select indmin = {indmin}, indmax = {indmax}')

            region_x = data_x[indmin:indmax]   
            region_y = data_y[indmin:indmax]   
        

            if len(region_x)>=2:
                self.line2.set_data(region_x, region_y)
                ax1.set_xlim(region_x[0], region_x[-1])     #select from region start till region end...
                ax1.set_ylim(-1.2,1.2)#region_y[0], region_y[-1])
                self.span2.extents = (region_x[0], region_x[-1])
                self.canvas.draw_idle()
                self.mouse_SpanSelected = True;    
            else: 
                print('onselect_span: region too small') 
            
            self.span1.clear()
            print('onselect_span end')
  
    
        self.span1 = SpanSelector(
            ax1,
            onselect_span,
            "horizontal",
            useblit=True,
            props=dict(alpha=0.3, facecolor="tab:red"),
            interactive=True,
            drag_from_anywhere=True,
            grab_range = 3,
        )
    
        self.span2 = SpanSelector(
            ax2,
            onselect_span,
            "horizontal",
            useblit=True,
            props=dict(alpha=0.3, facecolor="tab:red"),
            interactive=True,
            drag_from_anywhere=True,
            grab_range = 3,
        )
    
        def onclick(event):
            global ix
            ix = event.xdata
            print('onclick end')

                
        def onrelease(event):
            print('onrelease')
            global ix, ixrel
            ixrel = event.xdata

            if abs(ix-ixrel)<=2:
                print('Release: region too small')
                width_half = int ((self.line2._x[-1] - self.line2._x[0])/2)
                self.span2.extents = (ixrel - width_half, ixrel + width_half)
                onselect_span(ixrel - width_half, ixrel + width_half)
                self.span2.update()
            
            self.canvas.draw_idle()
            
            print('onrelease end')
        
        
        click_id = self.figure.canvas.mpl_connect('button_press_event', onclick)
        relaese_id = self.figure.canvas.mpl_connect('button_release_event', onrelease)

        self.span2.extents = (4000,15501)  #set selectred region
        onselect_span(4000,15501)

        self.canvas.draw()
    
        MainWindow.setCentralWidget(self.centralwidget)


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = MainWindow_code_serarch()
    ui.setup_code_serarch(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

Image: at start: this is how it looks after starting the app this is how it looks after starting the app when I click in the upper graph the range shifts when I click in the upper graph the range shifts when I click in the lower graph the range disappears => this is my problem when I click in the lower graph the range disappears => this is my problem


Solution

  • The thing is that when you click, that is like selecting an empty span (I take that you are already aware of that, since you instrument the code with a lot of print that tell you exactly what happen, with what values).

    And SpanSelector is removed when you select an empty span. And this occurs even before you had a chance to call your callbacks. (I have a vague feeling, but very vague, I didn't take time to understand why you did all these things with a callback not only for select but also for click and release, that you are recoding what SpanSelector is already doing).

    You can prevent that removal by playing with parameter minspan of SpanSelectors. Make it minspan=-1, and then they will never be removed. Sure, it is a aberration theoretically (default minspan=0 is supposed to be the least value that makes sense), but in your case, since you will then do what it takes to put it back if needed...

    At any rate, it seems to be working like I think you expect

    So, my minimal reproducible answer :-) (it is only a copy of your question, with two additional minspan=-1)

    import sys
    import numpy as np
    from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
    import matplotlib.pyplot as plt
    from matplotlib.widgets import MultiCursor
    from PyQt5.QtWidgets import QMainWindow,QVBoxLayout
    from PyQt5.QtWidgets import QApplication
    from PyQt5 import QtCore, QtGui, QtWidgets
    from matplotlib.ticker import FuncFormatter
    from matplotlib.widgets import SpanSelector
    import matplotlib.ticker as ticker
    
    class MainWindow_code_serarch(object):
    
        def setup_code_serarch(self, MainWindow):
            MainWindow.setObjectName("MainWindow")
            MainWindow.resize(1024, 800)
            self.centralwidget = QtWidgets.QWidget(MainWindow)
            self.centralwidget.setObjectName("centralwidget")
            self.verticalLayoutWidget = QtWidgets.QWidget(self.centralwidget)
            self.verticalLayoutWidget.setGeometry(QtCore.QRect(0, 0, 1024, 800))
            self.verticalLayoutWidget.setObjectName("verticalLayoutWidget")
            self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget)
            self.verticalLayout.setContentsMargins(0,0,0,0)
            self.verticalLayout.setObjectName("verticalLayout")
    
    
            self.figure = plt.figure()
            self.canvas = FigureCanvas(self.figure)
            self.verticalLayout.addWidget(self.canvas)
            axes, axes2 = self.figure.subplots(nrows=2, sharex=True)
        
            datacount = 100000
        
            data_x = []
            data_y = []
        
            for i in range(1, datacount):
                data_x.append(i)
                if i % 250 <= 50:
                    data_y.append(np.nan)
                else:
                    data_y.append(np.sin(i/100)+0.05*np.sin(i))
        
            #** matplotlib subplot *********************************************
            self.figure.subplots_adjust(left=0.03, right=0.97, top=0.975, bottom=0.075, wspace=0, hspace=0.2)
        
            ax1, ax2 = self.figure.subplots(2, height_ratios=[0.8,0.2])
            ax1.grid()
            ax1.tick_params(axis = 'x', length=0)
            ax1.tick_params(axis = 'y', length=0)
    
            ax2.tick_params(axis = 'x', length=0)
            ax2.tick_params(axis = 'y', length=0)
            ax2.grid()
        
            def format_tick_labels(x, pos):
                return '{0:2.0e}'.format(x)
    
            ax2.xaxis.set_major_formatter(FuncFormatter(format_tick_labels))
            
            ax2.plot(data_x, data_y, linewidth = 0.5)
            ax2.set_xlim(0, datacount)
            ax2.set_ylim(-1.2, 1.2)
        
            self.line2, = ax1.plot([], [], linewidth=0.5)
        
            def onselect_span(xmin, xmax):
                print('onselect_span')
                indmin, indmax = np.searchsorted(data_x, (xmin, xmax))
            
                print(f'Span: select indmin = {indmin}, indmax = {indmax}')
    
                region_x = data_x[indmin:indmax]   
                region_y = data_y[indmin:indmax]   
            
    
                if len(region_x)>=2:
                    self.line2.set_data(region_x, region_y)
                    ax1.set_xlim(region_x[0], region_x[-1])     #select from region start till region end...
                    ax1.set_ylim(-1.2,1.2)#region_y[0], region_y[-1])
                    self.span2.extents = (region_x[0], region_x[-1])
                    self.canvas.draw_idle()
                    self.mouse_SpanSelected = True;    
                else: 
                    print('onselect_span: region too small') 
                
                self.span1.clear()
                print('onselect_span end')
      
        
            self.span1 = SpanSelector(
                ax1,
                onselect_span,
                "horizontal",
                useblit=True,
                props=dict(alpha=0.3, facecolor="tab:red"),
                interactive=True,
                drag_from_anywhere=True,
                minspan=-1,                      ## <<<<<<<< HERE
                grab_range = 3,
            )
        
            self.span2 = SpanSelector(
                ax2,
                onselect_span,
                "horizontal",
                useblit=True,
                props=dict(alpha=0.3, facecolor="tab:red"),
                interactive=True,
                drag_from_anywhere=True,
                minspan=-1,                            ## <<<<<<<< AND HERE
                grab_range = 3,
            )
        
            def onclick(event):
                global ix
                ix = event.xdata
                print('onclick end')
    
                    
            def onrelease(event):
                print('onrelease')
                global ix, ixrel
                ixrel = event.xdata
    
                if abs(ix-ixrel)<=2:
                    print('Release: region too small')
                    width_half = int ((self.line2._x[-1] - self.line2._x[0])/2)
                    self.span2.extents = (ixrel - width_half, ixrel + width_half)
                    onselect_span(ixrel - width_half, ixrel + width_half)
                    self.span2.update()
                
                self.canvas.draw_idle()
                
                print('onrelease end')
            
            
            click_id = self.figure.canvas.mpl_connect('button_press_event', onclick)
            relaese_id = self.figure.canvas.mpl_connect('button_release_event', onrelease)
    
            self.span2.extents = (4000,15501)  #set selectred region
            onselect_span(4000,15501)
    
            self.canvas.draw()
        
            MainWindow.setCentralWidget(self.centralwidget)
    
    
    if __name__ == "__main__":
        import sys
        app = QtWidgets.QApplication(sys.argv)
        MainWindow = QtWidgets.QMainWindow()
        ui = MainWindow_code_serarch()
        ui.setup_code_serarch(MainWindow)
        MainWindow.show()
        sys.exit(app.exec_())