0

Leveraging on this topic, and using the documentation of ipywidgets, I would like to create an interactive plot with a dropdown menu (essentially used to filter the data and plot a different line) and draggable markers. Moreover, I would like to add an "Export" button to export the new y-values of the curve created by the drag action of the user. The main goal of this tool is that a user can define a custom curve for each object in the dropdown menu, and to export the new values that will be used as input in another tool.

Here is a sketch of the code (built in Jupyter):

import matplotlib.animation as animation
from matplotlib.widgets import Slider, Button
import matplotlib as mpl
from matplotlib import pyplot as plt
from ipywidgets import interactive
    
%matplotlib widget

def brand_selection(brand):
    df_by_brand = df.query("Brand == @brand and Time == -1")
    df_by_brand[['Label','Qty']]
    x = df_by_brand['Label'].tolist()
    N = len(x)
    y = df_by_brand['Qty'].tolist()
    yvals = y
    fig,axes = plt.subplots(1,1,figsize=(9.0,8.0),sharex=True)
    #mpl.rcParams['figure.subplot.right'] = 0.8
    ax1 = axes
    pind = None #active point
    epsilon = 5 #max pixel distance
    def update(val):
        global yvals
        # update curve
        for i in np.arange(N):
            yvals[i] = sliders[i].val 
        l.set_ydata(yvals)
        # redraw canvas while idle
        fig.canvas.draw_idle()

    def reset(event):
        global yvals
        #reset the values
        yvals = y
        for i in np.arange(N):
            sliders[i].reset()
        l.set_ydata(yvals)
        # redraw canvas while idle
        fig.canvas.draw_idle()

    def export(event):
        global yvals
        np.savetxt(f'{brand}.csv', yvals)

    def button_press_callback(event):
        'whenever a mouse button is pressed'
        global pind
        if event.inaxes is None:
            return
        if event.button != 1:
            return
        #print(pind)
        pind = get_ind_under_point(event)    

    def button_release_callback(event):
        'whenever a mouse button is released'
        global pind
        if event.button != 1:
            return
        pind = None

    def get_ind_under_point(event):
        'get the index of the vertex under point if within epsilon tolerance'

        # display coords
        #print('display x is: {0}; display y is: {1}'.format(event.x,event.y))
        t = ax1.transData.inverted()
        tinv = ax1.transData 
        xy = t.transform([event.x,event.y])
        #print('data x is: {0}; data y is: {1}'.format(xy[0],xy[1]))
        xr = np.reshape(x,(np.shape(x)[0],1))
        yr = np.reshape(yvals,(np.shape(yvals)[0],1))
        xy_vals = np.append(xr,yr,1)
        xyt = tinv.transform(xy_vals)
        xt, yt = xyt[:, 0], xyt[:, 1]
        d = np.hypot(xt - event.x, yt - event.y)
        indseq, = np.nonzero(d == d.min())
        ind = indseq[0]

        #print(d[ind])
        if d[ind] >= epsilon:
            ind = None
        
        #print(ind)
        return ind

    def motion_notify_callback(event):
        'on mouse movement'
        global yvals
        if pind is None:
            return
        if event.inaxes is None:
            return
        if event.button != 1:
            return
        
        #update yvals
        #print('motion x: {0}; y: {1}'.format(event.xdata,event.ydata))
        yvals[pind] = event.ydata 

        # update curve via sliders and draw
        sliders[pind].set_val(yvals[pind])
        fig.canvas.draw_idle()

    #ax1.plot (x, y_original, 'k--', label='original', alpha=0.2)
    l, = ax1.plot (x,yvals,color='k',linestyle='-',marker='o',markersize=8)




    ax1.set_yscale('linear')
    ax1.set_xlim(0, N)
    ax1.set_ylim(0,100)
    ax1.set_xlabel('x')
    ax1.set_ylabel('y')
    ax1.grid(True)
    ax1.yaxis.grid(True,which='minor',linestyle='--')
    ax1.legend(loc=2,prop={'size':22})

    sliders = []

    for i in np.arange(N):

        axamp = plt.axes([0.84, 0.8-(i*0.05), 0.12, 0.02])
        # Slider
        s = Slider(axamp, 'p{0}'.format(i), 0, 100, valinit=yvals[i])
        sliders.append(s)

        
    for i in np.arange(N):
        #samp.on_changed(update_slider)
        sliders[i].on_changed(update)

    axres = plt.axes([0.84, 0.8-((N)*0.05), 0.12, 0.02])
    bres = Button(axres, 'Reset')
    bres.on_clicked(reset)

    axres = plt.axes([0.84, 0.7-((N)*0.05), 0.12, 0.02])
    bres = Button(axres, 'Export')
    bres.on_clicked(export)

    fig.canvas.mpl_connect('button_press_event', button_press_callback)
    fig.canvas.mpl_connect('button_release_event', button_release_callback)
    fig.canvas.mpl_connect('motion_notify_event', motion_notify_callback)

    plt.show()





