pythonmatplotlibuser-interface

Input string name graphically, directly on figure via matplotlib


I have a simple idea that I would like to execute. From some data on a 1D plot, extract different segments by selecting the starting and ending points. Moreover, for each segment selected, specify a corresponding (string) name for this line. These three values (name, starting index, ending index) are then stored for later use.

I have included a minimum working example. Now, this works, but what I prefer is to have the "name" input done on the figure instead of the terminal. In other words, use something like TextBox, rather that input. The problem is that I cannot replicate the same behavior of input from the TextBox example. Mainly, I cannot have the program "wait for the user to input a string name, before continuing onwards with the program". Instead, it keeps taking input.

Anyway, below is a MWE. Can you please help me find a way to replicate the input function in a graphical way? Thank you and I hope this proves useful for future readers in their endeavors :)

import numpy as np
from matplotlib import pyplot as plt


def main():

    # Predefine a fixed number of segments, for demonstration.
    nSegment = 3

    # Counter for the number of points selected.
    nPointPressed = 0   

    # Default initial starting point index.
    I0 = 0

    # Initialize segment variable.
    segment = []

    def onpick(event):
        # Non-local and global variables.
        nonlocal nPointPressed
        nonlocal I0

        # Get the index of the point chosen.
        ind = event.ind

        # Points selected must be one.
        if len(ind) != 1:
            return
    
        # Illustrate graphically the selected segment.
        ax.plot(x[ind], y[ind], 'o', markerfacecolor='none', color='red', linewidth=2, markersize=5)
        plt.draw() 

        # Increment the number of points pressed.
        nPointPressed += 1
        
        # Distinguish between starting/ending indices. Odd values are starting indices, even ones are ending indices.
        if nPointPressed % 2 == 0:
        
            # An overkill, but for demonstration, explicitly define the interval.
            ij = np.linspace( I0, ind, dtype=int )

            # Plot the connecting line between the two indices.
            plt.plot(x[ij], y[ij], color='green')
            
            # Ask the user for an explicit segment name.
            name = input("Enter a name for current line segment: ")

            # Book-keep the current segment information: ["name", iStart, iEnd].
            segment.append([name, ij[0], ij[-1]])

            # Extract the plots from the figure and remove the starting and ending points.
            pts = ax.get_lines()
            pts[-3].remove() # start point
            pts[-2].remove() # end   point


            # Change the color of the most recent connecting line.
            pts[-1].set_color("green")
            
        else:
            # Save the starting index.
            I0 = ind

       
        # Expected segments are complete, close the figure.
        if len(segment) == nSegment:
            plt.close()


    # Some data.
    x = np.linspace( 0, 2*np.pi, 20 , dtype=float )
    y = np.sin( x ) 
   
    # Plot original data and enable interaction.
    fig, ax = plt.subplots()
    ax.plot(x, y, '-^', picker=True)
    fig.canvas.mpl_connect("pick_event", onpick)
    plt.show()
 
    # For demonstration, print the resulting segment information.
    for i in range( len(segment) ):
        print( segment[i] )


if __name__ == '__main__':
    main()

EDIT: Here's a snippet of what I considered via the TextBox widget, but didn't work. The below replaces the name = input... line above (after importing the TextBox widgeet, ofc).

name = []
def submit(expression):
    nonlocal name
    name = expression

axbox = fig.add_axes([0.1, 0.05, 0.8, 0.075])
text_box = TextBox(axbox, "Segment name: ", textalignment="center")
text_box.on_submit(submit)
text_box.set_val("Unknown") 

Solution

  • Reproducing your code, I have experience two main issues:

    Find here the snippet implementing what I have described:

    name = []
    done = False    # flag to stop updating the plot when the user hits Enter
    def on_submit(expression):
           nonlocal name, done
           name = expression
           done = True
    
    axbox = fig.add_axes([0.1, 0.05, 0.8, 0.075])
                
    initial_string = 'Unknown' # the default string in the TextBox
    text_box = TextBox(axbox, "Segment name: ", 
                       textalignment="center",
                       initial=initial_string)
    text_box.on_submit(on_submit)
    # The plot has to be re-drawn at each keystroke for the typing process to be seen
    while not done:
        plt.draw()
        plt.pause(0.01)
                
    # Reset the box to the initial value without triggering `on_pick`        
    text_box.eventson = False
    text_box.set_val(initial_string)
    text_box.eventson = True
    

    Hope it helps you!