Retaining changes for Streamlit data editor when hiding or switching between widgets/pages
Asked Answered
S

1

5

I created this simple python example below. I used streamlit together with pandas. This example has an editable dataframe in each selectbox "A" and "B". When I hit the selectbox "A", and edit the table: for example, add a new row as "a4" and "4" as value, then hit the selectbox "B" and come back to selectbox "A", the df1 goes back to original dataframe because the whole funct1 is rerun from the start. How can the edited dataframe information be stored so the edited dataframe infromation wouldn't be lost? I don't want to @st.cache_data it as I want the dataframe to be editable continuously.

import streamlit as st
import pandas as pd

page = st.sidebar.selectbox("Select: ", ("A","B"))

### Added code - but this doesn't work:
st.session_state['page'] = page
selected_app_mode = st.session_state.get('page')
app_mode_ix = 0
if selected_app_mode: 
    app_mode_ix = page.index(selected_app_mode)
page = st.sidebar.selectbox(page, index=app_mode_ix)
st.session_state['page'] = page
### End of added code

def funct1():
    df1 = pd.DataFrame({"col1": ["a1", "a2", "a3"], 
                       "Values": [1, 2, 3]})
    edited_df1 = st.experimental_data_editor(df1, num_rows="dynamic")
    return df1

def funct2():
    df2 = pd.DataFrame({"col1": ["b1", "b2", "b3"], 
                       "Values": [1, 2, 3]})
    edited_df1 = st.experimental_data_editor(df2, num_rows="dynamic")
    return df2

if  page == "A":
    funct1()
elif page == "B":
    funct2()

What I got (if I remove the added code):

df1
a1  1
a2  2
a3  3

Expected to get:

df1
a1  1
a2  2
a3  3
a4  4
Scheel answered 27/5, 2023 at 0:15 Comment(1)
(experimental_)data_editor is difficult to use. Maybe this link will give you some clues, the way using st.rerun() might help you come up with a relatively simple solution. github.com/streamlit/streamlit/issues/…Edp
B
6

Comments in the code below.

Something to keep in mind

The data editor is a little different than other widgets; you can't "store" its state directly. However, widgets lose their information when they disappear from the screen. This creates a problem.

