Ipywidgets and plotly interactions

I’m trying to make an interactive plot with ipywidgets using plotly, but I’m afraid i’m not getting something. I have some dataframe with coordinates and some columns. I’d want to plot the dataframe in a scatterplot so that coord1=x, coord2=y and each marker point is colored by the value of a column selected by a column selected interactively.

Additionally I’d want that when I change the column value with the interactive menu, the color for every point changes to the column that i selected, rescaling the min and max of the colorbar accordingly to the min and max of the new column. Furthermore, when I change another selector (selector2) then i want the plot to display only the subset of mu dataframe that matched a certain colID big_grid[big_grid[“id_col”]==selector2.value]. Lastly there should be a rangeslider widget to adjust the color range of the colorbar

so by now i have this:

big_grid=pd.DataFrame(data=dict(id_col=[1,2,3,4,5],
                         col1=[0.1,0.2,0.3,0.4,0.5],
                         col2=[10,20,30,40,50],
                         coord1=[6,7,8,9,10],
                         coord2=[6,7,8,9,10]))
list_elem=["col1","col2"]
list_id=big_grid.id_col.values

dropm_elem=widgets.Dropdown(options=list(list_elem)) 
dropm_id=widgets.SelectMultiple(
                options=list_id,
                description="Active",
                disabled=False
) 

rangewidg=widgets.FloatRangeSlider(value=[big_grid[dropm_elem.value].min(),big_grid[dropm_elem.value].max()],
                               min=big_grid[dropm_elem.value].min(),
                               max=big_grid[dropm_elem.value].max(),
                               step=0.001,
                               readout_format='.3f',
                               description="Color Range",
                              continuous_update=False)


fig = go.FigureWidget(data=px.scatter(big_grid, 
                                      x="coord1",
                                      y="coord2",
                                      color=big_grid[dropm_elem.value],
                                      color_continuous_scale="Turbo",)
                         )
def handle_id_change(change):
    fig.data[0]['x']=big_grid[big_grid['id_col'].isin(dropm_id.value)]["coord1"]
    fig.data[0]['y']=big_grid[big_grid['id_col'].isin(dropm_id.value)]["coord2"]
    fig.data[0]['marker']['color']=big_grid[big_grid['id_col'].isin(dropm_id.value)][dropm_elem.value]
    fig.data[0]['marker']['cmin']=big_grid[big_grid['id_col'].isin(dropm_id.value)][dropm_elem.value].min()
    fig.data[0]['marker']['cmax']=big_grid[big_grid['id_col'].isin(dropm_id.value)][dropm_elem.value].max()
    
def handle_elem_change(change):
    fig.data[0]['marker']['color']=big_grid[big_grid['id_col'].isin(dropm_id.value)][dropm_elem.value]   
   
dropm_elem.observe(handle_elem_change,names='value')
dropm_id.observe(handle_id_change,names='value')


right_box1 =widgets.HBox([fig])
right_box2=widgets.VBox([dropm_elem,dropm_id,rangewidg])
box=widgets.HBox([right_box1,right_box2])
box

So, the selection of the subset (from dropm_id) works, but the rangewidget and the hovering are broken. Basically when i change dromp_elem the color doesn’t adjust as i am expecting, and instead it gets dark and uniform. At the same time if you change column and you hover over the points it lists the value of col2, but the label still says col1.

I’m afraid that I’m overcomplicating my life and there is surely an easier way, could someone enlighten me?

If I instead use a different approach and I use a global variable to define the subset to plot, a plotting function and a the widget.interact function I can make it work. The problem is that in this case the plot is not a widget, so i cannot put it into a VBox or HBox. It also still feels wrong and using global variables is not grood practice. I’ll provide the code anyway for reference:

def plot(elem,rang):
    fig = px.scatter(subset, x="coord1", y="coord2", color=elem,color_continuous_scale="Turbo",range_color=rang)
    fig.show()

def handle_elem_change(change):
    with rangewidg.hold_trait_notifications():    #This is because if you do't put it it set max, 

        rangewidg.max=big_grid[dropm_elem.value].max() #and if max is < min he freaks out. Like this he first
        rangewidg.min=big_grid[dropm_elem.value].min() #set everything and then send the eventual errors notification.
        rangewidg.value=[big_grid[dropm_elem.value].min(),big_grid[dropm_elem.value].max()]

def handle_id_change(change):
    global subset
    subset=big_grid[big_grid['id_col'].isin(dropm_id.value)]

big_grid=pd.DataFrame(data=dict(id_col=[1,2,3,4,5],
                         col1=[0.1,0.2,0.3,0.4,0.5],
                         col2=[10,20,30,40,50],
                         coord1=[6,7,8,9,10],
                         coord2=[6,7,8,9,10]))
subset=big_grid
list_elem=["col1","col2"]
list_id=big_grid.id_col.values

dropm_elem=widgets.Dropdown(options=list(list_elem)) 
dropm_id=widgets.SelectMultiple(
                options=list_id,
                description="Active",
                disabled=False
) 

