PablocFonseca / streamlit-aggrid

Implementation of Ag-Grid component for Streamlit
https://pypi.org/project/streamlit-aggrid/
MIT License
991 stars 191 forks source link

How to keep streamlit ag-grid selected rows after page update? #197

Open andrewssobral opened 1 year ago

andrewssobral commented 1 year ago

I'm currently working on a real-time metrics dashboard with an AgGrid table to display the data. To keep the data up-to-date, I would like to reload it automatically every 15 seconds. I've tried using st_autorefresh to accomplish this, but I'm facing an issue where the AgGrid table loses the selected rows after each page update, even when using pre_selected_rows.

Do any of you have experience with this issue and know how to solve it?

Here's the link for the example repository: https://github.com/andrewssobral/streamlit-example

Here's the link of the running application: https://andrewssobral-streamlit-example-streamlit-app-txrvx1.streamlit.app/

import numpy as np
import pandas as pd
import streamlit as st

from streamlit_autorefresh import st_autorefresh
from st_aggrid import GridOptionsBuilder, AgGrid, GridUpdateMode, DataReturnMode, JsCode

if 'pre_selected_rows' not in st.session_state:
    st.session_state.pre_selected_rows = []

df = pd.DataFrame(columns=['ID', 'STATUS'])
df['ID'] = [1, 2, 3]
df['STATUS'] = np.random.randint(0,100,size=(3))
st.write(df)

# get pre-selected rows from session state
pre_selected_rows = st.session_state.pre_selected_rows
st.write("pre_selected_rows: ", st.session_state.pre_selected_rows)

# use the pre-selected rows when building the grid options
gb = GridOptionsBuilder.from_dataframe(df)
gb.configure_default_column(editable=True, wrapText=True, autoHeight=True)
gb.configure_column('ID', minWidth=80, maxWidth=80, type=["numericColumn","numberColumnFilter"], sortable=True, sort='desc', checkboxSelection=True, headerCheckboxSelection=True)
gb.configure_column('STATUS', minWidth=100, maxWidth=100)
gb.configure_pagination(paginationAutoPageSize=False, paginationPageSize=3)
gb.configure_side_bar()
gb.configure_selection('multiple', pre_selected_rows=pre_selected_rows, use_checkbox=True)
gb_grid_options = gb.build()

# render the grid and get the selected rows
grid_return = AgGrid(
        df,
        gridOptions = gb_grid_options,
        key = 'ID',
        reload_data = True,
        data_return_mode = DataReturnMode.AS_INPUT,
        update_mode = GridUpdateMode.MODEL_CHANGED, # GridUpdateMode.SELECTION_CHANGED or GridUpdateMode.VALUE_CHANGED or 
        allow_unsafe_jscode = True,
        fit_columns_on_grid_load = False,
        enable_enterprise_modules = False,
        height = 320,
        width = '100%',
        theme = "streamlit"
    )
selected_rows = grid_return["selected_rows"]

# save the row indexes of the selected rows in the session state
pre_selected_rows = []
for selected_row in selected_rows:
        pre_selected_rows.append(selected_row['_selectedRowNodeInfo']['nodeRowIndex'])
st.session_state.pre_selected_rows = pre_selected_rows

# print the selected rows
st.write("Selected rows: ", selected_rows)

st_autorefresh(interval=((1*15*1000)), key="dataframerefresh")
vovavili commented 1 year ago

You can try to use session state to save the rows from grid_return['selected_rows'], and upon restart, configure AgGrid in a way to make them pre-selected. If st_autorefresh nullifies the session state as well, you'd have to store selected rows in some sort of pickle file.

EDIT: Disregard this, you've already tried this approach.

andrewssobral commented 1 year ago

Hello @vovavili ,

Thank you for your response and suggestion. However, I am already using session state to save the selected rows and configuring AgGrid to make them pre-selected upon restart (please see the code above), but unfortunately, this did not solve my problem.

Additionally, st_autorefresh does not nullifies the session state as well. To help you better understand my issue, I have provided an example here: https://andrewssobral-streamlit-example-streamlit-app-txrvx1.streamlit.app/

If you have any other suggestions or solutions that could help me with this issue, I would be grateful to hear them.

Thank you for your assistance.

Best regards, Andrews

PablocFonseca commented 1 year ago

Set a fixed key on AgGrid call, by using key="something". Grid won't be destroyed between reruns. If you need to reload data use reload_data=True

andrewssobral commented 1 year ago

Hello @PablocFonseca ,

Thank you for your response and suggestion.

That's exactly what I did.

