How to unfocus (blur) a button

I have developed an app to tag curves.

In order to do it, I display the curve to tag inside a plotly graph, and some buttons (corresponding to each tag) below the plot. When the user press a button, the tag is saved in a file, and automatically the next curve is shown in the plot.

My problem is the following: when the user press a button, the button gets focused and the font and border colors transforms to red. But since the next curve is shown in the plot, the button still have the focus so the font and border are still red, despite the next curve is already shown. And this can cofuse the user since it seems that the previous tag is the one selected in the current curve (which is not the case and is confusing).

I would like some way to force the button to lose focus, once the next curve is shown. I can share the code if requested.

1 Like

The best way I know of to unfocus a button is to update the button’s key so that the whole button gets redrawn.

Here’s a simple example:

import streamlit as st

items = ["foo", "bar", "baz"]

if "num_to_show" not in st.session_state:
    st.session_state.num_to_show = 1

if "button_key" not in st.session_state:
    st.session_state.button_key = 1


def button_pressed():
    st.session_state.num_to_show += 1
    st.session_state.button_key += 1


for i in range(st.session_state.num_to_show)[:3]:
    st.write(items[i])

st.button("Add one", on_click=button_pressed, key=st.session_state.button_key)
2 Likes

Many thanks for your example, it does exactly what I need. However, I had no success in adapting your code to my app :frowning:

My app, when the buttons that I want to unfocus (or “redraw”) are clicked, they call a function named “label_data” which in turn calls a function named “next_data”, which somehow is not working as expected, and I cannot find why.

I attach here my working code, and also my attempt to “redraw” the button named “Normal”, in case you can take it a look.

Working code:

import os
import streamlit as st
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots

st.session_state["username"] = "user"

# Set paths
basedir = os.getcwd()
flow_data = pd.read_csv(os.path.join(basedir, 'data/Flow.csv'))
paw_data = pd.read_csv(os.path.join(basedir, 'data/Paw.csv'))
last_index_file = os.path.join(basedir, f'{st.session_state["username"]}_last_index.txt')
labels_file = os.path.join(basedir, f'{st.session_state["username"]}_labels.csv')

st.markdown("""
            <style>
                div[data-testid="column"] {
                    width: fit-content !important;
                    flex: unset;
                }
                div[data-testid="column"] * {
                    width: fit-content !important;
                }
            </style>
            """, unsafe_allow_html=True)