brand_list = df.Brand.unique()
interactive_plot = interactive(brand_selection, brand =brand_list)
output = interactive_plot.children[-1]
output.layout.height = '950px'
interactive_plot

Moreover, here is the df dataframe I used for the test:

import pandas as pd

data = {'Time': [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2], 
        'Brand': ['CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'DG', 'DG', 'DG', 'DG', 'DG', 'DG', 'DG', 'DG', 'DG', 'CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'CH', 'DG', 'DG', 'DG', 'DG', 'DG', 'DG', 'DG', 'DG', 'DG'], 
        'Label': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6], 
        'Qty': [50, 86, 90, 78, 2, 66, 17, 79, 0, 40, 9, 84, 68, 30, 39, 31, 33, 53, 58, 22, 88, 55, 46, 49, 21, 69, 86, 66, 91, 44, 72, 63, 1, 94, 79, 95, 32, 30, 91, 16, 57, 44]}

df = pd.DataFrame(data)

The initial output of this code is not so bad. But:

  1. The points are not draggable
  2. The buttons are useless. They do not work

Moreover, whenever I try to change the values via the sliders I get this error:

NameError                                 Traceback (most recent call last)
File c:\Users\mottad\AppData\Local\Programs\Python\Python39\lib\site-packages\ipympl\backend_nbagg.py:279, in Canvas._handle_message(self, object, content, buffers)
    276     self.manager.handle_json(content)
    278 else:
--> 279     self.manager.handle_json(content)

File ~\AppData\Roaming\Python\Python39\site-packages\matplotlib\backends\backend_webagg_core.py:462, in FigureManagerWebAgg.handle_json(self, content)
    461 def handle_json(self, content):
--> 462     self.canvas.handle_event(content)

File ~\AppData\Roaming\Python\Python39\site-packages\matplotlib\backends\backend_webagg_core.py:266, in FigureCanvasWebAggCore.handle_event(self, event)
    263 e_type = event['type']
    264 handler = getattr(self, 'handle_{0}'.format(e_type),
    265                   self.handle_unknown_event)
--> 266 return handler(event)

File ~\AppData\Roaming\Python\Python39\site-packages\matplotlib\backends\backend_webagg_core.py:296, in FigureCanvasWebAggCore._handle_mouse(self, event)
    294 guiEvent = event.get('guiEvent')
    295 if e_type in ['button_press', 'button_release']:
--> 296     MouseEvent(e_type + '_event', self, x, y, button,
    297                modifiers=modifiers, guiEvent=guiEvent)._process()
    298 elif e_type == 'dblclick':
    299     MouseEvent('button_press_event', self, x, y, button, dblclick=True,
    300                modifiers=modifiers, guiEvent=guiEvent)._process()
...
---> 24     yvals[i] = sliders[i].val 
     25 l.set_ydata(yvals)
     26 # redraw canvas while idle

NameError: name 'yvals' is not defined

I did not understand why it tells me that the yvals is not defined.

Question: Could someone who has faced a similar problem help me to fix and improve the code? I am open to new solutions that I can take advantage of other packages such as Dash

I'm new on topics like this, so any help will be grateful enter image description here

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Davide Motta
  • 133
  • 7

0 Answers0