For other widgets, you can save their value in session state (assigned to a different key than the widget's key) to keep their information while they are not displayed. When the widget comes back, you can assign it its previous state directly. However, because the data editor is the way it is, you can't directly save and assign its state. The best you can do is save the result of edits and then initialize a new editor that starts out where the previous one left off.

A caution

You don't want to feed a dataframe's edited result back into itself in real time. This will not work:

st.session_state.df = st.experimental_data_editor(st.session_state.df)

Such a pattern will cause the data editor to need each change entered twice to be reflected in the result. If an argument is changed in the creation of a widget, Streamlit thinks its a brand new widget and throws away any retained "memory" it had.

The solution

For each "page" you need to have two dataframes saved in session state: an original and an edited version. While on a page, you have a data editor based on the original and it saves the edited result directly into session state with each edit the user makes. When the page is changed, the edited version in session state is copied and overwrites the original one. Thus, when you return to the page, the data editor will start off where the last edit ended.

import streamlit as st
import pandas as pd

# Initialize session state with dataframes
# Include initialization of "edited" slots by copying originals
if 'df1' not in st.session_state:
    st.session_state.df1 = pd.DataFrame({
        "col1": ["a1", "a2", "a3"],
        "Values": [1, 2, 3]
    })
    st.session_state.edited_df1 = st.session_state.df1.copy()
    st.session_state.df2 = pd.DataFrame({
        "col1": ["b1", "b2", "b3"], 
        "Values": [1, 2, 3]
    })
    st.session_state.edited_df2 = st.session_state.df2.copy()

# Save edits by copying edited dataframes to "original" slots in session state
def save_edits():
    st.session_state.df1 = st.session_state.edited_df1.copy()
    st.session_state.df2 = st.session_state.edited_df2.copy()

# Sidebar to select page and commit changes upon selection
page = st.sidebar.selectbox("Select: ", ("A","B"), on_change=save_edits)

# Convenient shorthand notation
df1 = st.session_state.df1
df2 = st.session_state.df2

# Page functions commit edits in real time to "editied" slots in session state
def funct1():
    st.session_state.edited_df1 = st.experimental_data_editor(df1, num_rows="dynamic")
    return

def funct2():
    st.session_state.edited_df2 = st.experimental_data_editor(df2, num_rows="dynamic")
    return

if  page == "A":
    st.header("Page A")
    funct1()
elif page == "B":
    st.header("Page B")
    funct2()

PS. Strictly speaking, you can get away without the .copy() methods since the data editor is not performing any modification in place to the dataframe it's given. I just left them in as a kind of conceptual nod.

Edit: Further detailed explanation of the code per comment below

There are two pieces to focus on in the script:

page = st.sidebar.selectbox("Select: ", ("A","B"), on_change=save_edits)

and for each dataframe:

st.session_state.edited_df1 = st.experimental_data_editor(df1, num_rows="dynamic")

Say you have a page displaying the data for df1 for the user. If the user is editing the dataframe, then withe each edit:

  1. User makes an edit
  2. The value of the widget in session state is updated (we didn't use a manually assigned key, so you can't see this)
  3. The page reloads
  4. When the script gets to the widget again, it outputs the new state
  5. This new output is saved to the edited_df1 key in session state.
  6. Repeat 1-5 for however many edits the user does.
  7. User changes to df2
  8. on_change=save_edits executes before the new page load, hence st.session_state.edited_df1 is copied to st.session_state.df1 (same for df2 but it's trivial since they are the same)
  9. Page reloads
  10. User sees df2
  11. Let's say the user immediately switches back to df1
  12. Now the user sees the edited df1 because st.session_state.df1 was overwritten with the edited version when the user left that page/view
Bickering answered 27/5, 2023 at 3:8 Comment(16)
Thanks for you reply @MathCatsAnd. I have a few followup questions. Could you explain what this part is doing: '# Convenient shorthand notation df1 = st.session_state.df1 df2 = st.session_state.df2' Also, if you could explain what this does: 'on_change=save_edits' in the selectbox?Scheel
The "convenient shorthand" isn't really "doing" anything other than saving a shorter variable name to access the data; st.experimental_data_editor(df1) is shorter than st.experimental_data_editor(st.session_state.df1) in a line of code with other things going on. The purpose of "on_change=save_edits" is to copy the edited df to overwrite the original df. When you return to the page, you have to create the widget from where the edits left off and having the original overwritten with the edited result is the way that is realized.Bickering
Great answers, thank you very much @MathCatsAnd. Do you know why 'experimental_data_editor' doesn't do the symbols and subscripts? When I do ' st.session_state.df1 = pd.DataFrame({ "col1": ["$a_1$", "$\alpha^{2}$", "$a^3$"], "Values": [1, 2, 3] }) ' this doesn't work.Scheel
Streamlit is going to treat strings as strings; it won't treat strings in dataframes as html, markdown, or latex to be rendered. Outside of the data editor, you could convert your whole dataframe to html to do a static rendering with the appropriate arguments, but that won't help you with the data editor.Bickering
Thanks for your explanations @MathCatsAnd. I am still a bit lost. I am trying to follow the code you sent. I know it works, but I don't know how. Some steps make sense, but I can't tell what's going on when a new value inputted to a dataframe, then how it updates the session_state variable etc. Is there a chance you could explain the steps? I wish visual studio code had a debug mode that worked, but it gives error when I try to debug this code. Exception has occurred: AttributeError st.session_state has no attribute "df1". Did you forget to initialize it?Scheel
Thanks a lot, @MathCatsAnd. When step 3 "The Page Reloads" is executed, is the entire code rerun? Also, df1 is defined outside of funct1, how can df1 can go inside funct1? Is there such a thing in python that variable of a def function doesn't have to be inputted in the function as def funct1('df1')?Scheel
Yes, reruns with rerun the entire script top to bottom. I deleted the returns, they were accidental copies from your original and weren't doing anything.Bickering
Thanks a lot, @MathCatsAnd. My last question: how was df1 be able to go into the def funct1 when df1 was defined outside of the function? It was still returning df1 with return df1 before you fixed it and it wasn't giving an error message. Is it like in the memory somewhere...?Scheel
That is vanilla Python: if a variable is not defined within a function, it will look outside of the function for its definition within the enclosing namespace.Bickering
Thanks for all these explanation, @MathCatsAnd. I have one more question regarding printing this app's each page to to a .pdf file. Do you know how to do that? I couldn't find answer to that online. I can create a new post if you prefer if you please let me know.Scheel
I'd create a new question to go into another topic.Bickering
Thanks for your reply, @MathCatsAnd. I tried to summarize and post the question here: linkScheel
Hi @MathCatsAnd, I posted my question in this link below, but I got no replies so far. Do you know if it is possible to create pdf file of a page in streamlit? I don't want to waste time if not possible. #76420103Scheel
Hi @MathCatsAnd. Your example does work (replacing experimental_data_editor with data_editor). However, try assigning the same (value wise) dataframes to df1 and df2 in the initialization and things break - you can edit a value on page A and the exact same value will show up on page B even though it wasn't edited there.Both
In the meantime I have also filed a bug report at the streamlit githup repo: github.com/streamlit/streamlit/issues/7685Both
To keep the data_editor widgets unique per page one must also use the "key" argument to data_editor, as outlined in the github issue.Both

© 2022 - 2024 — McMap. All rights reserved.