Make Streamlit table results hyperlinks or add radio buttons to table?

I have a large data set that I’m filtering through. After the user chooses the filters they want and clicks submit, I have a table that displays results (similar to the image below).

I want to make each entry in the “Link Number” column clickable. Is there a way to easily make the items in the list clickable with hyperlinks? Or does the hyperlink text need to just be included in the original dataframe?

Or as another option, is there a way to show radio buttons for each line entry in a table? If so, is there a way to use that radio button to grab the text from the “Line Number” column then use that text in the URL?

1 Like

Hey @srog,

I dont think its easily doable with st.table but you sure can do it with bokeh.

import streamlit as st
import pandas as pd
from bokeh.plotting import figure

from bokeh.models import ColumnDataSource, CustomJS
from bokeh.models import DataTable, TableColumn, HTMLTemplateFormatter


df = pd.DataFrame({
        "links": ["https://www.google.com", "https://streamlit.io", "https://outlook.com"],
})
cds = ColumnDataSource(df)
columns = [
TableColumn(field="links", title="links", formatter=HTMLTemplateFormatter(template='<a href="<%= value %>"target="_blank"><%= value %>')),
]
p = DataTable(source=cds, columns=columns, css_classes=["my_table"])
st.bokeh_chart(p)

Now if you want the text from the line number you need use a custom component, streamlit-bokeh-events something like this,

import streamlit as st
import pandas as pd
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.models import DataTable, TableColumn, HTMLTemplateFormatter
from streamlit_bokeh_events import streamlit_bokeh_events


df = pd.DataFrame({
        "links": ["https://www.google.com", "https://streamlit.io", "https://outlokk"],
})
# create plot
cds = ColumnDataSource(df)
columns = [
TableColumn(field="links", title="links", formatter=HTMLTemplateFormatter(template='<a href="<%= value %>"target="_blank"><%= value %>')),
]

# define events
cds.selected.js_on_change(
"indices",
CustomJS(
        args=dict(source=cds),
        code="""
        document.dispatchEvent(
        new CustomEvent("INDEX_SELECT", {detail: {data: source.selected.indices}})
        )
        """
)
)
p = DataTable(source=cds, columns=columns, css_classes=["my_table"])
result = streamlit_bokeh_events(bokeh_plot=p, events="INDEX_SELECT", key="foo", refresh_on_update=False, debounce_time=0, override_height=100)
if result:
        if result.get("INDEX_SELECT"):
                st.write(df.iloc[result.get("INDEX_SELECT")["data"]])

This will work look like this,

Hope it helps !

4 Likes

Hey! I had a similar problem - to add an action button to a table.
I came to the following approach:

import streamlit as st

        # # Show user table 
        colms = st.columns((1, 2, 2, 1, 1))
        fields = ["№", 'email', 'uid', 'verified', "action"]
        for col, field_name in zip(colms, fields):
            # header
            col.write(field_name)

        for x, email in enumerate(user_table['email']):
            col1, col2, col3, col4, col5 = st.columns((1, 2, 2, 1, 1))
            col1.write(x)  # index
            col2.write(user_table['email'][x])  # email
            col3.write(user_table['uid'][x])  # unique ID
            col4.write(user_table['verified'][x])   # email status
            disable_status = user_table['disabled'][x]  # flexible type of button
            button_type = "Unblock" if disable_status else "Block"
            button_phold = col5.empty()  # create a placeholder
            do_action = button_phold.button(button_type, key=x)
            if do_action:
                 pass # do some action with a row's data
                 button_phold.empty()  #  remove button

And it works. user_table — a dictionary very similar to DataFrame, where each key — is a column.
And here how it looks like (Note “Blocked” — that is the result of action):

4 Likes

I want to thank Constantine_Kurbatov for his sharing

I tried to create the following code :

import streamlit as st

# Show users table

colms = st.columns((1, 2, 2, 1, 1))
fields = [“№”, ‘email’, ‘uid’, ‘verified’, ‘action’]
for col, field_name in zip(colms, fields):
# header
col.write(field_name)

user_table = {
‘№’ : [1,2,3],
‘email’ : [“aa@gmail.fr,"bb@gmail.fr","cc@gmail.fr"],
‘uid’ : [1,2,3],
‘verified’ : [“yes”,“no”,“yes/no”],
‘disabled’ : [“hh”,“pp”,‘kak’]
}

for x, email in enumerate(user_table[‘email’]):
col1, col2, col3, col4, col5 = st.columns((1, 2, 2, 1, 1))
col1.write(x) # index
col2.write(user_table[‘email’]) # email
col3.write(user_table[‘uid’]) # unique ID
col4.write(user_table[‘verified’]) # email status
disable_status = user_table[‘disabled’] # flexible type of button
button_type = “Unblock” if disable_status else “Block”
button_phold = col5.empty() # create a placeholder
do_action = button_phold.button(button_type, key=x)
if do_action:
pass # do some action with row’s data
button_phold.write(“Blocked”)

what I want is if I click on Block button , I get an Unblock button and I want this for EACH Row

Thanks for your help.

Ah, ok.
The problem is in the logic behind the streamlit processing.
You may replace the pressed button instead of text like here:
button_phold.write(“Blocked”)
with something like:
do_another_action = button_phold.button(inverted_button_type, key=x)
but you cannot repeat the following part of the code within the same routing without complete rerun:
pass # do some action with row’s data

In my case — if the button is pressed, the button state is updated in the user_table list. And in the next run, the button will be shown as “Unlock” (you may see that on the original screenshot) that will invert state back if pressed.