To help you better understand my issue, I have provided an example here: https://andrewssobral-streamlit-example-streamlit-app-txrvx1.streamlit.app/

and its source code here: https://github.com/andrewssobral/streamlit-example/blob/master/streamlit_app.py

Please let me know if you have any suggestion. Thank you for your assistance.

SibylleEngelhard commented 1 year ago

Hi Andrews, Your problem helped me a lot to sort out my problem with pre-selected rows. I already posted an alternative solution as answer to your question on stack exchange.

Here is now a solution with using pre-selected rows in your example: Firstly: it doesn't work correctly with streamlit-agggrid 0.3.4! Even the streamlit-aggrid example how to use pre-selected rows [https://staggrid-examples.streamlit.app/] doesn't work with that version. Version 0.3.4 wrongly changes the order of the pre-selection. It works with version 0.3.3 with a few small changes to your code: reload_data = False key = st.session_state.grid_key Every ag-grid display gets a new key from the session_state which is updated after every rerun. (In this case I just used an integer as the key wich is increased by 1 after very rerun.)

For some reason ag-grid needs a bit of time when a selection is clicked to detect the change. In my application it was therefore necessary to add a button to get the correct grid_response or to add a time.sleep(1). As your application anyway needs to update every few seconds, a time.sleep(5) is added and the rerun is done by st.experimental_rerun.

Please let me know if this solution works for you. Here is your code edited with these changes:

import numpy as np
import pandas as pd
import streamlit as st
import time

from streamlit_autorefresh import st_autorefresh
from st_aggrid import GridOptionsBuilder, AgGrid, GridUpdateMode, DataReturnMode, JsCode

if 'pre_selected_rows' not in st.session_state:
    st.session_state.pre_selected_rows = []
if "grid_key" not in st.session_state:
    st.session_state.grid_key = 0

# button to stop application
button = st.button("Stop")

df = pd.DataFrame(columns=['ID', 'STATUS'])
df['ID'] = [1, 2, 3]
df['STATUS'] = np.random.randint(0,100,size=(3))
# st.write(df)
# the dataframe is only displayed after the AG-grid
# placeholder_df to keep its place
placeholder_df = st.empty()

# get pre-selected rows from session state
pre_selected_rows = st.session_state.pre_selected_rows

placeholder_session_state = st.empty()
#st.write("pre_selected_rows: ", pre_selected_rows)

# use the pre-selected rows when building the grid options
gb = GridOptionsBuilder.from_dataframe(df)
gb.configure_default_column(editable=True, wrapText=True, autoHeight=True)
gb.configure_column('ID', minWidth=80, maxWidth=80, type=["numericColumn","numberColumnFilter"], sortable=True, sort='asc', headerCheckboxSelection=True)
gb.configure_column('STATUS', minWidth=100, maxWidth=100)
gb.configure_pagination(paginationAutoPageSize=False, paginationPageSize=3)
gb.configure_side_bar()
gb.configure_selection('multiple', pre_selected_rows=pre_selected_rows, rowMultiSelectWithClick=False, use_checkbox=True)
gb.configure_grid_options(suppressRowHoverHighlight=True, columnHoverHighlight=False, pre_selected_rows=pre_selected_rows)
gb_grid_options = gb.build()

# render the grid and get the selected rows
grid_return = AgGrid(
    df,
    gridOptions = gb_grid_options,# key changed to key in session state that gets changed before every rerun
    key = st.session_state.grid_key,  # reload_data set to False
    reload_data = False,
    data_return_mode = DataReturnMode.AS_INPUT,
    update_mode = GridUpdateMode.MODEL_CHANGED,
    allow_unsafe_jscode = True,
    fit_columns_on_grid_load = False,
    enable_enterprise_modules = False,
    height = 200,
    width = '100%',
    theme = "streamlit"
)

# the displayed dataframe shows the return from the grid
placeholder_df.write(grid_return['data'])

selected_rows = grid_return["selected_rows"]

# print the selected rows
# st.write("Selected rows: ", selected_rows)

# save the row indexes of the selected rows in the session state
pre_selected_rows = []
for selected_row in selected_rows:
        pre_selected_rows.append(selected_row['_selectedRowNodeInfo']['nodeRowIndex'])

if st.session_state.pre_selected_rows != pre_selected_rows:
    st.session_state.pre_selected_rows = pre_selected_rows
    placeholder_session_state.write("pre_selected_rows:  "+str(list(st.session_state.pre_selected_rows)))

time.sleep(5)
st.session_state.grid_key += 1

if button:
    st.stop()
else:
    st.experimental_rerun()
    #st_autorefresh(interval=((1*1*1000)), key="dataframerefresh")