def main():
        
    def save_last_index(current_index, last_index_file):
        with open(last_index_file, 'w') as f:
            f.write(str(current_index))

    def load_last_index():
        if os.path.exists(last_index_file):
            with open(last_index_file, 'r') as f:
                return int(f.read())
        else:
            return 0 
          
    def previous_data(current_index):
        if current_index > 0:
            current_index -= 1
            save_last_index(current_index, last_index_file)  
            st.rerun()

    def next_data(current_index, flow_data):
        if current_index == len(flow_data)-1:
            current_index = len(flow_data)-1 
            st.rerun()
        elif current_index < len(flow_data)-1:
            current_index += 1
            save_last_index(current_index, last_index_file)     
            st.rerun()

    def plot_data(current_index,flow_data,paw_data):
        flow_data_to_plot = flow_data.iloc[current_index, 5:]
        paw_data_to_plot = paw_data.iloc[current_index, 5:]
        fig = make_subplots(rows=2, cols=1, shared_xaxes=True,vertical_spacing=0.05)
        fig.add_trace(go.Scatter(y=flow_data_to_plot, mode='lines', name='Flow [L/min]'), row=1, col=1)
        fig.add_trace(go.Scatter(y=paw_data_to_plot, mode='lines', name='Paw [cmH2O]', line=dict(color='red')), row=2, col=1)
        central_region_start = 60  
        central_region_end = (flow_data.iloc[current_index, 3] + flow_data.iloc[current_index, 4]) / 10 + central_region_start - 1
        fig.add_vrect(x0=central_region_start, x1=central_region_end, 
                    fillcolor='lightgreen', opacity=0.3, layer='below', line_width=0,
                    annotation_text='Breath to tag', annotation_position='top',row=1,col=1)
        fig.add_vrect(x0=central_region_start, x1=central_region_end, 
                    fillcolor='lightgreen', opacity=0.3, layer='below', line_width=0,row=2,col=1)
        fig.update_layout(height=520, width=630, showlegend=False, title=f"Breath index: {current_index}")
        fig.update_yaxes(title_text='Flow [L/min]', row=1, col=1)
        fig.update_yaxes(title_text='Paw [cmH2O]', row=2, col=1)
        fig.update_xaxes(title_text='Time [s]', row=2, col=1)
        xticks_short = [(x-60)/20 for x in list(range(0,180,10))]
        fig.update_xaxes(tickvals=[10*x for x in list(range(len(xticks_short)))],ticktext=xticks_short, row=2, col=1)
        st.plotly_chart(fig, config={'displaylogo': False})
          
    def label_data(label,current_index,flow_data,labels_file):
        df_labels = pd.DataFrame(columns=['id', 'label'])
        if os.path.exists(labels_file):
            df_labels = pd.read_csv(labels_file)
        df_labels = df_labels[df_labels['id'] != flow_data.iloc[current_index, 0]]
        new_row = pd.DataFrame({'id': [flow_data.iloc[current_index, 0]-1], 'label': [label]})
        df_labels = pd.concat([df_labels, new_row], ignore_index=True)
        df_labels.to_csv(labels_file, index=False)
        next_data(current_index, flow_data)  
    
    current_index = load_last_index()         
          
    # Sidebar
    st.sidebar.title(f"Welcome {st.session_state["username"]}")
    st.sidebar.caption(":gray[Label the breath by clicking one of the label buttons below the graph. When a label button is pressed, the next breath will be automatically displayed.]")
    st.sidebar.markdown("")
    st.sidebar.caption(":gray[Use the buttons below to display the previous or next breath, if necessary:]")
    if st.sidebar.button("Previous breath"):
        previous_data(current_index)
    if st.sidebar.button("Next breath"):
        next_data(current_index, flow_data)     
    st.sidebar.markdown("")   
    desired_breathindex = st.sidebar.text_input(f":gray[Otherwise, write the breath index to display (integer between 0 and {len(flow_data)-1}):]")
    if desired_breathindex:
        if not desired_breathindex.isnumeric():
            st.sidebar.write(":red[Not a valid input, please provide a valid index value.]") 
        elif ((desired_breathindex.isnumeric()) & (int(desired_breathindex) >=0) & (int(desired_breathindex) < len(flow_data))):
            current_index = int(desired_breathindex)
            save_last_index(current_index, last_index_file) 
        else:
            st.sidebar.write(":red[Not a valid input, please provide a valid index value.]")   

    plot_data(current_index, flow_data, paw_data)
    
    # Labels buttons
    colb1, colb2, colb3, colb4, colb5, colb6, colb7, colb8, colb9 = st.columns([1,1,1,1,1,1,1,1,1])
    if colb1.button("Normal"):
        label_data("Normal",current_index,flow_data,labels_file)
    if colb2.button("DT"):
        label_data("DT",current_index,flow_data,labels_file)
    if colb3.button("IE"):
        label_data("IE",current_index,flow_data,labels_file)
    if colb4.button("SC"):
        label_data("SC",current_index,flow_data,labels_file)
    if colb5.button("PC"):
        label_data("PC",current_index,flow_data,labels_file)
    if colb6.button("RT-DT"):
        label_data("RT-DT",current_index,flow_data,labels_file)
    if colb7.button("RTinsp"):
        label_data("RTinsp",current_index,flow_data,labels_file)
    if colb8.button("RTexp"):
        label_data("RTexp",current_index,flow_data,labels_file)
    if colb9.button("Others"):
        label_data("Others",current_index,flow_data,labels_file)

if __name__ == "__main__":
    main()

Attempt to redraw the “Normal” button:

...
    
    # Labels buttons
    colb1, colb2, colb3, colb4, colb5, colb6, colb7, colb8, colb9 = st.columns([1,1,1,1,1,1,1,1,1])
    if colb1.button("Normal", onclick=button_pressed, key=st.session_state.button_keyb1):
        label_data("Normal",current_index,flow_data,labels_file)
    if colb2.button("DT"):
        label_data("DT",current_index,flow_data,labels_file)
    if colb3.button("IE"):
        label_data("IE",current_index,flow_data,labels_file)
    if colb4.button("SC"):
        label_data("SC",current_index,flow_data,labels_file)
    if colb5.button("PC"):
        label_data("PC",current_index,flow_data,labels_file)
    if colb6.button("RT-DT"):
        label_data("RT-DT",current_index,flow_data,labels_file)
    if colb7.button("RTinsp"):
        label_data("RTinsp",current_index,flow_data,labels_file)
    if colb8.button("RTexp"):
        label_data("RTexp",current_index,flow_data,labels_file)
    if colb9.button("Others"):
        label_data("Others",current_index,flow_data,labels_file)

