I am building a table that updates the values of an output DF into a csv file (or whatever output defined).
I defined a generate_agrid(df) function that outputs a class that contains a data method that is a pd.DataFrame. When I run the code grid_table = generate_agrid(df), the grid_table generated contains the original df, even if I modify it in the UI. I noticed that when I checked the input that my update function received.
What I want is to:
Graph the data in df -> update DF data in the UI and return -> save new df data into a csv every time I press update button
Why does my generate_agrid method always returns the initial DF used as an input? How can i update it?
My code
import streamlit as st
from metrics.get_metrics import get_data
from metrics.config import PATH_SAMPLES
filename: str = 'updated_sample.csv'
save_path = PATH_SAMPLES.joinpath(filename)
def generate_agrid(data: pd.DataFrame):
gb = GridOptionsBuilder.from_dataframe(data)
gb.configure_default_column(editable=True) # Make columns editable
gb.configure_pagination(paginationAutoPageSize=True) # Add pagination
gb.configure_side_bar() # Add a sidebar
gb.configure_selection('multiple', use_checkbox=True,
groupSelectsChildren="Group checkbox select children") # Enable multi-row selection
gridOptions = gb.build()
grid_response = AgGrid(
data,
gridOptions=gridOptions,
data_return_mode=DataReturnMode.AS_INPUT,
update_on='MANUAL', # <- Should it let me update before returning?
fit_columns_on_grid_load=False,
theme=AgGridTheme.STREAMLIT, # Add theme color to the table
enable_enterprise_modules=True,
height=350,
width='100%',
reload_data=True
)
data = grid_response['data']
selected = grid_response['selected_rows']
df = pd.DataFrame(selected) # Pass the selected rows to a new dataframe df
return grid_response
def update(grid_table: classmethod, filename: str = 'updated_sample.csv'):
save_path = PATH_SAMPLES.joinpath(filename)
grid_table_df = pd.DataFrame(grid_table['data'])
grid_table_df.to_csv(save_path, index=False)
# First data gather
df = get_data()
if __name__ == '__main__':
# Start graphing
grid_table = generate_agrid(df)
# Update
st.sidebar.button("Update", on_click=update, args=[grid_table])
Found the issue, it was just a small parameter that was activated.
While instantiating the AgGrid, I had to eliminate the reload_data=True parameter. Doing that, everything worked as expected and the data could be successfully updated after manually inputting and pressing "update"
This is how AgGrid must be instantiated
grid_response = AgGrid(
data,
gridOptions=gridOptions,
data_return_mode=DataReturnMode.AS_INPUT,
update_on='MANUAL',
fit_columns_on_grid_load=False,
theme=AgGridTheme.STREAMLIT, # Add theme color to the table
enable_enterprise_modules=True,
height=350,
width='100%',
)
Related
This is my code and although I initialize the session state, I'm having a conflict with it.
import streamlit as st
if "user_inputs" not in st.session_state:
st.session_state["user_inputs"] = True
# Loop over the 8 questions
for i in range(8):
# Get the question
question = f'Question {i+1}'
# Add it to the page
st.subheader(question)
# Create 3 checkbox options
checkbox1 = st.checkbox('Option 1')
checkbox2 = st.checkbox('Option 2')
checkbox3 = st.checkbox('Option 3')
# Save the checkbox inputs in the session state object
st.session_state.user_inputs[f'{question}_checkbox_1'] = checkbox1
st.session_state.user_inputs[f'{question}_checkbox_2'] = checkbox2
st.session_state.user_inputs[f'{question}_checkbox_3'] = checkbox3
# Create an integer input
integer_input = st.number_input('Integer Input')
# Save the integer input in the session state object
st.session_state.user_inputs[f'{question}_integer_input'] = integer_input
# Create a slider
slider = st.slider('Slider', 0, 100)
# Save the slider value in the session state object
st.session_state.user_inputs['slider'] = slider
# Add a submit button
if st.button('Submit'):
st.success('Form submitted!')
I tried different ways to initialize but they didn't work. When I try something like st.session_state.user_inputs = "test" or st.session_state["user_inputs"] = "test", again I have same error:
The error is:
st.session_state has no attribute "user_inputs". Did you forget to
initialize it? More info:
https://docs.streamlit.io/library/advanced-features/session-state#initialization
I tried to create a kind of form but I received a repetitive error.
The error I get for your main block of code is TypeError: 'bool' object does not support item assignment.
This makes sense, because st.session_state["user_inputs"] = True means you've set up a boolean, not a dictionary, so when you do:
st.session_state.user_inputs[f'{question}_checkbox_1'] = checkbox1
you're basically doing:
True[f'{question}_checkbox_1'] = checkbox1
giving the error. Instead, use:
if "user_inputs" not in st.session_state:
st.session_state["user_inputs"] = {}
Next, you have duplicate keyed inputs. Try adding unique keys to these:
checkbox1 = st.checkbox('Option 1', key=f'{i}-1')
checkbox2 = st.checkbox('Option 2', key=f'{i}-2')
checkbox3 = st.checkbox('Option 3', key=f'{i}-3')
# ...
integer_input = st.number_input('Integer Input', key=i)
Note also that keyed user inputs are automatically added to state, which is a bit surprising, but that's Streamlit for you. If you do prefer to explicitly set them, I'd use actual nested structures like 2d lists or dicts rather than keys with concatenated identifiers.
Finally, you may want to use st.form to group all of your inputs into a collection, avoiding triggering rerenders.
I'm displaying already available data from csv file in a GUI.
If I update some values in the GUI, i want them to be updated in the csv file as well.
def onClickedSaveReturn(self):
"""closes GUI and returns to calling (main) GUI"""
df_general = Clean.get_GeneralData()
# First of all, read general data so that pre-/intra- and postoperative share these
df_subj = {k: '' for k in Content.extract_saved_data(self.date).keys()} # create empty dictionary
df_subj['ID'] = General.read_current_subj().id[0]
df_subj['PID'] = df_general['PID_ORBIS'][0]
df_subj['Gender'] = df_general['Gender'][0]
df_subj['Diagnosis_preop'] = df_general['diagnosis'][0]
# Now extract teh changed data from the GUI
df_subj["First_Diagnosed_preop"] = self.lineEditFirstDiagnosed.text()
df_subj['Admission_preop'] = self.lineEditAdmNeurIndCheck.text()
df_subj['Dismissal_preop'] = self.DismNeurIndCheckLabel.text()
.
.
.
subj_id = General.read_current_subj().id[0] # reads data from current_subj (saved in ./tmp)
df = General.import_dataframe('{}.csv'.format(self.date), separator_csv=',')
if df.shape[1] == 1:
df = General.import_dataframe('{}.csv'.format(self.date), separator_csv=';')
idx2replace = df.index[df['ID'] == subj_id][0]
df_subj = df.iloc[idx2replace, :]
df = df.replace(['nan', ''], [np.nan, np.nan])
#df.to_csv(os.path.join(FILEDIR, "preoperative.csv"), index=False)
If i run this code including the line
"df.to_csv(os.path.join(FILEDIR, "preoperative.csv"), index=False)", all data in the csv file (except the columns "ID", "PID", "Gender" "Diagnosis_preop")
So I want all the data i definied in df_subj to replace the data in csv.
I have no idea why my code isn't working ..
Thank you guys!
def kayit_ekle_update(): ##
kayit_listesiVar.set('') # remove default selection only, not the full list
kayit_listesiOM['menu'].delete(0, 'end') # remove full list
kayitlilarOM = os.listdir("allin1/data_records")
for opt in kayitlilarOM:
kayit_listesiOM['menu'].add_command(label=opt, command=tk._setit(kayit_listesiVar, opt))
kayit_listesiVar.set(kayitlilarOM[0]) # default value set
def entri_doldur(se):
## get data from excel and append them to a new list(veri_al_list)
veri_al_list = []
om_deger = "allin1/data_records/" + kayit_listesiVar.get()
wb_kayit = load_workbook(om_deger)
ws_kayit = wb_kayit.active
for column_data in ws_kayit['1']:
veri_al_list.append(column_data.value)
veri_al_list = [' ' if v is None else v for v in veri_al_list] # None elemanı hata verdiriyor. Burada tüm None olanları "" ile değiştiriyoruz.
print(veri_al_list)
## There are Entry widgets created with for loop.
## en in entry_liste ==> Entries
## ne in veri_al_liste ==> data list from excel
for en, ne in zip(entry_liste, veri_al_list):
en.delete(0,END)
en.insert(END, ne)
wb_kayit.close()
kayitlilarOM = os.listdir("allin1/data_records")
kayit_listesiVar = StringVar()
kayit_listesiVar.set("Kayıt seç")
kayit_listesiOM = OptionMenu(ust, kayit_listesiVar, *kayitlilarOM, command=entri_doldur)
I have entry widgets in my code. The user can enter data and save them. Each time this data is saved, a new excel file is created and the data is saved in it.
The name of each saved excel file is displayed in an optionmenu. When we select a record from the Optionmenu, the entri_doldur() function works and the entries are filled according to the record.
I can delete the selected record with the button. When deleted, both the entries are empty and the option menu is updated.
But the problem occurs when I create a new record. Creating a new excel file. The data is saved in it. Option menu is being updated. But after that, when I select a record in the optionmenu, the entri_doldur() function does not work. The information of the selected record does not fill the entries.
All,
I have used multiselect successfully before, but when I try this specific example that I was trying as a POC, the behavior is very weird. Essentially, what I am trying to do is use multiselect to make the app wait for user input at an intermediate step. However, multiselect does not wait for me to select the inputs I want to select, as soon as I select one thing, it just runs and doesn’t even execute correctly. Can someone guide me as to what am I doing wrong ? I am on version 0.82.
I also tested the same using selectbox and am seeing the same behavior.
So, here is what I have:
import streamlit as st
import pandas as pd
def basic_skeleton() -> tuple:
"""Prepare the basic UI for the app"""
st.sidebar.title('User Inputs')
beta_expander = st.sidebar.beta_expander("Upload csv")
with beta_expander:
user_file_path = st.sidebar.file_uploader(
label='Random Data',
type='csv'
)
return user_file_path
def get_filtered_dataframe(df) -> pd.DataFrame:
columns_list = df.columns
with st.form(key='Selecting Columns'):
columns_to_aggregate = st.selectbox(
label='Select columns to summarize',
options=columns_list
)
submit_button = st.form_submit_button(label='Submit')
if submit_button:
df1 = df[columns_to_aggregate]
return df1
def main():
"""Central wrapper to control the UI"""
# add title
st.header('Streamlit Testing')
# add high level site inputs
user_file_path = basic_skeleton()
load = st.sidebar.button(label='Load Data')
if load:
df = pd.read_csv(user_file_path)
st.dataframe(df)
clean_df = get_filtered_dataframe(df)
run = st.button("Aggregate Selected columns")
if run:
result = clean_df.describe(include='all')
st.dataframe(result)
main()
A user on the streamlit community helped answer this question. I wanted to make sure, the answer was provided here so anybody who comes looking is also provided here:
import streamlit as st
import pandas as pd
def basic_skeleton() -> tuple:
"""Prepare the basic UI for the app"""
st.sidebar.title('User Inputs')
beta_expander = st.sidebar.beta_expander("Upload csv")
with beta_expander:
user_file_path = st.sidebar.file_uploader(
label='Random Data',
type='csv'
)
return user_file_path
def get_filtered_dataframe(df):
columns_list = df.columns
with st.form(key='Selecting Columns'):
columns_to_aggregate = st.multiselect(
label='Select columns to summarize',
options=columns_list
)
submit_button = st.form_submit_button(label='Submit')
if submit_button:
df1 = df[columns_to_aggregate]
return df1
def main():
"""Central wrapper to control the UI"""
# add title
st.header('Streamlit Testing')
# add high level site inputs
user_file_path = basic_skeleton()
if user_file_path:
load = st.sidebar.checkbox(label='Load Data')
if load:
df = pd.read_csv(user_file_path)
st.dataframe(df)
clean_df = get_filtered_dataframe(df)
if clean_df is not None:
result = clean_df.describe()
st.dataframe(result)
main()
In a callback, a dataframe is created from user inputs. I need to use that dataframe in another function, in order to serve it to the user.
I read that server.route can do this, with Flask SendFile, but I can't access the dataframe since I cannot use global variables.
I have read there is a hidden div method but I don't know how I can access a html div property from inside of python.
'''
server = flask.Flask('app')
app = dash.Dash(__name__,
external_stylesheets=external_css,
server=server)
master = pd.read_csv('master_dataframe.csv')
#server.route("/downloadable/")
def download_file():
df = # The dataframe I need that is in the other function
buffer = io.BytesIO()
dff.to_excel(buffer) # write to BytesIO buffer
buffer.seek(0)
return send_file(
buffer,
attachment_filename='data.xlsx',
as_attachment=True,
cache_timeout=0
)
#app.callback(
Output('plot_button','n_clicks_timestamp'),
[Input('account_selector','value')]
)
def generate_layout(value):
df = make_something(master, value)
return html_layout
'''
You could output the contents of the dataframe in JSON format to the children prop of a div with display='none'. Then use another callback with the children of that div as its Input, and you'll be able to read the JSON and use that data.
Quick example:
#app.callback(
Output('my-hidden-div','children'),
[Input('my-input','value')] # whatever this will be
)
def generate_df_callback(value):
df = make_df_from_input(value)
return df
#app.callback(
Output('my-output', 'value'), # whatever this will be
[Input('my-hidden-div', 'children')]
def use_df_callback(df):
foo = do_something_with_df(df)
return foo