pythonmatplotlib

mplcursor not appearing over dynamically selected plot


I am trying to show a few different sets of data on one plot. The user can then select a specific data set to view alone, and in that case, I want to offer a hover cursor to show information about that data.

My hover cursors never appear, though. Why not?

It's taken me a while to produce an MRE, but here it is:

import customtkinter as ctk
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import mplcursors

def plot_data(x_data, y_data, plot_var):
    x_plot = [] # data to draw on the plot
    y_plot = [] 
    if plot_var in [1,2]:
        x_plot.append(x_data[0])
        y_plot.append(y_data[0])
    if plot_var in [1,3]:
        x_plot.append(x_data[1])
        y_plot.append(y_data[1])
    if plot_var in [1,4]:
        x_plot.append(x_data[2])
        y_plot.append(y_data[2])
    if plot_var in [1,5]:
        x_plot.append(x_data[3])
        y_plot.append(y_data[3])
    
    fig = Figure(figsize=(5, 5))
    ax  = fig.add_subplot(111)
    for i in range(len(x_plot)):
        ax.plot(x_plot[i], y_plot[i], marker='o', label=f"Data {i+1}")
    
    if plot_var != 1:
        cursor = mplcursors.cursor(ax, hover=True)
        cursor.connect("add", lambda sel: sel.annotation.set_text(f"Data {plot_var-1}"))
        
    return fig

def draw_plot_from_rb(canvas, x_data, y_data, plot_var):
    canvas.figure = plot_data(x_data, y_data, plot_var)
    canvas.draw()
    canvas.get_tk_widget().pack(side=ctk.TOP, fill=ctk.BOTH, expand=True)
    
    return canvas

# Create the main window
window = ctk.CTk()
window.title("Custom Window")

# Create the options frame
options_frame = ctk.CTkFrame(window)
options_frame.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True)

# Create the data frame
data_frame = ctk.CTkFrame(window)
data_frame.pack(side=ctk.RIGHT, fill=ctk.BOTH, expand=True)

# Add widgets to the options frame
options_label = ctk.CTkLabel(options_frame, text="Options", font=("Arial", 16))
options_label.pack()

# Add widgets to the data frame
data_label = ctk.CTkLabel(data_frame, text="Data", font=("Arial", 16))
data_label.pack()

# Create four separate x, y datasets
x1 = [1, 2, 3, 4, 5]
y1 = [1, 4, 9, 16, 25]

x2 = [1, 2, 3, 4, 5]
y2 = [36, 49, 64, 81, 100]

x3 = [1, 2, 3, 4, 5]
y3 = [121, 144, 169, 196, 225]

x4 = [1, 2, 3, 4, 5]
y4 = [256, 289, 324, 361, 400]

x = [x1, x2, x3, x4]
y = [y1, y2, y3, y4]

# Create four radio button options
plot_var = ctk.IntVar()
plot_var.set(1)
radio1 = ctk.CTkRadioButton(
    options_frame, text="View All", variable=plot_var, value=1,
    command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get())
)
radio1.pack()
radio2 = ctk.CTkRadioButton(
    options_frame, text="Data 1", variable=plot_var, value=2,
    command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get())
)
radio2.pack()
radio3 = ctk.CTkRadioButton(
    options_frame, text="Data 2", variable=plot_var, value=3,
    command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get())
)
radio3.pack()
radio4 = ctk.CTkRadioButton(
    options_frame, text="Data 3", variable=plot_var, value=4,
    command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get())
)
radio4.pack()
radio5 = ctk.CTkRadioButton(
    options_frame, text="Data 4", variable=plot_var, value=5,
    command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get())
)
radio5.pack()

fig = plot_data(x, y, plot_var.get())

# Create the canvas for the plot
canvas = FigureCanvasTkAgg(fig, master=data_frame)
canvas.draw()
canvas.get_tk_widget().pack(side=ctk.TOP, fill=ctk.BOTH, expand=True)

# Start the main loop
window.mainloop()