So there is no way without callbacks to do so with streamlit.
BUT
the recent versions have this callback function.

So you may put the action/button invert part into a callback function, and it would work, but I didn’t test it yet.

I will post an update when will try it.

Constantine.

Hello ,

Constantine , can I have your email please in the private message to send you a file so that you can see properly what happens and what I want.

Thank you very much

Hi,

Awesome code! Thanks a lot.

FYI: you have forgotten to close the <a> tag in the HTML Formatter; it should be something lik:

template='<a href="<%= value %>"target="_blank"><%= value %></a>'

Hi, I agree with the others that the code is awesome! Thanks all of you. :partying_face:
I have a specific issue with the row opening mechanics demonstrated in the following video:
streamlit-app-2022-11-03-21-11-41

I guess this is because the selectbox triggers an execution of the script and thus resets the status of the button? MWE:

import streamlit as st

cols   = st.columns(2)
fields = ["id", "content"]

# header
for col, field in zip(cols, fields):
	col.write("**"+field+"**")

# rows
for idx, row in zip([1,2,3],["test1", "test2", "test3"]):
	
	col1, col2 = st.columns(2)
	col1.write(str(idx))
	
	placeholder = col2.empty()
	show_more   = placeholder.button("more", key=idx, type="primary")

	# if button pressed
	if show_more:

		# rename button
		placeholder.button("less", key=str(idx)+"_")
		
		# do stuff
		st.write("This is some more stuff with a checkbox")
		temp = st.selectbox("Select one", ["A", "B", "C"])
		st.write("You picked ", temp)
		st.write("---")

Does someone have an idea how to stay within a selected row even when there is a new widget?

Thank you very much.

1 Like

Yes, so you need to keep your choices in session_state. But there is more.

Once you click the button, the script is rerun, and only when you instantiate the same button again will its return value become True. So if you update the state by assigning the return value of the button, you may (and, in this case, you will) be a rerun late. You need to use callbacks to update the state and choose which button to draw based on state, not the return value of some other button.

import streamlit as st


def on_more_click(show_more, idx):
    show_more[idx] = True


def on_less_click(show_more, idx):
    show_more[idx] = False


if "show_more" not in st.session_state:
    st.session_state["show_more"] = dict.fromkeys([1, 2, 3], False)
show_more = st.session_state["show_more"]

cols = st.columns(2)
fields = ["id", "content"]

# header
for col, field in zip(cols, fields):
    col.write("**" + field + "**")

# rows
for idx, row in zip([1, 2, 3], ["test1", "test2", "test3"]):

    col1, col2 = st.columns(2)
    col1.write(str(idx))
    placeholder = col2.empty()

    if show_more[idx]:
        placeholder.button(
            "less", key=str(idx) + "_", on_click=on_less_click, args=[show_more, idx]
        )

        # do stuff
        st.write("This is some more stuff with a checkbox")
        temp = st.selectbox("Select one", ["A", "B", "C"], key=idx)
        st.write("You picked ", temp)
        st.write("---")
    else:
        placeholder.button(
            "more",
            key=idx,
            on_click=on_more_click,
            args=[show_more, idx],
            type="primary",
        )
2 Likes

Crazy! Thanks for the explanation and the super fast reply.
This should work. :partying_face:

As you can see from my previous video, I have a dropdown before the self-generated table. Now, this changes the number of rows and their respective indices, so it wouldn’t work if you changed the radio button.

I implemented this rather “hacky” solution with a radio-memo list in the cache that triggers a reset of the cached show_more

import streamlit as st

radio = st.radio("Number", [1,2])

# radio_mem will be a list storing the values of the radio button
if "radio_mem" not in st.session_state:
	st.session_state["radio_mem"] = [radio]
else:
	st.session_state["radio_mem"].append(radio)

refs = {
	1: ["a", "b", "c"],
	2: ["d", "e", "f", "g"]
}

if "refs" not in st.session_state:
	st.session_state["refs"] = refs[radio]

# when there is more than one value and the last is not the same
# as the penultimate, then reset_session state variable
if len(st.session_state["radio_mem"]) > 1:
	if st.session_state["radio_mem"][-1] != st.session_state["radio_mem"][-2]:
		st.session_state["refs"] = refs[radio]

# shorten list when there are more than three items
if len(st.session_state["radio_mem"]) > 3:
	st.session_state["radio_mem"][:2] = []

Maybe this helps someone, maybe there is a smarter solution :slight_smile:

1 Like

Great solution for table with expandable rows. @Goyo, with your solution, how can I expand two rows together? In my application, I am using idx in "do stuff " section to do some calculations.
ezgif.com-video-to-gif

My answer was about state management, everything else was @victor-234’s code. I don’t understand what you mean by “table with expandable rows” or “expand two rows toguether” and the animated gif does not help.

Maybe you are talking about the bug in my code, where an error occurs if you click two “More” buttons?

PD. I edited my answer to fix the bug.

Yes, when I click two “more” button it gives error. I want to keep open “more” section for both of the rows.

In my code that was because each click creates a new selectbox, but all of them had the same parameters and you cannot create two identical widgets. The fix was adding a unique key to the selectbox.

I have updated the code, it is the same as before except for the key in the selectbox.

Yes, that was the mistake. Now it’s working perfectly! Thanks for the help!

@Goyo I was looking for same solution. Can you please share the full code for the reference.

All the code I have ever written in relation to this is in my answer above. What do you think is missing?