From f0c66ce13f449afbe1fea761f0aeb3f53646251a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20David=20M=C3=BCller?= Date: Thu, 23 May 2024 19:13:27 +0200 Subject: [PATCH] add LayoutManager to FLASHTagger --- app.py | 1 + pages/FLASHTaggerViewer.py | 110 ++------------- pages/LayoutManagerTagger.py | 256 +++++++++++++++++++++++++++++++++++ 3 files changed, 270 insertions(+), 97 deletions(-) create mode 100644 pages/LayoutManagerTagger.py diff --git a/app.py b/app.py index e29bfbe7..485ea4d0 100644 --- a/app.py +++ b/app.py @@ -45,6 +45,7 @@ def flashtagPages(): Page("app.py", "FLASHViewer", "🏠"), Page("pages/FLASHTaggerWorkflow.py", "Workflow", "⚙️"), Page("pages/FileUploadTagger.py", "File Upload", "📁"), + Page("pages/LayoutManagerTagger.py", "Layout Manager", "📝️"), Page("pages/FLASHTaggerViewer.py", "Viewer", "👀"), ]) diff --git a/pages/FLASHTaggerViewer.py b/pages/FLASHTaggerViewer.py index 17e3bdab..71234db4 100644 --- a/pages/FLASHTaggerViewer.py +++ b/pages/FLASHTaggerViewer.py @@ -19,20 +19,16 @@ # Annotated Spectrum will be replaced with new component including tags -def sendDataToJS(selected_data, layout_info_per_exp): - st.session_state.input_sequence = 'TENWMFIGQALRLTHNYRRPNQDLECVKGYRGDSLEAIVLWERRHSFPCI' - - # getting data +def sendDataToJS(selected_data, layout_info_per_exp, grid_key='flash_viewer_grid'): + selected_anno_file = selected_data.iloc[0]['Annotated Files'] selected_deconv_file = selected_data.iloc[0]['Deconvolved Files'] selected_tag_file = selected_data.iloc[0]['Tag Files'] - selected_db_file = selected_data.iloc[0]['DB Files'] + selected_db_file = selected_data.iloc[0]['Protein Files'] # getting data from mzML files spec_df = st.session_state['deconv_dfs_tagger'][selected_deconv_file] anno_df = st.session_state['anno_dfs_tagger'][selected_anno_file] - #spec_df.to_excel('Deconv.xlsx') - #anno_df.to_excel('Anno.xlsx') tag_df = st.session_state['tag_dfs_tagger'][selected_tag_file] # Process tag df into a linear data format @@ -100,87 +96,6 @@ def sendDataToJS(selected_data, layout_info_per_exp): str(sequence), p_cov, np.max(coverage) ) - - - - - - # # Stores sequence information {id : {sequence, coverage}} - # sequence_data = {} - - # tag_df = tag_df[~pd.isna(tag_df['ProteinIndex'])] - # tag_df['Scan'] = 0 - # for i, row in tag_df.iterrows(): - # if isinstance(row['ProteinIndex'], str) and (';' in row['ProteinIndex']): - # tag_df.loc[i, 'ProteinIndex'] = row['ProteinIndex'].split(';')[0] - - - # protein_df = tag_df.loc[:,['ProteinIndex', 'ProteinAccession', 'ProteinDescription']].drop_duplicates() - # for i, row in protein_df.iterrows(): - # if not pd.isna(row['ProteinDescription']): - # acc = f"{row['ProteinAccession']} {row['ProteinDescription']}" - # else: - # acc = row['ProteinAccession'] - # if pd.isna(acc): - # continue - # if ';' in acc: - # acc = acc.split(';')[0] - # protein_df.loc[i, 'ProteinAccession'] = acc - # protein_df.loc[i,'length'] = len(protein_db[acc]) - # protein_df.loc[i,'sequence'] = str(protein_db[acc]) - # sequence_data[row['ProteinIndex']] = {'sequence' : protein_db[acc]} - - # protein_df = protein_df.rename( - # columns={ - # 'ProteinIndex' : 'index', - # 'ProteinAccession' : 'accession', - # 'ProteinDescription' : 'description' - # } - # ) - # protein_df.loc[:,'index'] = protein_df['index'].astype('int') - - # # Augment tags with sequence position - # for i, row in tag_df.iterrows(): - # pid = row['ProteinIndex'] - # if pd.isna(pid): - # continue - # sequence = sequence_data[pid]['sequence'] - # tag_sequence = row['TagSequence'] - # for j in range(len(sequence)): - # if str(sequence[j:j+len(tag_sequence)]) == str(tag_sequence): - # tag_df.loc[i,'StartPos'] = j - # tag_df.loc[i,'EndPos'] = j+len(tag_sequence)-1 - # break - - # tag_df = tag_df[~pd.isna(tag_df['StartPos']) | ~pd.isna(tag_df['EndPos'])] - - # # Compute coverage - # for pid, data in sequence_data.items(): - # sequence = data['sequence'] - # coverage = np.zeros(len(sequence), dtype='float') - # for i in range(len(sequence)): - # coverage[i] = np.sum( - # (tag_df['ProteinIndex'] == pid) & - # (tag_df['StartPos'] <= i) & - # (tag_df['EndPos'] >= i) - # ) - # p_cov = np.zeros(len(coverage)) - # if np.max(coverage) > 0: - # p_cov = coverage/np.max(coverage) - # sequence_data[pid] = getFragmentDataFromSeq( - # str(sequence), p_cov, np.max(coverage) - # ) - - # tag_df.loc[:,'ProteinIndex'] = tag_df['ProteinIndex'].astype('int') - # tag_df.loc[:,'TagIndex'] = tag_df['TagIndex'].astype('int') - # if 'Score' not in tag_df.columns: - # tag_df.loc[:,'Score'] = tag_df['DeNovoScore'] - - # print(protein_df) - # print(protein_df.columns) - # print(tag_df) - # print(tag_df.columns) - components = [] data_to_send = {} per_scan_contents = {'mass_table': False, 'anno_spec': False, 'deconv_spec': False, '3d': False} @@ -211,10 +126,10 @@ def sendDataToJS(selected_data, layout_info_per_exp): component_arguments = Tabulator('MassTable') elif comp_name == 'protein_table': data_to_send['protein_table'] = protein_df + data_to_send['per_scan_data'] = getSpectraTableDF(spec_df) component_arguments = Tabulator('ProteinTable') elif comp_name == 'tag_table': data_to_send['tag_table'] = tag_df - data_to_send['per_scan_data'] = getSpectraTableDF(spec_df) component_arguments = Tabulator('TagTable') elif comp_name == '3D_SN_plot': per_scan_contents['3d'] = True @@ -260,7 +175,7 @@ def sendDataToJS(selected_data, layout_info_per_exp): # Set sequence data data_to_send['sequence_data'] = sequence_data - flash_viewer_grid_component(components=components, data=data_to_send) + flash_viewer_grid_component(components=components, data=data_to_send, component_key=grid_key) def setSequenceViewInDefaultView(): @@ -292,15 +207,15 @@ def content(): st.selectbox("choose experiment", experiment_df['Experiment Name'], key="selected_experiment0") selected_exp0 = experiment_df[experiment_df['Experiment Name'] == st.session_state.selected_experiment0] layout_info = DEFAULT_LAYOUT - if "saved_layout_setting" in st.session_state: # when layout manager was used - layout_info = st.session_state["saved_layout_setting"][0] + if "saved_layout_setting_tagger" in st.session_state: # when layout manager was used + layout_info = st.session_state["saved_layout_setting_tagger"][0] with st.spinner('Loading component...'): sendDataToJS(selected_exp0, layout_info) ### for multiple experiments on one view - if "saved_layout_setting" in st.session_state and len(st.session_state["saved_layout_setting"]) > 1: + if "saved_layout_setting_tagger" in st.session_state and len(st.session_state["saved_layout_setting_tagger"]) > 1: - for exp_index, exp_layout in enumerate(st.session_state["saved_layout_setting"]): + for exp_index, exp_layout in enumerate(st.session_state["saved_layout_setting_tagger"]): if exp_index == 0: continue # skip the first experiment st.divider() # horizontal line @@ -311,13 +226,14 @@ def content(): selected_exp = experiment_df[ experiment_df['Experiment Name'] == st.session_state["selected_experiment%d"%exp_index]] - layout_info = st.session_state["saved_layout_setting"][exp_index] + layout_info = st.session_state["saved_layout_setting_tagger"][exp_index] + print(layout_info) with st.spinner('Loading component...'): - sendDataToJS(selected_exp, layout_info) + sendDataToJS(selected_exp, layout_info, 'flash_viewer_grid_%d' % exp_index) selected_tags = selected_exp0.iloc[0]['Tag Files'] - selected_proteins = selected_exp0.iloc[0]['DB Files'] + selected_proteins = selected_exp0.iloc[0]['Protein Files'] tag_df = st.session_state['tag_dfs_tagger'][selected_tags] protein_df = st.session_state['protein_dfs_tagger'][selected_proteins] diff --git a/pages/LayoutManagerTagger.py b/pages/LayoutManagerTagger.py new file mode 100644 index 00000000..7e3ad604 --- /dev/null +++ b/pages/LayoutManagerTagger.py @@ -0,0 +1,256 @@ +import streamlit as st +from src.common import page_setup, v_space, save_params +import json + +COMPONENT_OPTIONS=[ + 'Protein table', + 'Sequence view (Protein table needed)', + 'Tag table (Protein table needed)', + 'Spectrum view (Tag table needed)', +] + +COMPONENT_NAMES=[ + 'protein_table', + 'sequence_view', + 'tag_table', + 'deconv_spectrum' +] + + +def resetSettingsToDefault(num_of_exp=1): + st.session_state["layout_setting_tagger"] = [[['']]] # 1D: experiment, 2D: row, 3D: column, element=component name + st.session_state["num_of_experiment_to_show_tagger"] = num_of_exp + for index in range(1, num_of_exp): + st.session_state.layout_setting_tagger.append([['']]) + + +def containerForNewComponent(exp_index, row_index, col_index): + + def isThisComponentUnique(new_component_option): + if any(col for row in st.session_state.layout_setting_tagger[exp_index] for col in row if col==new_component_option): + st.session_state["component_error_message_tagger"] = 'Duplicated component!' + return False + else: + return True + + def addNewComponent(): + new_component_option = 'SelectNewComponent%d%d%d'%(exp_index, row_index, col_index) + if isThisComponentUnique(st.session_state[new_component_option]): + st.session_state.layout_setting_tagger[exp_index][row_index][col_index] = st.session_state[new_component_option] + + # new component + st.selectbox("New component to add", ['Select...'] + COMPONENT_OPTIONS, + key='SelectNewComponent%d%d%d'%(exp_index, row_index, col_index), + on_change=addNewComponent, + placeholder='Select...', + ) + + +def layoutEditorPerExperiment(exp_index): + layout_info = st.session_state.layout_setting_tagger[exp_index] + + for row_index, row in enumerate(layout_info): + st_cols = st.columns(len(row)+1 if len(row)<3 else len(row)) + for col_index, col in enumerate(row): + if not col: # if empty, add newComponent container + with st_cols[col_index].container(): + containerForNewComponent(exp_index, row_index, col_index) + else: + with st_cols[col_index]: + c1, c2 = st.columns([5, 1]) + c1.info(col) + if c2.button("x", key='DelButton%d%d%d'%(exp_index, row_index, col_index), type='primary'): + layout_info[row_index].pop(col_index) + st.rerun() + + # new column button + if len(row) < 3: # limit for #column is 3 + if st_cols[-1].button("***+***", key='NewColumnButton%d%d'%(exp_index, row_index)): + layout_info[row_index].append('') + st.rerun() + + # new row button + if st.button("***+***", key='NewRowButton%d'%exp_index): + layout_info.append(['']) + st.rerun() + + +def validateSubmittedLayout(input_layout=None): + layout_setting = input_layout if input_layout is not None else st.session_state.layout_setting_tagger + + # check if submitted layout is empty + if not any(col for exp in layout_setting for row in exp for col in row if col): + return 'Empty input' + + # check if submitted layout contains "needed" components + for exp in layout_setting: + submitted_components = [col for row in exp for col in row if col] + required_components = [comp.split('(')[1].split('needed')[0].rstrip() for comp in submitted_components if 'needed' in comp] + if required_components: + for required in required_components: + required_exist = False + for submitted in submitted_components: + if submitted.startswith(required): + required_exist = True + if not required_exist: + return 'Required component is missing' + return '' + + +def getTrimmedLayoutSetting(): + trimmed_layout_setting = [] + for exp in st.session_state.layout_setting_tagger: + rows = [] + for row in exp: + cols = [] + for col in row: + if col: + cols.append(COMPONENT_NAMES[COMPONENT_OPTIONS.index(col)]) + if cols: + rows.append(cols) + if rows: + trimmed_layout_setting.append(rows) + return trimmed_layout_setting + + +def handleEditAndSaveButtons(): + # if "Edit" button was clicked, + if "edit_btn_clicked" in st.session_state and st.session_state["edit_btn_clicked"]: + # reset variables based on "saved_layout_setting" + st.session_state["num_of_experiment_to_show"] = len(st.session_state["saved_layout_setting_tagger"]) + st.session_state["layout_setting_tagger"] = [[[COMPONENT_OPTIONS[COMPONENT_NAMES.index(col)] + for col in row if col] + for row in exp if row] + for exp in st.session_state.saved_layout_setting_tagger] + # remove saved state, if any + del st.session_state["saved_layout_setting_tagger"] + + # if "Save" button was clicked, + if "layout_saved_tagger" in st.session_state and st.session_state["layout_saved_tagger"]: + got_error = validateSubmittedLayout() + st.session_state['save_btn_error_message'] = got_error # to show error msg at the end + if not got_error: + # get only submitted info from "layout_setting" + st.session_state["saved_layout_setting_tagger"] = getTrimmedLayoutSetting() + + +def handleSettingButtons(): + if "reset_btn_clicked" in st.session_state and st.session_state.reset_btn_clicked: + resetSettingsToDefault() + if "saved_layout_setting_tagger" in st.session_state: + del st.session_state["saved_layout_setting_tagger"] + + if "uploaded_json_file" in st.session_state and st.session_state.uploaded_json_file is not None: + uploaded_layout = json.load(st.session_state.uploaded_json_file) + validated = validateSubmittedLayout(uploaded_layout) + if validated!='': + st.session_state["component_error_message"] = validated + else: + st.session_state.layout_setting_tagger = [[[COMPONENT_OPTIONS[COMPONENT_NAMES.index(col)] + for col in row if col] + for row in exp if row] + for exp in uploaded_layout] + st.session_state.num_of_experiment_to_show = len(uploaded_layout) + + +def setSequenceView(): + if 'input_sequence' in st.session_state and st.session_state.input_sequence: + global COMPONENT_OPTIONS + COMPONENT_OPTIONS = COMPONENT_OPTIONS + ['Sequence view (Mass table needed)', + 'Internal fragment map (Mass table needed)'] + global COMPONENT_NAMES + COMPONENT_NAMES = COMPONENT_NAMES + ['sequence_view', 'internal_fragment_map'] + + +# page initialization +params = page_setup() + +# when sequence is submitted, add "Sequence View" as a component option +#setSequenceView() + +# handles "onclick" of buttons +handleSettingButtons() +handleEditAndSaveButtons() + +# initialize setting information +if "layout_setting_tagger" not in st.session_state: + resetSettingsToDefault() +# the "num_of_experiment_to_show" changed +elif "num_of_experiment_to_show" in st.session_state and \ + len(st.session_state.layout_setting_tagger) != st.session_state.num_of_experiment_to_show: + resetSettingsToDefault(st.session_state.num_of_experiment_to_show) + +### title and setting buttons +c1, c2, c3, c4 = st.columns([6, 1, 1, 1]) +c1.title("Layout Manager") + +# Load existing layout setting file +v_space(1, c2) +c2.button("Load Setting", key="load_btn_clicked") + +# Save current layout setting (only after "Saved" button) +v_space(1, c3) +c3.download_button( + label="Save Setting", + data=json.dumps(getTrimmedLayoutSetting()), + file_name='FLASHViewer_layout_settings.json', + mime='json', + disabled=(validateSubmittedLayout()!=''), +) + +# Reset settings to default +v_space(1, c4) +c4.button("Reset Setting", key="reset_btn_clicked") + +### space for File Uploader, when "Load Setting" button is clicked +if "load_btn_clicked" in st.session_state and st.session_state.load_btn_clicked: + st.file_uploader("Choose a json file", type="json", key="uploaded_json_file") + +### Main part +if "saved_layout_setting_tagger" in st.session_state: + # show saved-mode + for index_of_experiment in range(len(st.session_state.saved_layout_setting_tagger)): + layout_info_per_experiment = st.session_state.saved_layout_setting_tagger[index_of_experiment] + with st.expander("Experiment #%d"%(index_of_experiment+1), expanded=True): + for row_index, row in enumerate(layout_info_per_experiment): + st_cols = st.columns(len(row)) + for col_index, col in enumerate(row): + st_cols[col_index].info(COMPONENT_OPTIONS[COMPONENT_NAMES.index(col)]) +else: + # show edit-mode + st.selectbox("**#Experiments to view at once**", [1, 2, 3, 4, 5], + key="num_of_experiment_to_show", + ) + + for index_of_experiment in range(st.session_state.num_of_experiment_to_show): + with st.expander("Experiment #%d"%(index_of_experiment+1)): + layoutEditorPerExperiment(index_of_experiment) + +### buttons for edit/save +_, edit_btn_col, save_btn_col = st.columns([9, 1, 1]) +edit_btn_col.button("Edit", key="edit_btn_clicked", + disabled=False if "saved_layout_setting_tagger" in st.session_state else True) +save_btn_col.button("Save", key="layout_saved_tagger", + disabled=True if "saved_layout_setting_tagger" in st.session_state else False) + +### showing error/success message +if "save_btn_error_message" in st.session_state and st.session_state.layout_saved_tagger: + error_message = st.session_state["save_btn_error_message"] + if error_message: + st.error('Error: '+error_message, icon="🚨") + else: + st.success('Layouts Saved', icon="✔️") +if "component_error_message" in st.session_state and st.session_state.component_error_message: + st.error('Error: ' + st.session_state.component_error_message, icon="🚨") + del st.session_state["component_error_message"] + +### TIPs (TODO: Add image) +st.info(""" +**💡 Tips** + +- If nothing is set, the default layout will be used in the **👀 Viewer** page + +- Don't forget to click "save" on the bottom-right corner to save your setting +""") + +save_params(params)