Solution

  • It works for me if I create fig and ax only once and later I use ax.clear() to remove previous plots.

    Maybe it works because this way cursor is created after adding fig to FigureCanvasTkAgg like suggests answer for mplcursors not getting generated when used with Tkinter

    I think new fig would need to be added again to FigureCanvasTkAgg before creating new cursor but your original code first creates cursor and later it assigns new fig to FigureCanvasTkAgg. But I didn't test if changing order could resolve problem - because there is no need to create new fig.

    import customtkinter as ctk
    from matplotlib.figure import Figure
    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
    import mplcursors
    
    def plot_data(x_data, y_data, plot_var):
        ax.clear()  # <--- clear plot instead of creating new one
    
        x_plot = [] # data to draw on the plot
        y_plot = [] 
        if plot_var in [1,2]:
            x_plot.append(x_data[0])
            y_plot.append(y_data[0])
        if plot_var in [1,3]:
            x_plot.append(x_data[1])
            y_plot.append(y_data[1])
        if plot_var in [1,4]:
            x_plot.append(x_data[2])
            y_plot.append(y_data[2])
        if plot_var in [1,5]:
            x_plot.append(x_data[3])
            y_plot.append(y_data[3])
        
        for i in range(len(x_plot)):
            ax.plot(x_plot[i], y_plot[i], marker='o', label=f"Data {i+1}")
        
        if plot_var != 1:
            print('add cursor:', plot_var)
            cursor = mplcursors.cursor(ax, hover=True)
            cursor.connect("add", lambda sel: sel.annotation.set_text(f"Data {plot_var-1}\nx: {sel.target[0]}\ny: {sel.target[1]}"))
    
    def draw_plot_from_rb(canvas, x_data, y_data, plot_var):
        plot_data(x_data, y_data, plot_var)
        canvas.draw()
        #canvas.get_tk_widget().pack(side=ctk.TOP, fill=ctk.BOTH, expand=True)  # there is no need to put new widget
        #return canvas  # button can't get this value - is using `return` is useless
    
    # Create the main window
    window = ctk.CTk()
    window.title("Custom Window")
    
    # Create the options frame
    options_frame = ctk.CTkFrame(window)
    options_frame.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True)
    
    # Create the data frame
    data_frame = ctk.CTkFrame(window)
    data_frame.pack(side=ctk.RIGHT, fill=ctk.BOTH, expand=True)
    
    # Add widgets to the options frame
    options_label = ctk.CTkLabel(options_frame, text="Options", font=("Arial", 16))
    options_label.pack()
    
    # Add widgets to the data frame
    data_label = ctk.CTkLabel(data_frame, text="Data", font=("Arial", 16))
    data_label.pack()
    
    # Create four separate x, y datasets
    x1 = [1, 2, 3, 4, 5]
    y1 = [1, 4, 9, 16, 25]
    
    x2 = [1, 2, 3, 4, 5]
    y2 = [36, 49, 64, 81, 100]
    
    x3 = [1, 2, 3, 4, 5]
    y3 = [121, 144, 169, 196, 225]
    
    x4 = [1, 2, 3, 4, 5]
    y4 = [256, 289, 324, 361, 400]
    
    x = [x1, x2, x3, x4]
    y = [y1, y2, y3, y4]
    
    # Create four radio button options
    plot_var = ctk.IntVar()
    plot_var.set(1)
    radio1 = ctk.CTkRadioButton(
        options_frame, text="View All", variable=plot_var, value=1,
        command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get())
    )
    radio1.pack()
    radio2 = ctk.CTkRadioButton(
        options_frame, text="Data 1", variable=plot_var, value=2,
        command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get())
    )
    radio2.pack()
    radio3 = ctk.CTkRadioButton(
        options_frame, text="Data 2", variable=plot_var, value=3,
        command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get())
    )
    radio3.pack()
    radio4 = ctk.CTkRadioButton(
        options_frame, text="Data 3", variable=plot_var, value=4,
        command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get())
    )
    radio4.pack()
    radio5 = ctk.CTkRadioButton(
        options_frame, text="Data 4", variable=plot_var, value=5,
        command=lambda: draw_plot_from_rb(canvas, x, y, plot_var.get())
    )
    radio5.pack()
    
    # <--- create `fig` and `ax` only once
    fig = Figure(figsize=(5, 5))
    ax  = fig.add_subplot(111)
    
    plot_data(x, y, plot_var.get())
    
    # Create the canvas for the plot
    canvas = FigureCanvasTkAgg(fig, master=data_frame)
    canvas.draw()
    canvas.get_tk_widget().pack(side=ctk.TOP, fill=ctk.BOTH, expand=True)
    
    # Start the main loop
    window.mainloop()
    

    BTW: I added details sel.target in

    sel.annotation.set_text(f"Data {plot_var-1}\nx: {sel.target[0]}\ny: {sel.target[1]}")