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:
- The points are not draggable
- 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