rangewidg=widgets.FloatRangeSlider(value=[big_grid[dropm_elem.value].min(),big_grid[dropm_elem.value].max()],
                               min=big_grid[dropm_elem.value].min(),
                               max=big_grid[dropm_elem.value].max(),
                               step=0.001,
                               readout_format='.3f',
                               description="Color Range",
                              continuous_update=False)


dropm_elem.observe(handle_elem_change,names='value')
dropm_id.observe(handle_id_change,names='value')

display(dropm_id)
widgets.interact(plot,elem=dropm_elem,rang=rangewidg) 

So, I would want the behaviour of this second code, but in a widget.Hbox, ans possibly without using global variables. Is it possible?

Yes.

First, the globals:
You can always avoid using globals by passing in the variable in the call to the function by including it as a parameter in the function definition. However, I wouldn’t stress about this. Get the plot working. I think I found code that worked for a file upload and used globals and I just left it because it works, see here.

On putting it in a Hbox:
You should assign widgets.interact(plot,elem=dropm_elem,rang=rangewidg) to a variable and then place that within the list of children of your Hbox. Or do similar to how I did in the same example I linked to above, I use above and put that within the widgets.Output() and then place what in your Hbox.

By the way, you can run that example notebook (or as a Voila app) actively by going here and clicking the launch binder badge.

Thank you for the reply, I found that the documentation for ipywidgets is not as rich as I would like it and every practical example helps a lot. I eventually came around it using this code:

def handle_elem_change(change):
    with rangewidg.hold_trait_notifications():    #This is because if you do't put it it set max, 
        
        rangewidg.max=big_grid[dropm_elem.value].max() #and if max is < min he freaks out. Like this he first
        rangewidg.min=big_grid[dropm_elem.value].min() #set everything and then send the eventual errors notification.
        rangewidg.value=[big_grid[dropm_elem.value].min(),big_grid[dropm_elem.value].max()]

    
def plot_change(change):
    df=big_grid[big_grid['id_col'].isin(dropm_id.value)]
    output.clear_output(wait=True)
    with output:
            fig = px.scatter(df, x="coord1", y="coord2", color=dropm_elem.value,hover_data=["info"],
                 width=500,height=800, color_continuous_scale="Turbo",range_color=rangewidg.value)
            fig.show()
    
    
#define the widgets dropm_elem and rangewidg, which are the possible df.columns and the color range
#used in the function plot.
big_grid=pd.DataFrame(data=dict(id_col=[1,2,3,4,5],
                         col1=[0.1,0.2,0.3,0.4,0.5],
                         col2=[10,20,30,40,50],
                         coord1=[6,7,8,9,10],
                         coord2=[6,7,8,9,10],
                         info=["info1","info2","info3","info4","info5",]))
list_elem=["col1","col2","info"]
list_id=big_grid.id_col.values


dropm_elem=widgets.Dropdown(options=list_elem) #creates a widget dropdown with all the _ppms
dropm_id=widgets.SelectMultiple(
                options=list_id,
                description="Active Jobs",
                disabled=False
) 

rangewidg=widgets.FloatRangeSlider(value=[big_grid[dropm_elem.value].min(),big_grid[dropm_elem.value].max()],
                                   min=big_grid[dropm_elem.value].min(),
                                   max=big_grid[dropm_elem.value].max(),
                                   step=0.001,
                                   readout_format='.3f',
                                   description="Color Scale Range",
                                  continuous_update=False)
output=widgets.Output()
# this line is crucial, it basically says: Whenever you move the dropdown menu widget, call the function
# #handle_elem_change, which will in turn update the values of rangewidg
dropm_elem.observe(handle_elem_change,names='value')
dropm_elem.observe(plot_change,names='value')
dropm_id.observe(plot_change,names='value')
rangewidg.observe(plot_change,names='value')

# # #this line is also crucial, it links the widgets dropmenu and rangewidg with the function plot, assigning
# # #to elem and to rang (parameters of function plot) the values of dropmenu and rangewidg

left_box = widgets.VBox([output])
right_box =widgets.VBox([dropm_elem,rangewidg,dropm_id])
tbox=widgets.HBox([left_box,right_box]) 
# widgets.interact(plot,elem=dropm_elem,rang=rangewidg) 

display(tbox)

I will definitely try to muck around with putting interact in a variable and then in a HBox. In the meanwhile I’m trying to make it so that I can upload files and plot the files that I upload in the same Cell. In your example you solve this using a Button and basically reading the files once you press the “plot” button, so that nothing crashes. Is there a way to instead make it so that when the user select the files then the rest of the cell is run?

I tried something like :

while(not(df_uploader.value)):
    print("Select_files")
    time.sleep(5)
    print((df_uploader.value))

But it doesn’t work, it seems that it never manages to read the file. Today maybe I’ll try with threading but not sure it’s the way. I am sure there must be a standard way to handle this kind of problems, but can’t find any documentation on it.

1 Like

I don’t know. There probably is but I wanted the user to commit to that as the input for what would follow.
If you find a way to do that, please post it back here though.