if "button_keyb1" not in st.session_state:
    st.session_state.button_keyb1 = 1
def button_pressed():
    st.session_state.button_keyb1 += 1

if __name__ == "__main__":
    main()
1 Like

There is a lot going on in that script, so it’s difficult to give a complete answer – in the future, both for your own debugging, and to help others on the forum provide better help, try and slim down your code to the bare essentials that still shows the issue, and is easy for others to run on their own.

I think the problem comes down to the fact that you’re effectively tracking button presses two different ways: with an on_click function, and with if button()...

The problem is that if you change the key, it’s effectively a new button, and since the on_click will run first, the old button will never be counted as clicked, so the code inside the “if” will never run.

For example:

import streamlit as st

if "button_keyb1" not in st.session_state:
    st.session_state.button_keyb1 = 1


def button_pressed():
    st.session_state.button_keyb1 += 1


def label_data():
    st.write("Label data")
    next_data()


def next_data():
    st.write("Next data")


if st.button("Normal", on_click=button_pressed, key=st.session_state.button_keyb1):
    label_data()

If you try this, you’ll never see “Label data” or “next data” printed.

However, if you use the on_click to call label_data(), it works:

import streamlit as st

if "button_keyb1" not in st.session_state:
    st.session_state.button_keyb1 = 1


def button_pressed():
    st.session_state.button_keyb1 += 1
    label_data()


def label_data():
    st.write("Label data")
    next_data()


def next_data():
    st.write("Next data")


st.button("Normal", on_click=button_pressed, key=st.session_state.button_keyb1)

So, I would put anything you want to happen on that “Normal” button inside the on_click function.

If you want to use it for multiple buttons, you can pass args to the button_pressed function like this:

import streamlit as st

if "button_keyb1" not in st.session_state:
    st.session_state.button_keyb1 = 1


def button_pressed(button_name, values):
    st.session_state.button_keyb1 += 1
    label_data(button_name, values)


def label_data(button_name, values):
    st.write("Label data")
    st.write(f"Button name: {button_name}")
    st.write(f"Values: {values}")
    next_data()


def next_data():
    st.write("Next data")


st.button(
    "Normal",
    on_click=button_pressed,
    key=st.session_state.button_keyb1,
    args=("Normal", "values"),
)
2 Likes

Many many thanks for your detailed response and for spending your time taking a look at my code! You deserve a big thank you!!

Again, your examples work great, in my case I think I have a couple of pieces missing since when I press the button(s) --that I want to blur–, those call the label_data() function, that in turn call the next_data() function that call the st.rerun() function, and I get a warning saying that calling st.rerun() from a callback is a no-op. Anyway, even if I manage to avoid this st.rerun(), I get the error that two buttons share the same key (=1), and I don’t know how to avoid this.

if "button_b1" not in st.session_state:
    st.session_state.button_b1 = 1
if "button_b2" not in st.session_state:
    st.session_state.button_b2 = 1
    
def buttonb1_pressed():
    st.session_state.button_b1 += 1
    label_data("Normal",current_index,flow_data,labels_file)
def buttonb2_pressed():
    st.session_state.button_b2 += 1
    label_data("DT",current_index,flow_data,labels_file)

st.button("Normal",on_click=buttonb1_pressed,key=st.session_state.button_b1)
st.button("DT",on_click=buttonb2_pressed,key=st.session_state.button_b2)

But don’t worry, I think will give up and I will leave it as it was, you already spent too much of your time and my problem is that I should read more carefully the documentation. Thank you!

1 Like

As suggested by the error message, if you’re using a callback (e.g. on_click=function), you don’t need to do st.rerun – it always reruns the whole script after it runs the callback function. So, you can just safely remove it.

An easy trick for the “buttons have the same key” is just do something like this:

st.button("Normal",on_click=buttonb1_pressed,key=f"normal_button_{st.session_state.button_b1}")

Do that differently for each button, so they each have a different prefix, and that should resolve that problem.

2 Likes

Great!!! Now it works perfectly!! Many many thanks for your time and patience!!! :slight_smile:

2 Likes

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.