From 06823bf18b32ab757bf4867bc725583cbf1572c9 Mon Sep 17 00:00:00 2001 From: Qing Date: Sun, 8 Sep 2024 23:22:42 -0400 Subject: [PATCH 1/4] - New: 1. Added Box-Cox method of Data Preprocessing. 2. Added an option to change number of cos of trends clusuter figure. - Fix: 1. Fixed the bug of the heatmap plot, When duplicate samples are present after renaming taxa. --- Docs/ChangeLog.md | 6 + metax/gui/main_gui.py | 27 +- metax/gui/metax_gui/main_window.ui | 292 ++++++++++-------- metax/gui/metax_gui/ui_main_window.py | 155 ++++++---- .../analyzer_utils/basic_stats.py | 26 +- .../analyzer_utils/data_preprocessing.py | 4 +- metax/taxafunc_ploter/heatmap_plot.py | 10 +- metax/taxafunc_ploter/trends_plot.py | 29 +- metax/utils/version.py | 2 +- pyproject.toml | 2 +- 10 files changed, 335 insertions(+), 218 deletions(-) diff --git a/Docs/ChangeLog.md b/Docs/ChangeLog.md index a276296..ca74a4b 100644 --- a/Docs/ChangeLog.md +++ b/Docs/ChangeLog.md @@ -1,3 +1,9 @@ +# Version: 1.1118 +## Date: 2024-09-08 +### Changes: +- New: 1. Added Box-Cox method of Data Preprocessing. 2. Added an option to change number of cos of trends clusuter figure. + + # Version: 1.111.7 ## Date: 2024-09-06 ### Changes: diff --git a/metax/gui/main_gui.py b/metax/gui/main_gui.py index c35f3e5..49c4a67 100644 --- a/metax/gui/main_gui.py +++ b/metax/gui/main_gui.py @@ -2552,6 +2552,8 @@ def set_multi_table(self, restore_taxafunc=False, saved_obj=None): "Log 10 transformation": "log10", "Square root transformation": "sqrt", "Cube root transformation": "cube", + "Box-Cox": "boxcox", + } normalize_dict = { "None": None, @@ -3732,7 +3734,8 @@ def plot_trends_cluster(self): table_name = self.comboBox_trends_table.currentText() font_size = self.spinBox_trends_font_size.value() - title = f'{table_name.capitalize()} Cluster' + # title = f'{table_name.capitalize()} Cluster' + title = 'Cluster' num_cluster = self.spinBox_trends_num_cluster.value() @@ -3786,15 +3789,20 @@ def plot_trends_cluster(self): df = dft.loc[self.trends_cluster_list] try: + num_col = self.spinBox_trends_num_col.value() + if num_col > num_cluster: + print(f'num_col: {num_col} > num_cluster: {num_cluster}. Reset num_col to num_cluster.') + num_col = num_cluster + df = df.loc[(df!=0).any(axis=1)] - self.show_message(f'Plotting trends cluster...') + self.show_message('Plotting trends cluster...') # plot trends and get cluster table fig, cluster_df = TrendsPlot(self.tfa).plot_trends(df= df, num_cluster = num_cluster, width=width, height=height, title=title - , font_size=font_size) + , font_size=font_size, num_col=num_col) # create a dialog to show the figure # plt_dialog = PltDialog(self.MainWindow, fig) - plt_size= (width*50,height*num_cluster*50) + plt_size= (width*50, int(height*num_cluster*50/num_col) ) plt_dialog = ExportablePlotDialog(self.MainWindow,fig, plt_size) #set title plt_dialog.setWindowTitle(title) @@ -4431,7 +4439,7 @@ def plot_top_heatmap(self): self.logger.write_log(f'plot_top_heatmap error: {error_message}') self.logger.write_log(f'plot_top_heatmap: table_name: {table_name}, top_num: {top_num}, value_type: {value_type}, fig_size: {fig_size}, pvalue: {pvalue}, sort_by: {sort_by}, cmap: {cmap}, scale: {scale}', 'e') if 'No significant' in error_message: - QMessageBox.warning(self.MainWindow, 'Warning', f'No significant results.') + QMessageBox.warning(self.MainWindow, 'Warning', f'No significant results. \n\n{error_message}') else: QMessageBox.warning(self.MainWindow, 'Error', f'{error_message}') @@ -4495,14 +4503,15 @@ def get_top_cross_table(self): rename_taxa=rename_taxa, sort_by = sort_by, scale_method = scale_method, return_type = 'table') - except ValueError as e: - QMessageBox.warning(self.MainWindow, 'Warning', f'No significant results.\n\n{e}') - return None except Exception as e: error_message = traceback.format_exc() self.logger.write_log(f'get_top_cross_table error: {error_message}', 'e') self.logger.write_log(f'get_top_cross_table: table_name: {table_name}, top_num: {top_num}, value_type: {value_type}, pvalue: {pvalue}, sort_by: {sort_by}', 'e') - QMessageBox.warning(self.MainWindow, 'Erro', error_message) + if 'No significant' in error_message: + QMessageBox.warning(self.MainWindow, 'Warning', f'No significant results.\n\n{error_message}') + else: + QMessageBox.warning(self.MainWindow, 'Error', f'{error_message}') + return None try: diff --git a/metax/gui/metax_gui/main_window.ui b/metax/gui/metax_gui/main_window.ui index f266400..0eb8b4e 100644 --- a/metax/gui/metax_gui/main_window.ui +++ b/metax/gui/metax_gui/main_window.ui @@ -7,7 +7,7 @@ 0 0 1122 - 794 + 774 @@ -46,7 +46,7 @@ Qt::LeftToRight - 2 + 5 false @@ -246,7 +246,7 @@ 0 0 528 - 569 + 549 @@ -937,6 +937,11 @@ Cube root transformation + + + Box-Cox + + @@ -2730,8 +2735,8 @@ 0 0 - 1016 - 158 + 999 + 150 @@ -5602,7 +5607,7 @@ 0 0 996 - 140 + 120 @@ -6120,7 +6125,7 @@ QTabWidget::Triangular - 0 + 1 @@ -6153,7 +6158,7 @@ 0 0 1016 - 179 + 169 @@ -7382,60 +7387,31 @@ 0 0 - 538 - 63 + 1016 + 120 - - - - - 75 - true - - - - General - - - - - + + 0 0 - - 1 - - - 200 - - - 9 - - - - - - - - 75 - true - - - Specific cluster + Simplify Taxa Names + + + true - - + + 0 @@ -7443,37 +7419,37 @@ - Get Intnsity Results + Plot Samples - - - - - 0 - 0 - + + + + + 75 + true + - Font Size + General - - - - - 0 - 0 - + + + + + 75 + true + - Height + Specific cluster - + @@ -7489,21 +7465,8 @@ - - - - - 0 - 0 - - - - Plot Samples - - - - - + + 0 @@ -7511,60 +7474,135 @@ - Simplify Taxa Names - - - true + Get Intnsity Results - - - - 0 - 0 - - - - Width - - + + + + + + 0 + 0 + + + + Width + + + + + + + + 0 + 0 + + + + 1 + + + 200 + + + 16 + + + + - - - - 0 - 0 - - - - 1 - - - 200 - - - 16 - - + + + + + + 0 + 0 + + + + Height + + + + + + + + 0 + 0 + + + + 1 + + + 200 + + + 9 + + + + - - - - - 0 - 0 - - - - 1 - - - 10 - - + + + + + + + 0 + 0 + + + + Font Size + + + + + + + + 0 + 0 + + + + 1 + + + 10 + + + + + + + + + + + Number of Col for Cluster Plot + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 1 + + + + diff --git a/metax/gui/metax_gui/ui_main_window.py b/metax/gui/metax_gui/ui_main_window.py index ba6a8a6..3f40a9e 100644 --- a/metax/gui/metax_gui/ui_main_window.py +++ b/metax/gui/metax_gui/ui_main_window.py @@ -14,7 +14,7 @@ class Ui_metaX_main(object): def setupUi(self, metaX_main): metaX_main.setObjectName("metaX_main") - metaX_main.resize(1122, 794) + metaX_main.resize(1122, 774) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -147,7 +147,7 @@ def setupUi(self, metaX_main): self.toolBox_2.setMaximumSize(QtCore.QSize(1677, 16777215)) self.toolBox_2.setObjectName("toolBox_2") self.page_2 = QtWidgets.QWidget() - self.page_2.setGeometry(QtCore.QRect(0, 0, 528, 569)) + self.page_2.setGeometry(QtCore.QRect(0, 0, 528, 549)) self.page_2.setObjectName("page_2") self.gridLayout_27 = QtWidgets.QGridLayout(self.page_2) self.gridLayout_27.setObjectName("gridLayout_27") @@ -488,6 +488,7 @@ def setupUi(self, metaX_main): self.comboBox_set_data_transformation.addItem("") self.comboBox_set_data_transformation.addItem("") self.comboBox_set_data_transformation.addItem("") + self.comboBox_set_data_transformation.addItem("") self.gridLayout_15.addWidget(self.comboBox_set_data_transformation, 4, 1, 1, 3) self.comboBox_set_data_normalization = QtWidgets.QComboBox(self.tab_set_taxa_func) self.comboBox_set_data_normalization.setLayoutDirection(QtCore.Qt.LeftToRight) @@ -1394,7 +1395,7 @@ def setupUi(self, metaX_main): self.scrollArea_2.setWidgetResizable(True) self.scrollArea_2.setObjectName("scrollArea_2") self.scrollAreaWidgetContents_2 = QtWidgets.QWidget() - self.scrollAreaWidgetContents_2.setGeometry(QtCore.QRect(0, 0, 1016, 158)) + self.scrollAreaWidgetContents_2.setGeometry(QtCore.QRect(0, 0, 999, 150)) self.scrollAreaWidgetContents_2.setObjectName("scrollAreaWidgetContents_2") self.gridLayout_50 = QtWidgets.QGridLayout(self.scrollAreaWidgetContents_2) self.gridLayout_50.setObjectName("gridLayout_50") @@ -2868,7 +2869,7 @@ def setupUi(self, metaX_main): self.scrollArea_3.setWidgetResizable(True) self.scrollArea_3.setObjectName("scrollArea_3") self.scrollAreaWidgetContents_4 = QtWidgets.QWidget() - self.scrollAreaWidgetContents_4.setGeometry(QtCore.QRect(0, 0, 996, 140)) + self.scrollAreaWidgetContents_4.setGeometry(QtCore.QRect(0, 0, 996, 120)) self.scrollAreaWidgetContents_4.setObjectName("scrollAreaWidgetContents_4") self.gridLayout_68 = QtWidgets.QGridLayout(self.scrollAreaWidgetContents_4) self.gridLayout_68.setObjectName("gridLayout_68") @@ -3169,7 +3170,7 @@ def setupUi(self, metaX_main): self.scrollArea_4.setWidgetResizable(True) self.scrollArea_4.setObjectName("scrollArea_4") self.scrollAreaWidgetContents_5 = QtWidgets.QWidget() - self.scrollAreaWidgetContents_5.setGeometry(QtCore.QRect(0, 0, 1016, 179)) + self.scrollAreaWidgetContents_5.setGeometry(QtCore.QRect(0, 0, 1016, 169)) self.scrollAreaWidgetContents_5.setObjectName("scrollAreaWidgetContents_5") self.gridLayout_49 = QtWidgets.QGridLayout(self.scrollAreaWidgetContents_5) self.gridLayout_49.setObjectName("gridLayout_49") @@ -3814,12 +3815,29 @@ def setupUi(self, metaX_main): self.scrollArea_5.setWidgetResizable(True) self.scrollArea_5.setObjectName("scrollArea_5") self.scrollAreaWidgetContents_6 = QtWidgets.QWidget() - self.scrollAreaWidgetContents_6.setGeometry(QtCore.QRect(0, 0, 538, 63)) + self.scrollAreaWidgetContents_6.setGeometry(QtCore.QRect(0, 0, 1016, 120)) self.scrollAreaWidgetContents_6.setObjectName("scrollAreaWidgetContents_6") self.gridLayout_57 = QtWidgets.QGridLayout(self.scrollAreaWidgetContents_6) self.gridLayout_57.setObjectName("gridLayout_57") self.gridLayout_59 = QtWidgets.QGridLayout() self.gridLayout_59.setObjectName("gridLayout_59") + self.checkBox_trends_plot_interactive_rename_taxa = QtWidgets.QCheckBox(self.scrollAreaWidgetContents_6) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.checkBox_trends_plot_interactive_rename_taxa.sizePolicy().hasHeightForWidth()) + self.checkBox_trends_plot_interactive_rename_taxa.setSizePolicy(sizePolicy) + self.checkBox_trends_plot_interactive_rename_taxa.setChecked(True) + self.checkBox_trends_plot_interactive_rename_taxa.setObjectName("checkBox_trends_plot_interactive_rename_taxa") + self.gridLayout_59.addWidget(self.checkBox_trends_plot_interactive_rename_taxa, 1, 7, 1, 1) + self.checkBox_trends_plot_interactive_plot_samples = QtWidgets.QCheckBox(self.scrollAreaWidgetContents_6) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.checkBox_trends_plot_interactive_plot_samples.sizePolicy().hasHeightForWidth()) + self.checkBox_trends_plot_interactive_plot_samples.setSizePolicy(sizePolicy) + self.checkBox_trends_plot_interactive_plot_samples.setObjectName("checkBox_trends_plot_interactive_plot_samples") + self.gridLayout_59.addWidget(self.checkBox_trends_plot_interactive_plot_samples, 1, 1, 1, 3) self.label_174 = QtWidgets.QLabel(self.scrollAreaWidgetContents_6) font = QtGui.QFont() font.setBold(True) @@ -3827,17 +3845,6 @@ def setupUi(self, metaX_main): self.label_174.setFont(font) self.label_174.setObjectName("label_174") self.gridLayout_59.addWidget(self.label_174, 0, 0, 1, 1) - self.spinBox_trends_height = QtWidgets.QSpinBox(self.scrollAreaWidgetContents_6) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.spinBox_trends_height.sizePolicy().hasHeightForWidth()) - self.spinBox_trends_height.setSizePolicy(sizePolicy) - self.spinBox_trends_height.setMinimum(1) - self.spinBox_trends_height.setMaximum(200) - self.spinBox_trends_height.setProperty("value", 9) - self.spinBox_trends_height.setObjectName("spinBox_trends_height") - self.gridLayout_59.addWidget(self.spinBox_trends_height, 0, 4, 1, 1) self.label_175 = QtWidgets.QLabel(self.scrollAreaWidgetContents_6) font = QtGui.QFont() font.setBold(True) @@ -3845,30 +3852,6 @@ def setupUi(self, metaX_main): self.label_175.setFont(font) self.label_175.setObjectName("label_175") self.gridLayout_59.addWidget(self.label_175, 1, 0, 1, 1) - self.checkBox_get_trends_cluster_intensity = QtWidgets.QCheckBox(self.scrollAreaWidgetContents_6) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.checkBox_get_trends_cluster_intensity.sizePolicy().hasHeightForWidth()) - self.checkBox_get_trends_cluster_intensity.setSizePolicy(sizePolicy) - self.checkBox_get_trends_cluster_intensity.setObjectName("checkBox_get_trends_cluster_intensity") - self.gridLayout_59.addWidget(self.checkBox_get_trends_cluster_intensity, 1, 3, 1, 2) - self.label_158 = QtWidgets.QLabel(self.scrollAreaWidgetContents_6) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_158.sizePolicy().hasHeightForWidth()) - self.label_158.setSizePolicy(sizePolicy) - self.label_158.setObjectName("label_158") - self.gridLayout_59.addWidget(self.label_158, 0, 5, 1, 1) - self.label_92 = QtWidgets.QLabel(self.scrollAreaWidgetContents_6) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_92.sizePolicy().hasHeightForWidth()) - self.label_92.setSizePolicy(sizePolicy) - self.label_92.setObjectName("label_92") - self.gridLayout_59.addWidget(self.label_92, 0, 3, 1, 1) self.checkBox_trends_plot_interactive_show_legend = QtWidgets.QCheckBox(self.scrollAreaWidgetContents_6) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -3877,24 +3860,17 @@ def setupUi(self, metaX_main): self.checkBox_trends_plot_interactive_show_legend.setSizePolicy(sizePolicy) self.checkBox_trends_plot_interactive_show_legend.setChecked(True) self.checkBox_trends_plot_interactive_show_legend.setObjectName("checkBox_trends_plot_interactive_show_legend") - self.gridLayout_59.addWidget(self.checkBox_trends_plot_interactive_show_legend, 1, 5, 1, 1) - self.checkBox_trends_plot_interactive_plot_samples = QtWidgets.QCheckBox(self.scrollAreaWidgetContents_6) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.checkBox_trends_plot_interactive_plot_samples.sizePolicy().hasHeightForWidth()) - self.checkBox_trends_plot_interactive_plot_samples.setSizePolicy(sizePolicy) - self.checkBox_trends_plot_interactive_plot_samples.setObjectName("checkBox_trends_plot_interactive_plot_samples") - self.gridLayout_59.addWidget(self.checkBox_trends_plot_interactive_plot_samples, 1, 1, 1, 2) - self.checkBox_trends_plot_interactive_rename_taxa = QtWidgets.QCheckBox(self.scrollAreaWidgetContents_6) + self.gridLayout_59.addWidget(self.checkBox_trends_plot_interactive_show_legend, 1, 6, 1, 1) + self.checkBox_get_trends_cluster_intensity = QtWidgets.QCheckBox(self.scrollAreaWidgetContents_6) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.checkBox_trends_plot_interactive_rename_taxa.sizePolicy().hasHeightForWidth()) - self.checkBox_trends_plot_interactive_rename_taxa.setSizePolicy(sizePolicy) - self.checkBox_trends_plot_interactive_rename_taxa.setChecked(True) - self.checkBox_trends_plot_interactive_rename_taxa.setObjectName("checkBox_trends_plot_interactive_rename_taxa") - self.gridLayout_59.addWidget(self.checkBox_trends_plot_interactive_rename_taxa, 1, 6, 1, 1) + sizePolicy.setHeightForWidth(self.checkBox_get_trends_cluster_intensity.sizePolicy().hasHeightForWidth()) + self.checkBox_get_trends_cluster_intensity.setSizePolicy(sizePolicy) + self.checkBox_get_trends_cluster_intensity.setObjectName("checkBox_get_trends_cluster_intensity") + self.gridLayout_59.addWidget(self.checkBox_get_trends_cluster_intensity, 1, 4, 1, 2) + self.horizontalLayout_97 = QtWidgets.QHBoxLayout() + self.horizontalLayout_97.setObjectName("horizontalLayout_97") self.label_97 = QtWidgets.QLabel(self.scrollAreaWidgetContents_6) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -3902,7 +3878,7 @@ def setupUi(self, metaX_main): sizePolicy.setHeightForWidth(self.label_97.sizePolicy().hasHeightForWidth()) self.label_97.setSizePolicy(sizePolicy) self.label_97.setObjectName("label_97") - self.gridLayout_59.addWidget(self.label_97, 0, 1, 1, 1) + self.horizontalLayout_97.addWidget(self.label_97) self.spinBox_trends_width = QtWidgets.QSpinBox(self.scrollAreaWidgetContents_6) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -3913,7 +3889,40 @@ def setupUi(self, metaX_main): self.spinBox_trends_width.setMaximum(200) self.spinBox_trends_width.setProperty("value", 16) self.spinBox_trends_width.setObjectName("spinBox_trends_width") - self.gridLayout_59.addWidget(self.spinBox_trends_width, 0, 2, 1, 1) + self.horizontalLayout_97.addWidget(self.spinBox_trends_width) + self.gridLayout_59.addLayout(self.horizontalLayout_97, 0, 1, 1, 1) + self.horizontalLayout_98 = QtWidgets.QHBoxLayout() + self.horizontalLayout_98.setObjectName("horizontalLayout_98") + self.label_92 = QtWidgets.QLabel(self.scrollAreaWidgetContents_6) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_92.sizePolicy().hasHeightForWidth()) + self.label_92.setSizePolicy(sizePolicy) + self.label_92.setObjectName("label_92") + self.horizontalLayout_98.addWidget(self.label_92) + self.spinBox_trends_height = QtWidgets.QSpinBox(self.scrollAreaWidgetContents_6) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.spinBox_trends_height.sizePolicy().hasHeightForWidth()) + self.spinBox_trends_height.setSizePolicy(sizePolicy) + self.spinBox_trends_height.setMinimum(1) + self.spinBox_trends_height.setMaximum(200) + self.spinBox_trends_height.setProperty("value", 9) + self.spinBox_trends_height.setObjectName("spinBox_trends_height") + self.horizontalLayout_98.addWidget(self.spinBox_trends_height) + self.gridLayout_59.addLayout(self.horizontalLayout_98, 0, 2, 1, 1) + self.horizontalLayout_99 = QtWidgets.QHBoxLayout() + self.horizontalLayout_99.setObjectName("horizontalLayout_99") + self.label_158 = QtWidgets.QLabel(self.scrollAreaWidgetContents_6) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_158.sizePolicy().hasHeightForWidth()) + self.label_158.setSizePolicy(sizePolicy) + self.label_158.setObjectName("label_158") + self.horizontalLayout_99.addWidget(self.label_158) self.spinBox_trends_font_size = QtWidgets.QSpinBox(self.scrollAreaWidgetContents_6) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -3923,7 +3932,19 @@ def setupUi(self, metaX_main): self.spinBox_trends_font_size.setMinimum(1) self.spinBox_trends_font_size.setProperty("value", 10) self.spinBox_trends_font_size.setObjectName("spinBox_trends_font_size") - self.gridLayout_59.addWidget(self.spinBox_trends_font_size, 0, 6, 1, 1) + self.horizontalLayout_99.addWidget(self.spinBox_trends_font_size) + self.gridLayout_59.addLayout(self.horizontalLayout_99, 0, 4, 1, 1) + self.horizontalLayout_100 = QtWidgets.QHBoxLayout() + self.horizontalLayout_100.setObjectName("horizontalLayout_100") + self.label_195 = QtWidgets.QLabel(self.scrollAreaWidgetContents_6) + self.label_195.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_195.setObjectName("label_195") + self.horizontalLayout_100.addWidget(self.label_195) + self.spinBox_trends_num_col = QtWidgets.QSpinBox(self.scrollAreaWidgetContents_6) + self.spinBox_trends_num_col.setMinimum(1) + self.spinBox_trends_num_col.setObjectName("spinBox_trends_num_col") + self.horizontalLayout_100.addWidget(self.spinBox_trends_num_col) + self.gridLayout_59.addLayout(self.horizontalLayout_100, 0, 5, 1, 3) self.gridLayout_57.addLayout(self.gridLayout_59, 0, 0, 1, 1) self.scrollArea_5.setWidget(self.scrollAreaWidgetContents_6) self.gridLayout_60.addWidget(self.scrollArea_5, 0, 0, 1, 1) @@ -5360,11 +5381,11 @@ def setupUi(self, metaX_main): self.retranslateUi(metaX_main) self.stackedWidget.setCurrentIndex(0) - self.tabWidget_TaxaFuncAnalyzer.setCurrentIndex(2) + self.tabWidget_TaxaFuncAnalyzer.setCurrentIndex(5) self.toolBox_2.setCurrentIndex(0) self.tabWidget_4.setCurrentIndex(1) self.tabWidget_3.setCurrentIndex(3) - self.tabWidget.setCurrentIndex(0) + self.tabWidget.setCurrentIndex(1) self.tabWidget_2.setCurrentIndex(0) self.tabWidget_6.setCurrentIndex(1) self.toolBox_metalab_res_anno.setCurrentIndex(0) @@ -5497,6 +5518,7 @@ def retranslateUi(self, metaX_main): self.comboBox_set_data_transformation.setItemText(2, _translate("metaX_main", "Log 10 transformation")) self.comboBox_set_data_transformation.setItemText(3, _translate("metaX_main", "Square root transformation")) self.comboBox_set_data_transformation.setItemText(4, _translate("metaX_main", "Cube root transformation")) + self.comboBox_set_data_transformation.setItemText(5, _translate("metaX_main", "Box-Cox")) self.comboBox_set_data_normalization.setItemText(0, _translate("metaX_main", "None")) self.comboBox_set_data_normalization.setItemText(1, _translate("metaX_main", "Standard Scaling (Z-Score)")) self.comboBox_set_data_normalization.setItemText(2, _translate("metaX_main", "Min-Max Scaling")) @@ -5914,15 +5936,16 @@ def retranslateUi(self, metaX_main): self.label_148.setText(_translate("metaX_main", "Meta")) self.label_100.setText(_translate("metaX_main", "Select for plotting")) self.groupBox_expression_trends_plot_settings.setTitle(_translate("metaX_main", "Plotting Parameter")) + self.checkBox_trends_plot_interactive_rename_taxa.setText(_translate("metaX_main", "Simplify Taxa Names")) + self.checkBox_trends_plot_interactive_plot_samples.setText(_translate("metaX_main", "Plot Samples")) self.label_174.setText(_translate("metaX_main", "General")) self.label_175.setText(_translate("metaX_main", "Specific cluster")) - self.checkBox_get_trends_cluster_intensity.setText(_translate("metaX_main", "Get Intnsity Results")) - self.label_158.setText(_translate("metaX_main", "Font Size")) - self.label_92.setText(_translate("metaX_main", "Height")) self.checkBox_trends_plot_interactive_show_legend.setText(_translate("metaX_main", "Show Legend")) - self.checkBox_trends_plot_interactive_plot_samples.setText(_translate("metaX_main", "Plot Samples")) - self.checkBox_trends_plot_interactive_rename_taxa.setText(_translate("metaX_main", "Simplify Taxa Names")) + self.checkBox_get_trends_cluster_intensity.setText(_translate("metaX_main", "Get Intnsity Results")) self.label_97.setText(_translate("metaX_main", "Width")) + self.label_92.setText(_translate("metaX_main", "Height")) + self.label_158.setText(_translate("metaX_main", "Font Size")) + self.label_195.setText(_translate("metaX_main", "Number of Col for Cluster Plot")) self.label_93.setText(_translate("metaX_main", "Select Cluster")) self.pushButton_trends_get_trends_table.setText(_translate("metaX_main", "Get ClusterTable")) self.label_165.setText(_translate("metaX_main", "Plot Specific Cluster")) diff --git a/metax/taxafunc_analyzer/analyzer_utils/basic_stats.py b/metax/taxafunc_analyzer/analyzer_utils/basic_stats.py index a88243b..d79f70b 100644 --- a/metax/taxafunc_analyzer/analyzer_utils/basic_stats.py +++ b/metax/taxafunc_analyzer/analyzer_utils/basic_stats.py @@ -1,5 +1,6 @@ import pandas as pd from collections import OrderedDict +from scipy import stats class BasicStats: def __init__(self, tfa): @@ -178,4 +179,27 @@ def get_combined_sub_meta_df( else: group_list = [self.tfa.get_group_of_a_sample(i) for i in df.columns] if not plot_mean else df.columns.tolist() - return df, group_list \ No newline at end of file + return df, group_list + + # Shapiro-Wilk Test + def shapiro_test(self, df: pd.DataFrame, alpha=0.05) : + """ + Perform Shapiro-Wilk test on the given DataFrame and return the results. + + Args: + df (pd.DataFrame): The DataFrame to be tested. + alpha (float, optional): The significance level. Defaults to 0.05. + + Returns: + dict: A dictionary containing the boolean result of the Shapiro-Wilk test for each sample. + """ + shapiro_results = {} + for sample in df[self.tfa.sample_list]: + values = df[sample].dropna() + # remove zero values + values = values[values != 0] + _, p = stats.shapiro(values) + # save the boolean result in the dictionary + shapiro_results[sample] = {'p_value': p, 'is_normal': p > alpha} + + return shapiro_results diff --git a/metax/taxafunc_analyzer/analyzer_utils/data_preprocessing.py b/metax/taxafunc_analyzer/analyzer_utils/data_preprocessing.py index c5673c7..fd53b73 100644 --- a/metax/taxafunc_analyzer/analyzer_utils/data_preprocessing.py +++ b/metax/taxafunc_analyzer/analyzer_utils/data_preprocessing.py @@ -2,6 +2,7 @@ from .re_combat import reComBat import numpy as np from joblib import Parallel, delayed +from scipy import stats @@ -71,7 +72,8 @@ def _data_transform(self, df: pd.DataFrame, transform_method: str = None) -> pd. 'cube': np.cbrt, 'log10': lambda x: np.log10(x + 1), 'log2': lambda x: np.log2(x + 1), - 'sqrt': np.sqrt + 'sqrt': np.sqrt, + 'boxcox': lambda x: x.apply(lambda col: stats.boxcox(col + 1)[0]) } if transform_method in transform_operations: diff --git a/metax/taxafunc_ploter/heatmap_plot.py b/metax/taxafunc_ploter/heatmap_plot.py index cf5c5b2..839262f 100644 --- a/metax/taxafunc_ploter/heatmap_plot.py +++ b/metax/taxafunc_ploter/heatmap_plot.py @@ -52,6 +52,10 @@ def rename_taxa(self, df): f'{i.split(" <")[0].split("|")[-1]} <{i.split(" <")[1][:-1]}>' for i in index_list ] + # check if the new_index_list is unique + if len(new_index_list) != len(set(new_index_list)): + raise ValueError("Duplicate taxa names after renaming!") + df.index = new_index_list return df @@ -246,6 +250,10 @@ def plot_basic_heatmap_of_test_res(self, df, top_number:int = 100, value_type:st else: raise ValueError("No 'f-statistic' or 't-statistic' in the dataframe") + # if mat is empty, raise error + if mat.empty: + raise ValueError(f"No significant differences between groups in {plot_type} <= [{pvalue}]") + if len(mat) < 2: row_cluster = False if len(mat.columns) < 2: @@ -338,7 +346,7 @@ def plot_basic_heatmap_of_test_res(self, df, top_number:int = 100, value_type:st except Exception as e: print(f'Error: {e}') plt.close('all') - raise ValueError("No significant differences") + raise ValueError(f"Error: {e}") # Plot basic heatmap of matrix with color bar diff --git a/metax/taxafunc_ploter/trends_plot.py b/metax/taxafunc_ploter/trends_plot.py index e342843..6522bc5 100644 --- a/metax/taxafunc_ploter/trends_plot.py +++ b/metax/taxafunc_ploter/trends_plot.py @@ -3,12 +3,13 @@ from sklearn.cluster import KMeans import matplotlib.pyplot as plt import seaborn as sns +import math class TrendsPlot: def __init__(self, tfobj): - self.tfobj = tfobj - - def plot_trends(self, df, num_cluster, width=15, height=5, title='Cluster', font_size=10): + self.tfobj = tfobj + + def plot_trends(self, df, num_cluster, width=15, height=5, title='Cluster', font_size=10, num_col=1): # Load the data df = self.tfobj.BasicStats.get_stats_mean_df_by_group(df) @@ -26,18 +27,19 @@ def plot_trends(self, df, num_cluster, width=15, height=5, title='Cluster', font # Add the cluster labels to the DataFrame clustered_df = scaled_df.copy() - # 不再重新排序列 - # adding the cluster column clustered_df['Cluster'] = clusters - - custom_params = {"axes.spines.right": False, "axes.spines.top": False} + custom_params = {"axes.spines.right": False, "axes.spines.top": False} sns.set_theme(style="ticks", rc=custom_params) palette = sns.color_palette("dark", n_clusters) - try: - fig, axs = plt.subplots(n_clusters, 1, figsize=(width, height*n_clusters)) - if n_clusters == 1: - axs = [axs] + + try: + # Calculate the number of rows based on num_col + num_row = math.ceil(n_clusters / num_col) + + fig, axs = plt.subplots(num_row, num_col, figsize=(width, height * num_row)) + axs = axs.flatten() # Flatten the axs array for easy iteration + for i in range(n_clusters): cluster_data = clustered_df[clustered_df['Cluster'] == i] avg_data = cluster_data.drop('Cluster', axis=1).mean() @@ -53,6 +55,11 @@ def plot_trends(self, df, num_cluster, width=15, height=5, title='Cluster', font axs[i].tick_params(axis='x', rotation=90) # Rotate x-axis labels axs[i].tick_params(axis='both', which='major', labelsize=font_size) + # Remove any empty subplots if n_clusters is not a perfect multiple of num_col + for ax in axs[n_clusters:]: + ax.remove() + + plt.tight_layout() plt.close() return fig, clustered_df except Exception as e: diff --git a/metax/utils/version.py b/metax/utils/version.py index eab6ab7..d09d4cc 100644 --- a/metax/utils/version.py +++ b/metax/utils/version.py @@ -1,2 +1,2 @@ -__version__ = '1.111.7' +__version__ = '1.111.8' API_version = '2' \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 74307c6..95cd768 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "MetaXTools" -version = "1.111.7" +version = "1.111.8" description = "MetaXTools is a novel tool for linking peptide sequences with taxonomic and functional information in Metaproteomics." readme = "README_PyPi.md" license = { text = "NorthOmics" } From bd260b9ba37c3a23c27ee7bb94bc15cc49ad18fe Mon Sep 17 00:00:00 2001 From: Qing <44231502+byemaxx@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:34:36 -0400 Subject: [PATCH 2/4] - New: Added sub_meta for taxa-func link part. - Change: Chnaged the [Get Table] of the taxa-func link part to get the table from the heatmap(values and order). --- Docs/ChangeLog.md | 8 +- metax/gui/main_gui.py | 96 ++-- metax/gui/metax_gui/main_window.ui | 678 ++++++++++++++------------ metax/gui/metax_gui/ui_main_window.py | 372 +++++++------- metax/taxafunc_ploter/bar_plot_js.py | 29 +- metax/taxafunc_ploter/heatmap_plot.py | 39 +- metax/taxafunc_ploter/line_plot.py | 80 ++- metax/utils/version.py | 2 +- pyproject.toml | 2 +- 9 files changed, 709 insertions(+), 597 deletions(-) diff --git a/Docs/ChangeLog.md b/Docs/ChangeLog.md index ca74a4b..0030b11 100644 --- a/Docs/ChangeLog.md +++ b/Docs/ChangeLog.md @@ -1,4 +1,10 @@ -# Version: 1.1118 +# Version: 1.112.0 +## Date: 2024-09-09 +### Changes: +- New: Added sub_meta for taxa-func link part. +- Change: Chnaged the [Get Table] of the taxa-func link part to get the table from the heatmap(values and order). + +# Version: 1.111.8 ## Date: 2024-09-08 ### Changes: - New: 1. Added Box-Cox method of Data Preprocessing. 2. Added an option to change number of cos of trends clusuter figure. diff --git a/metax/gui/main_gui.py b/metax/gui/main_gui.py index 49c4a67..d3b668b 100644 --- a/metax/gui/main_gui.py +++ b/metax/gui/main_gui.py @@ -361,6 +361,7 @@ def __init__(self, MainWindow): self.checkBox_create_protein_table.stateChanged.connect(self.change_event_checkBox_create_protein_table) self.comboBox_method_of_protein_inference.currentIndexChanged.connect(self.update_method_of_protein_inference) self.comboBox_3dbar_sub_meta.currentIndexChanged.connect(self.change_event_comboBox_3dbar_sub_meta) + self.comboBox_tflink_sub_meta.currentIndexChanged.connect(self.change_event_comboBox_tflink_sub_meta) ## Basic Stat self.pushButton_plot_pca_sns.clicked.connect(lambda: self.plot_basic_info_sns('pca')) @@ -473,8 +474,8 @@ def __init__(self, MainWindow): self.comboBox_tfnet_select_list.add_all_searched.connect(self.add_all_searched_tfnet_to_focus_list) # Taxa-func link - self.pushButton_others_get_intensity_matrix.clicked.connect(self.get_tflink_intensity_matrix) - self.pushButton_others_plot_heatmap.clicked.connect(self.plot_tflink_heatmap) + self.pushButton_others_get_intensity_matrix.clicked.connect(lambda: self.plot_tflink_heatmap('table')) + self.pushButton_others_plot_heatmap.clicked.connect(lambda: self.plot_tflink_heatmap('fig')) self.pushButton_others_plot_line.clicked.connect(self.plot_tflink_bar) self.pushButton_others_show_linked_taxa.clicked.connect(self.show_others_linked_taxa) self.pushButton_others_show_linked_func.clicked.connect(self.show_others_linked_func) @@ -671,7 +672,9 @@ def update_all_condition_meta(self): self.comboBox_sub_meta_pca.addItems(['None'] + meta_list) self.comboBox_3dbar_sub_meta.clear() self.comboBox_3dbar_sub_meta.addItems(['None'] + meta_list) - + self.comboBox_tflink_sub_meta.clear() + self.comboBox_tflink_sub_meta.addItems(['None'] + meta_list) + except Exception as e: print(e) @@ -781,6 +784,13 @@ def change_event_comboBox_3dbar_sub_meta(self): # self.comboBox_3dbar_sub_meta.setEnabled(False) # else: # self.comboBox_3dbar_sub_meta.setEnabled(True) + def change_event_comboBox_tflink_sub_meta(self): + # when the sub_meta comboBox is not None, the mean plot is not available + if self.comboBox_tflink_sub_meta.currentText() != 'None': + self.checkBox_tflink_plot_mean.setEnabled(False) + + else: + self.checkBox_tflink_plot_mean.setEnabled(True) def hide_plot_setting_groupbox(self): groupbox_list = ["groupBox_basic_plot", "groupBox_basic_heatmap_plot_settings", @@ -3547,7 +3557,7 @@ def plot_basic_list(self, plot_type='heatmap'): scale=scale, row_cluster=row_cluster, col_cluster=col_cluster, cmap=cmap, rename_taxa=rename_taxa, font_size=font_size, show_all_labels=show_all_labels, rename_sample=rename_sample, - plot_mean = plot_mean, sub_meta = sub_meta) + plot_mean = plot_mean, sub_meta = sub_meta, return_type = 'fig') elif plot_type == 'bar': @@ -5327,39 +5337,7 @@ def plot_network(self): self.logger.write_log(f'plot_network error: {error_message}', 'e') self.logger.write_log(f'plot_network: sample_list:{sample_list}, focus_list:{focus_list}, plot_list_only:{plot_list_only}', 'e') QMessageBox.warning(self.MainWindow, 'Error', f'{error_message}') - - # link - def get_tflink_intensity_matrix(self): - taxa = self.remove_pep_num_str_and_strip(self.comboBox_others_taxa.currentText()) - func = self.remove_pep_num_str_and_strip(self.comboBox_others_func.currentText()) - - if not taxa and not func: - QMessageBox.warning(self.MainWindow, 'Warning', 'Please select taxa or function!') - return None - - params = {} - - # extract sample list - sample_list = self.get_sample_list_tflink() - - params['sample_list'] = sample_list - - if taxa: - params['taxon_name'] = taxa - if func: - params['func_name'] = func - - df = self.tfa.GetMatrix.get_intensity_matrix(**params) - - if not df.empty: - if self.checkBox_tflink_hetatmap_rename_taxa.isChecked(): - df = self.tfa.rename_taxa(df) - if self.checkBox_tflink_plot_mean.isChecked(): - df = self.tfa.BasicStats.get_stats_mean_df_by_group(df) - self.show_table(df, title=f'{taxa} [ {func} ]') - else: - QMessageBox.warning(self.MainWindow, 'Warning', 'No data!, please reselect!') - + def get_sample_list_tflink(self): # get sample list @@ -5441,7 +5419,7 @@ def filter_tflink(self): pass # Plot Heatmap - def plot_tflink_heatmap(self): + def plot_tflink_heatmap(self, return_type = 'fig'): taxa = self.remove_pep_num_str_and_strip(self.comboBox_others_taxa.currentText()) func = self.remove_pep_num_str_and_strip(self.comboBox_others_func.currentText()) width = self.spinBox_tflink_width.value() @@ -5452,21 +5430,14 @@ def plot_tflink_heatmap(self): rename_taxa = self.checkBox_tflink_hetatmap_rename_taxa.isChecked() show_all_labels = (self.checkBox_tflink_bar_show_all_labels_x.isChecked(), self.checkBox_tflink_bar_show_all_labels_y.isChecked()) plot_mean = self.checkBox_tflink_plot_mean.isChecked() + rename_sample=self.checkBox_tflink_hetatmap_rename_sample.isChecked() + row_cluster = True if self.checkBox_tflink_hetatmap_row_cluster.isChecked() else False + col_cluster = True if self.checkBox_tflink_hetatmap_col_cluster.isChecked() else False + sub_meta = self.comboBox_tflink_sub_meta.currentText() if cmap == 'Auto': cmap = None - row_cluster = False - col_cluster = False - - if self.checkBox_tflink_hetatmap_row_cluster.isChecked(): - row_cluster = True - - if self.checkBox_tflink_hetatmap_col_cluster.isChecked(): - col_cluster = True - - - if not taxa and not func: QMessageBox.warning(self.MainWindow, 'Warning', 'Please select taxa or function!') return None @@ -5477,10 +5448,18 @@ def plot_tflink_heatmap(self): if taxa: params['taxon_name'] = taxa - title = taxa + if rename_taxa: + short_taxa = taxa.split('|')[-1] + else: + short_taxa = taxa + title = short_taxa + if func: params['func_name'] = func - title = func if not title else f"{taxa} [ {func} ]" + title = func + + if taxa and func: + title = f"{short_taxa}\n{func}" df = self.tfa.GetMatrix.get_intensity_matrix(**params) @@ -5497,12 +5476,17 @@ def plot_tflink_heatmap(self): df = self.delete_zero_columns(df) try: - self.show_message('Plotting heatmap, please wait...') - HeatmapPlot(self.tfa, **self.heatmap_params_dict).plot_basic_heatmap(df=df, title=title, fig_size=(int(width), int(height)), + self.show_message('Plotting heatmap, please wait...') if return_type == 'fig' else self.show_message('Calculating data, please wait...') + fig_res = HeatmapPlot(self.tfa, **self.heatmap_params_dict).plot_basic_heatmap(df=df, title=title, fig_size=(int(width), int(height)), scale=scale, row_cluster=row_cluster, col_cluster=col_cluster, cmap=cmap, rename_taxa=rename_taxa, font_size=font_size, show_all_labels=show_all_labels, - rename_sample=self.checkBox_tflink_hetatmap_rename_sample.isChecked(), plot_mean=plot_mean + rename_sample=rename_sample, sub_meta=sub_meta, + plot_mean=plot_mean, return_type = return_type ) + + if return_type == 'table': + self.show_table(fig_res, title=title.replace('\n', '-')) + except Exception as e: error_message = traceback.format_exc() self.logger.write_log(f'plot_others_heatmap error: {error_message}', 'e') @@ -5554,7 +5538,8 @@ def plot_tflink_bar(self): show_legend = self.checkBox_tflink_bar_show_legend.isChecked() plot_mean = self.checkBox_tflink_plot_mean.isChecked() show_all_labels = (self.checkBox_tflink_bar_show_all_labels_x.isChecked(), self.checkBox_tflink_bar_show_all_labels_y.isChecked()) - + sub_meta = self.comboBox_tflink_sub_meta.currentText() + if not taxa and not func: QMessageBox.warning(self.MainWindow, 'Warning', 'Please select taxa or function!') @@ -5595,6 +5580,7 @@ def plot_tflink_bar(self): params['font_size'] = font_size params['plot_mean'] = plot_mean params['show_all_labels'] = show_all_labels + params['sub_meta'] = sub_meta self.show_message('Plotting bar plot, please wait...') pic = BarPlot_js(self.tfa, theme=self.html_theme).plot_intensity_bar(**params) diff --git a/metax/gui/metax_gui/main_window.ui b/metax/gui/metax_gui/main_window.ui index 0eb8b4e..d116c41 100644 --- a/metax/gui/metax_gui/main_window.ui +++ b/metax/gui/metax_gui/main_window.ui @@ -46,7 +46,7 @@ Qt::LeftToRight - 5 + 6 false @@ -246,7 +246,7 @@ 0 0 528 - 549 + 553 @@ -1464,7 +1464,7 @@ 0 0 - 660 + 1016 232 @@ -5607,7 +5607,7 @@ 0 0 996 - 120 + 124 @@ -6157,8 +6157,8 @@ 0 0 - 1016 - 169 + 493 + 128 @@ -7388,7 +7388,7 @@ 0 0 1016 - 120 + 124 @@ -7759,144 +7759,6 @@ Taxa-Func Link - - - - - 0 - 0 - - - - true - - - - - - - - - - 0 - 0 - - - - Linked Number: - - - - - - - - false - - - - 0 - 0 - - - - Show Linked Taxa Only - - - - - - - - - - - - 0 - 0 - - - - Linked Number: - - - - - - - - false - - - - 0 - 0 - - - - Show Linked Func Only - - - - - - - - - - 0 - 0 - - - - Sample - - - - - - - Qt::Horizontal - - - - - - - Qt::Horizontal - - - - - - - - - - - 0 - 0 - - - - Group - - - true - - - - - - - - 0 - 0 - - - - true - - - @@ -7955,7 +7817,7 @@ - Get Intensity Table + Get Heatmap Table @@ -7968,21 +7830,18 @@ - - - - - 0 - 0 - - - - Meta + + + + Qt::Horizontal - - + + + + + 0 @@ -7990,90 +7849,46 @@ - Function + Group + + + true - - + + - + - + 0 0 - - Qt::LeftToRight - - In Condition + Linked Number: - - + false - + 0 0 + + Show Linked Taxa Only + - - - - - - - - false - - - - 0 - 0 - - - - - 300 - 16777215 - - - - - - - - - - - - Qt::Horizontal - - - - - - - - 0 - 0 - - - - Qt::LeftToRight - - - @@ -8093,22 +7908,6 @@ - - - - - 0 - 0 - - - - Taxa - - - - - - @@ -8246,13 +8045,42 @@ - - - - - 16777215 - 220 - + + + + + + + + 0 + 0 + + + + Taxa + + + + + + + + 0 + 0 + + + + Meta + + + + + + + + 16777215 + 220 + Plotting Parameter @@ -8708,6 +8536,198 @@ + + + + + 0 + 0 + + + + Qt::LeftToRight + + + + + + + + + + 0 + 0 + + + + Linked Number: - + + + + + + + false + + + + 0 + 0 + + + + Show Linked Func Only + + + + + + + + + + 0 + 0 + + + + Sample + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + + + + + + + 0 + 0 + + + + Qt::LeftToRight + + + In Condition + + + + + + + false + + + + 0 + 0 + + + + + + + + + + + + false + + + + 0 + 0 + + + + + 300 + 16777215 + + + + + + + + + + + + + + + 0 + 0 + + + + true + + + + + + + + 0 + 0 + + + + Function + + + + + + + + 0 + 0 + + + + true + + + + + + + + + + 0 + 0 + + + + Sub Meta + + + + + + + + @@ -10183,7 +10203,7 @@ 0 0 1122 - 23 + 21 @@ -10381,11 +10401,11 @@ 63 - 102 + 100 70 - 102 + 100 @@ -10397,11 +10417,11 @@ 63 - 102 + 100 77 - 103 + 101 @@ -10413,11 +10433,11 @@ 96 - 96 + 94 108 - 96 + 94 @@ -10429,11 +10449,11 @@ 96 - 96 + 94 119 - 97 + 95 @@ -10445,11 +10465,11 @@ 96 - 95 + 93 108 - 95 + 93 @@ -10461,11 +10481,11 @@ 96 - 95 + 93 119 - 96 + 94 @@ -10508,12 +10528,12 @@ setEnabled(bool) - 87 - 107 + 170 + 195 - 99 - 107 + 269 + 197 @@ -10525,11 +10545,11 @@ 95 - 97 + 95 108 - 97 + 95 @@ -10541,11 +10561,11 @@ 95 - 97 + 95 119 - 98 + 96 @@ -10557,11 +10577,11 @@ 96 - 96 + 94 108 - 96 + 94 @@ -10573,11 +10593,11 @@ 96 - 96 + 94 119 - 97 + 95 @@ -10588,12 +10608,12 @@ setEnabled(bool) - 700 - 192 + 1056 + 522 - 265 - 229 + 289 + 559 @@ -10604,12 +10624,12 @@ setEnabled(bool) - 700 - 192 + 1056 + 522 - 265 - 358 + 289 + 688 @@ -10620,12 +10640,12 @@ setEnabled(bool) - 700 - 192 + 1056 + 522 - 604 - 231 + 845 + 561 @@ -10636,12 +10656,12 @@ setEnabled(bool) - 87 - 107 + 170 + 195 - 110 - 108 + 480 + 197 @@ -10653,11 +10673,11 @@ 330 - 168 + 166 423 - 168 + 166 @@ -10684,12 +10704,12 @@ setEnabled(bool) - 134 - 151 + 63 + 99 - 227 - 153 + 70 + 99 @@ -10700,12 +10720,12 @@ setEnabled(bool) - 134 - 151 + 63 + 99 - 309 - 153 + 227 + 100 @@ -10716,12 +10736,12 @@ setEnabled(bool) - 63 - 101 + 149 + 151 - 73 - 101 + 246 + 153 @@ -10732,12 +10752,12 @@ setEnabled(bool) - 63 - 101 + 149 + 151 - 82 - 102 + 449 + 153 @@ -10748,12 +10768,12 @@ setVisible(bool) - 63 - 109 + 76 + 436 - 121 - 112 + 134 + 512 @@ -10780,12 +10800,12 @@ setVisible(bool) - 101 - 559 + 123 + 541 - 92 - 730 + 113 + 712 @@ -10812,12 +10832,12 @@ setVisible(bool) - 151 - 503 + 77 + 112 - 175 - 727 + 121 + 114 @@ -10828,12 +10848,12 @@ setVisible(bool) - 52 - 122 + 66 + 543 - 121 - 117 + 134 + 620 @@ -10844,12 +10864,12 @@ setVisible(bool) - 119 - 456 + 133 + 444 - 149 - 727 + 162 + 709 @@ -10861,11 +10881,11 @@ 55 - 114 + 112 - 113 - 116 + 121 + 114 @@ -10901,5 +10921,21 @@ + + checkBox_tflink_plot_mean + clicked(bool) + comboBox_tflink_sub_meta + setDisabled(bool) + + + 1019 + 542 + + + 553 + 114 + + + diff --git a/metax/gui/metax_gui/ui_main_window.py b/metax/gui/metax_gui/ui_main_window.py index 3f40a9e..bf6f8f6 100644 --- a/metax/gui/metax_gui/ui_main_window.py +++ b/metax/gui/metax_gui/ui_main_window.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'c:\Users\max\OneDrive - University of Ottawa\code\TaxaFunc\MetaX\metax\gui\metax_gui\main_window.ui' +# Form implementation generated from reading ui file 'c:\Users\Qing\OneDrive - University of Ottawa\code\TaxaFunc\MetaX\metax\gui\metax_gui\main_window.ui' # # Created by: PyQt5 UI code generator 5.15.9 # @@ -147,7 +147,7 @@ def setupUi(self, metaX_main): self.toolBox_2.setMaximumSize(QtCore.QSize(1677, 16777215)) self.toolBox_2.setObjectName("toolBox_2") self.page_2 = QtWidgets.QWidget() - self.page_2.setGeometry(QtCore.QRect(0, 0, 528, 549)) + self.page_2.setGeometry(QtCore.QRect(0, 0, 528, 553)) self.page_2.setObjectName("page_2") self.gridLayout_27 = QtWidgets.QGridLayout(self.page_2) self.gridLayout_27.setObjectName("gridLayout_27") @@ -731,7 +731,7 @@ def setupUi(self, metaX_main): self.scrollArea.setWidgetResizable(True) self.scrollArea.setObjectName("scrollArea") self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 660, 232)) + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 1016, 232)) self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.gridLayout_34 = QtWidgets.QGridLayout(self.scrollAreaWidgetContents) self.gridLayout_34.setObjectName("gridLayout_34") @@ -2869,7 +2869,7 @@ def setupUi(self, metaX_main): self.scrollArea_3.setWidgetResizable(True) self.scrollArea_3.setObjectName("scrollArea_3") self.scrollAreaWidgetContents_4 = QtWidgets.QWidget() - self.scrollAreaWidgetContents_4.setGeometry(QtCore.QRect(0, 0, 996, 120)) + self.scrollAreaWidgetContents_4.setGeometry(QtCore.QRect(0, 0, 996, 124)) self.scrollAreaWidgetContents_4.setObjectName("scrollAreaWidgetContents_4") self.gridLayout_68 = QtWidgets.QGridLayout(self.scrollAreaWidgetContents_4) self.gridLayout_68.setObjectName("gridLayout_68") @@ -3170,7 +3170,7 @@ def setupUi(self, metaX_main): self.scrollArea_4.setWidgetResizable(True) self.scrollArea_4.setObjectName("scrollArea_4") self.scrollAreaWidgetContents_5 = QtWidgets.QWidget() - self.scrollAreaWidgetContents_5.setGeometry(QtCore.QRect(0, 0, 1016, 169)) + self.scrollAreaWidgetContents_5.setGeometry(QtCore.QRect(0, 0, 493, 128)) self.scrollAreaWidgetContents_5.setObjectName("scrollAreaWidgetContents_5") self.gridLayout_49 = QtWidgets.QGridLayout(self.scrollAreaWidgetContents_5) self.gridLayout_49.setObjectName("gridLayout_49") @@ -3815,7 +3815,7 @@ def setupUi(self, metaX_main): self.scrollArea_5.setWidgetResizable(True) self.scrollArea_5.setObjectName("scrollArea_5") self.scrollAreaWidgetContents_6 = QtWidgets.QWidget() - self.scrollAreaWidgetContents_6.setGeometry(QtCore.QRect(0, 0, 1016, 120)) + self.scrollAreaWidgetContents_6.setGeometry(QtCore.QRect(0, 0, 1016, 124)) self.scrollAreaWidgetContents_6.setObjectName("scrollAreaWidgetContents_6") self.gridLayout_57 = QtWidgets.QGridLayout(self.scrollAreaWidgetContents_6) self.gridLayout_57.setObjectName("gridLayout_57") @@ -4030,94 +4030,6 @@ def setupUi(self, metaX_main): self.tab_8.setObjectName("tab_8") self.gridLayout_4 = QtWidgets.QGridLayout(self.tab_8) self.gridLayout_4.setObjectName("gridLayout_4") - self.comboBox_others_func = QtWidgets.QComboBox(self.tab_8) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.comboBox_others_func.sizePolicy().hasHeightForWidth()) - self.comboBox_others_func.setSizePolicy(sizePolicy) - self.comboBox_others_func.setEditable(True) - self.comboBox_others_func.setObjectName("comboBox_others_func") - self.gridLayout_4.addWidget(self.comboBox_others_func, 6, 1, 1, 2) - self.horizontalLayout_81 = QtWidgets.QHBoxLayout() - self.horizontalLayout_81.setObjectName("horizontalLayout_81") - self.label_others_func_num = QtWidgets.QLabel(self.tab_8) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_others_func_num.sizePolicy().hasHeightForWidth()) - self.label_others_func_num.setSizePolicy(sizePolicy) - self.label_others_func_num.setObjectName("label_others_func_num") - self.horizontalLayout_81.addWidget(self.label_others_func_num) - self.pushButton_others_show_linked_taxa = QtWidgets.QPushButton(self.tab_8) - self.pushButton_others_show_linked_taxa.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.pushButton_others_show_linked_taxa.sizePolicy().hasHeightForWidth()) - self.pushButton_others_show_linked_taxa.setSizePolicy(sizePolicy) - self.pushButton_others_show_linked_taxa.setObjectName("pushButton_others_show_linked_taxa") - self.horizontalLayout_81.addWidget(self.pushButton_others_show_linked_taxa) - self.gridLayout_4.addLayout(self.horizontalLayout_81, 6, 3, 1, 1) - self.horizontalLayout_82 = QtWidgets.QHBoxLayout() - self.horizontalLayout_82.setObjectName("horizontalLayout_82") - self.label_others_taxa_num = QtWidgets.QLabel(self.tab_8) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_others_taxa_num.sizePolicy().hasHeightForWidth()) - self.label_others_taxa_num.setSizePolicy(sizePolicy) - self.label_others_taxa_num.setObjectName("label_others_taxa_num") - self.horizontalLayout_82.addWidget(self.label_others_taxa_num) - self.pushButton_others_show_linked_func = QtWidgets.QPushButton(self.tab_8) - self.pushButton_others_show_linked_func.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.pushButton_others_show_linked_func.sizePolicy().hasHeightForWidth()) - self.pushButton_others_show_linked_func.setSizePolicy(sizePolicy) - self.pushButton_others_show_linked_func.setObjectName("pushButton_others_show_linked_func") - self.horizontalLayout_82.addWidget(self.pushButton_others_show_linked_func) - self.gridLayout_4.addLayout(self.horizontalLayout_82, 7, 3, 1, 1) - self.radioButton_tflink_sample = QtWidgets.QRadioButton(self.tab_8) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.radioButton_tflink_sample.sizePolicy().hasHeightForWidth()) - self.radioButton_tflink_sample.setSizePolicy(sizePolicy) - self.radioButton_tflink_sample.setObjectName("radioButton_tflink_sample") - self.gridLayout_4.addWidget(self.radioButton_tflink_sample, 3, 0, 1, 1) - self.line_3 = QtWidgets.QFrame(self.tab_8) - self.line_3.setFrameShape(QtWidgets.QFrame.HLine) - self.line_3.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line_3.setObjectName("line_3") - self.gridLayout_4.addWidget(self.line_3, 1, 0, 1, 4) - self.line_6 = QtWidgets.QFrame(self.tab_8) - self.line_6.setFrameShape(QtWidgets.QFrame.HLine) - self.line_6.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line_6.setObjectName("line_6") - self.gridLayout_4.addWidget(self.line_6, 4, 0, 1, 4) - self.gridLayout_tflink_group = QtWidgets.QGridLayout() - self.gridLayout_tflink_group.setObjectName("gridLayout_tflink_group") - self.gridLayout_4.addLayout(self.gridLayout_tflink_group, 2, 2, 1, 2) - self.radioButton_tflink_group = QtWidgets.QRadioButton(self.tab_8) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.radioButton_tflink_group.sizePolicy().hasHeightForWidth()) - self.radioButton_tflink_group.setSizePolicy(sizePolicy) - self.radioButton_tflink_group.setChecked(True) - self.radioButton_tflink_group.setObjectName("radioButton_tflink_group") - self.gridLayout_4.addWidget(self.radioButton_tflink_group, 2, 0, 1, 1) - self.comboBox_others_taxa = QtWidgets.QComboBox(self.tab_8) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.comboBox_others_taxa.sizePolicy().hasHeightForWidth()) - self.comboBox_others_taxa.setSizePolicy(sizePolicy) - self.comboBox_others_taxa.setEditable(True) - self.comboBox_others_taxa.setObjectName("comboBox_others_taxa") - self.gridLayout_4.addWidget(self.comboBox_others_taxa, 7, 1, 1, 2) self.gridLayout_62 = QtWidgets.QGridLayout() self.gridLayout_62.setObjectName("gridLayout_62") self.pushButton_others_plot_heatmap = QtWidgets.QPushButton(self.tab_8) @@ -4153,73 +4065,43 @@ def setupUi(self, metaX_main): self.checkBox_6.setObjectName("checkBox_6") self.gridLayout_62.addWidget(self.checkBox_6, 1, 0, 1, 1) self.gridLayout_4.addLayout(self.gridLayout_62, 9, 0, 1, 4) - self.label_149 = QtWidgets.QLabel(self.tab_8) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_149.sizePolicy().hasHeightForWidth()) - self.label_149.setSizePolicy(sizePolicy) - self.label_149.setObjectName("label_149") - self.gridLayout_4.addWidget(self.label_149, 0, 0, 1, 1) - self.label_18 = QtWidgets.QLabel(self.tab_8) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_18.sizePolicy().hasHeightForWidth()) - self.label_18.setSizePolicy(sizePolicy) - self.label_18.setObjectName("label_18") - self.gridLayout_4.addWidget(self.label_18, 6, 0, 1, 1) - self.horizontalLayout_78 = QtWidgets.QHBoxLayout() - self.horizontalLayout_78.setObjectName("horizontalLayout_78") - self.checkBox_tflink_in_condition = QtWidgets.QCheckBox(self.tab_8) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.checkBox_tflink_in_condition.sizePolicy().hasHeightForWidth()) - self.checkBox_tflink_in_condition.setSizePolicy(sizePolicy) - self.checkBox_tflink_in_condition.setLayoutDirection(QtCore.Qt.LeftToRight) - self.checkBox_tflink_in_condition.setObjectName("checkBox_tflink_in_condition") - self.horizontalLayout_78.addWidget(self.checkBox_tflink_in_condition) - self.comboBox_tflink_condition_meta = QtWidgets.QComboBox(self.tab_8) - self.comboBox_tflink_condition_meta.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.comboBox_tflink_condition_meta.sizePolicy().hasHeightForWidth()) - self.comboBox_tflink_condition_meta.setSizePolicy(sizePolicy) - self.comboBox_tflink_condition_meta.setObjectName("comboBox_tflink_condition_meta") - self.horizontalLayout_78.addWidget(self.comboBox_tflink_condition_meta) - self.horizontalLayout_49 = QtWidgets.QHBoxLayout() - self.horizontalLayout_49.setObjectName("horizontalLayout_49") - self.horizontalLayout_77 = QtWidgets.QHBoxLayout() - self.horizontalLayout_77.setObjectName("horizontalLayout_77") - self.comboBox_tflink_condition_group = QtWidgets.QComboBox(self.tab_8) - self.comboBox_tflink_condition_group.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.comboBox_tflink_condition_group.sizePolicy().hasHeightForWidth()) - self.comboBox_tflink_condition_group.setSizePolicy(sizePolicy) - self.comboBox_tflink_condition_group.setMaximumSize(QtCore.QSize(300, 16777215)) - self.comboBox_tflink_condition_group.setObjectName("comboBox_tflink_condition_group") - self.horizontalLayout_77.addWidget(self.comboBox_tflink_condition_group) - self.horizontalLayout_49.addLayout(self.horizontalLayout_77) - self.horizontalLayout_78.addLayout(self.horizontalLayout_49) - self.gridLayout_4.addLayout(self.horizontalLayout_78, 2, 1, 1, 1) self.line_32 = QtWidgets.QFrame(self.tab_8) self.line_32.setFrameShape(QtWidgets.QFrame.HLine) self.line_32.setFrameShadow(QtWidgets.QFrame.Sunken) self.line_32.setObjectName("line_32") self.gridLayout_4.addWidget(self.line_32, 8, 0, 1, 4) - self.comboBox_tflink_meta = QtWidgets.QComboBox(self.tab_8) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + self.gridLayout_tflink_sample = QtWidgets.QGridLayout() + self.gridLayout_tflink_sample.setObjectName("gridLayout_tflink_sample") + self.gridLayout_4.addLayout(self.gridLayout_tflink_sample, 3, 1, 1, 3) + self.radioButton_tflink_group = QtWidgets.QRadioButton(self.tab_8) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.comboBox_tflink_meta.sizePolicy().hasHeightForWidth()) - self.comboBox_tflink_meta.setSizePolicy(sizePolicy) - self.comboBox_tflink_meta.setLayoutDirection(QtCore.Qt.LeftToRight) - self.comboBox_tflink_meta.setObjectName("comboBox_tflink_meta") - self.gridLayout_4.addWidget(self.comboBox_tflink_meta, 0, 1, 1, 1) + sizePolicy.setHeightForWidth(self.radioButton_tflink_group.sizePolicy().hasHeightForWidth()) + self.radioButton_tflink_group.setSizePolicy(sizePolicy) + self.radioButton_tflink_group.setChecked(True) + self.radioButton_tflink_group.setObjectName("radioButton_tflink_group") + self.gridLayout_4.addWidget(self.radioButton_tflink_group, 2, 0, 1, 1) + self.horizontalLayout_81 = QtWidgets.QHBoxLayout() + self.horizontalLayout_81.setObjectName("horizontalLayout_81") + self.label_others_func_num = QtWidgets.QLabel(self.tab_8) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_others_func_num.sizePolicy().hasHeightForWidth()) + self.label_others_func_num.setSizePolicy(sizePolicy) + self.label_others_func_num.setObjectName("label_others_func_num") + self.horizontalLayout_81.addWidget(self.label_others_func_num) + self.pushButton_others_show_linked_taxa = QtWidgets.QPushButton(self.tab_8) + self.pushButton_others_show_linked_taxa.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.pushButton_others_show_linked_taxa.sizePolicy().hasHeightForWidth()) + self.pushButton_others_show_linked_taxa.setSizePolicy(sizePolicy) + self.pushButton_others_show_linked_taxa.setObjectName("pushButton_others_show_linked_taxa") + self.horizontalLayout_81.addWidget(self.pushButton_others_show_linked_taxa) + self.gridLayout_4.addLayout(self.horizontalLayout_81, 6, 3, 1, 1) self.pushButton_others_fresh_taxa_func = QtWidgets.QPushButton(self.tab_8) self.pushButton_others_fresh_taxa_func.setEnabled(False) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) @@ -4229,17 +4111,6 @@ def setupUi(self, metaX_main): self.pushButton_others_fresh_taxa_func.setSizePolicy(sizePolicy) self.pushButton_others_fresh_taxa_func.setObjectName("pushButton_others_fresh_taxa_func") self.gridLayout_4.addWidget(self.pushButton_others_fresh_taxa_func, 5, 3, 1, 1) - self.label_19 = QtWidgets.QLabel(self.tab_8) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_19.sizePolicy().hasHeightForWidth()) - self.label_19.setSizePolicy(sizePolicy) - self.label_19.setObjectName("label_19") - self.gridLayout_4.addWidget(self.label_19, 7, 0, 1, 1) - self.gridLayout_tflink_sample = QtWidgets.QGridLayout() - self.gridLayout_tflink_sample.setObjectName("gridLayout_tflink_sample") - self.gridLayout_4.addLayout(self.gridLayout_tflink_sample, 3, 1, 1, 3) self.horizontalLayout_50 = QtWidgets.QHBoxLayout() self.horizontalLayout_50.setObjectName("horizontalLayout_50") self.label_75 = QtWidgets.QLabel(self.tab_8) @@ -4297,6 +4168,25 @@ def setupUi(self, metaX_main): self.pushButton_tflink_filter.setObjectName("pushButton_tflink_filter") self.horizontalLayout_50.addWidget(self.pushButton_tflink_filter) self.gridLayout_4.addLayout(self.horizontalLayout_50, 5, 1, 1, 2) + self.gridLayout_tflink_group = QtWidgets.QGridLayout() + self.gridLayout_tflink_group.setObjectName("gridLayout_tflink_group") + self.gridLayout_4.addLayout(self.gridLayout_tflink_group, 2, 2, 1, 2) + self.label_19 = QtWidgets.QLabel(self.tab_8) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_19.sizePolicy().hasHeightForWidth()) + self.label_19.setSizePolicy(sizePolicy) + self.label_19.setObjectName("label_19") + self.gridLayout_4.addWidget(self.label_19, 7, 0, 1, 1) + self.label_149 = QtWidgets.QLabel(self.tab_8) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_149.sizePolicy().hasHeightForWidth()) + self.label_149.setSizePolicy(sizePolicy) + self.label_149.setObjectName("label_149") + self.gridLayout_4.addWidget(self.label_149, 0, 0, 1, 1) self.groupBox_taxa_func_link_plot_settings = QtWidgets.QGroupBox(self.tab_8) self.groupBox_taxa_func_link_plot_settings.setMaximumSize(QtCore.QSize(16777215, 220)) self.groupBox_taxa_func_link_plot_settings.setObjectName("groupBox_taxa_func_link_plot_settings") @@ -4563,6 +4453,130 @@ def setupUi(self, metaX_main): self.scrollArea_6.setWidget(self.scrollAreaWidgetContents_7) self.gridLayout_65.addWidget(self.scrollArea_6, 0, 0, 1, 1) self.gridLayout_4.addWidget(self.groupBox_taxa_func_link_plot_settings, 10, 0, 1, 4) + self.comboBox_tflink_meta = QtWidgets.QComboBox(self.tab_8) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.comboBox_tflink_meta.sizePolicy().hasHeightForWidth()) + self.comboBox_tflink_meta.setSizePolicy(sizePolicy) + self.comboBox_tflink_meta.setLayoutDirection(QtCore.Qt.LeftToRight) + self.comboBox_tflink_meta.setObjectName("comboBox_tflink_meta") + self.gridLayout_4.addWidget(self.comboBox_tflink_meta, 0, 1, 1, 1) + self.horizontalLayout_82 = QtWidgets.QHBoxLayout() + self.horizontalLayout_82.setObjectName("horizontalLayout_82") + self.label_others_taxa_num = QtWidgets.QLabel(self.tab_8) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_others_taxa_num.sizePolicy().hasHeightForWidth()) + self.label_others_taxa_num.setSizePolicy(sizePolicy) + self.label_others_taxa_num.setObjectName("label_others_taxa_num") + self.horizontalLayout_82.addWidget(self.label_others_taxa_num) + self.pushButton_others_show_linked_func = QtWidgets.QPushButton(self.tab_8) + self.pushButton_others_show_linked_func.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.pushButton_others_show_linked_func.sizePolicy().hasHeightForWidth()) + self.pushButton_others_show_linked_func.setSizePolicy(sizePolicy) + self.pushButton_others_show_linked_func.setObjectName("pushButton_others_show_linked_func") + self.horizontalLayout_82.addWidget(self.pushButton_others_show_linked_func) + self.gridLayout_4.addLayout(self.horizontalLayout_82, 7, 3, 1, 1) + self.radioButton_tflink_sample = QtWidgets.QRadioButton(self.tab_8) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.radioButton_tflink_sample.sizePolicy().hasHeightForWidth()) + self.radioButton_tflink_sample.setSizePolicy(sizePolicy) + self.radioButton_tflink_sample.setObjectName("radioButton_tflink_sample") + self.gridLayout_4.addWidget(self.radioButton_tflink_sample, 3, 0, 1, 1) + self.line_3 = QtWidgets.QFrame(self.tab_8) + self.line_3.setFrameShape(QtWidgets.QFrame.HLine) + self.line_3.setFrameShadow(QtWidgets.QFrame.Sunken) + self.line_3.setObjectName("line_3") + self.gridLayout_4.addWidget(self.line_3, 1, 0, 1, 4) + self.line_6 = QtWidgets.QFrame(self.tab_8) + self.line_6.setFrameShape(QtWidgets.QFrame.HLine) + self.line_6.setFrameShadow(QtWidgets.QFrame.Sunken) + self.line_6.setObjectName("line_6") + self.gridLayout_4.addWidget(self.line_6, 4, 0, 1, 4) + self.horizontalLayout_78 = QtWidgets.QHBoxLayout() + self.horizontalLayout_78.setObjectName("horizontalLayout_78") + self.checkBox_tflink_in_condition = QtWidgets.QCheckBox(self.tab_8) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.checkBox_tflink_in_condition.sizePolicy().hasHeightForWidth()) + self.checkBox_tflink_in_condition.setSizePolicy(sizePolicy) + self.checkBox_tflink_in_condition.setLayoutDirection(QtCore.Qt.LeftToRight) + self.checkBox_tflink_in_condition.setObjectName("checkBox_tflink_in_condition") + self.horizontalLayout_78.addWidget(self.checkBox_tflink_in_condition) + self.comboBox_tflink_condition_meta = QtWidgets.QComboBox(self.tab_8) + self.comboBox_tflink_condition_meta.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.comboBox_tflink_condition_meta.sizePolicy().hasHeightForWidth()) + self.comboBox_tflink_condition_meta.setSizePolicy(sizePolicy) + self.comboBox_tflink_condition_meta.setObjectName("comboBox_tflink_condition_meta") + self.horizontalLayout_78.addWidget(self.comboBox_tflink_condition_meta) + self.horizontalLayout_49 = QtWidgets.QHBoxLayout() + self.horizontalLayout_49.setObjectName("horizontalLayout_49") + self.horizontalLayout_77 = QtWidgets.QHBoxLayout() + self.horizontalLayout_77.setObjectName("horizontalLayout_77") + self.comboBox_tflink_condition_group = QtWidgets.QComboBox(self.tab_8) + self.comboBox_tflink_condition_group.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.comboBox_tflink_condition_group.sizePolicy().hasHeightForWidth()) + self.comboBox_tflink_condition_group.setSizePolicy(sizePolicy) + self.comboBox_tflink_condition_group.setMaximumSize(QtCore.QSize(300, 16777215)) + self.comboBox_tflink_condition_group.setObjectName("comboBox_tflink_condition_group") + self.horizontalLayout_77.addWidget(self.comboBox_tflink_condition_group) + self.horizontalLayout_49.addLayout(self.horizontalLayout_77) + self.horizontalLayout_78.addLayout(self.horizontalLayout_49) + self.gridLayout_4.addLayout(self.horizontalLayout_78, 2, 1, 1, 1) + self.comboBox_others_func = QtWidgets.QComboBox(self.tab_8) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.comboBox_others_func.sizePolicy().hasHeightForWidth()) + self.comboBox_others_func.setSizePolicy(sizePolicy) + self.comboBox_others_func.setEditable(True) + self.comboBox_others_func.setObjectName("comboBox_others_func") + self.gridLayout_4.addWidget(self.comboBox_others_func, 6, 1, 1, 2) + self.label_18 = QtWidgets.QLabel(self.tab_8) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_18.sizePolicy().hasHeightForWidth()) + self.label_18.setSizePolicy(sizePolicy) + self.label_18.setObjectName("label_18") + self.gridLayout_4.addWidget(self.label_18, 6, 0, 1, 1) + self.comboBox_others_taxa = QtWidgets.QComboBox(self.tab_8) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.comboBox_others_taxa.sizePolicy().hasHeightForWidth()) + self.comboBox_others_taxa.setSizePolicy(sizePolicy) + self.comboBox_others_taxa.setEditable(True) + self.comboBox_others_taxa.setObjectName("comboBox_others_taxa") + self.gridLayout_4.addWidget(self.comboBox_others_taxa, 7, 1, 1, 2) + self.horizontalLayout_101 = QtWidgets.QHBoxLayout() + self.horizontalLayout_101.setObjectName("horizontalLayout_101") + self.label_196 = QtWidgets.QLabel(self.tab_8) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_196.sizePolicy().hasHeightForWidth()) + self.label_196.setSizePolicy(sizePolicy) + self.label_196.setObjectName("label_196") + self.horizontalLayout_101.addWidget(self.label_196) + self.comboBox_tflink_sub_meta = QtWidgets.QComboBox(self.tab_8) + self.comboBox_tflink_sub_meta.setObjectName("comboBox_tflink_sub_meta") + self.horizontalLayout_101.addWidget(self.comboBox_tflink_sub_meta) + self.gridLayout_4.addLayout(self.horizontalLayout_101, 0, 2, 1, 1) self.tabWidget_2.addTab(self.tab_8, "") self.tab_9 = QtWidgets.QWidget() self.tab_9.setObjectName("tab_9") @@ -5321,7 +5335,7 @@ def setupUi(self, metaX_main): self.statusbar.setObjectName("statusbar") metaX_main.setStatusBar(self.statusbar) self.menuBar = QtWidgets.QMenuBar(metaX_main) - self.menuBar.setGeometry(QtCore.QRect(0, 0, 1122, 23)) + self.menuBar.setGeometry(QtCore.QRect(0, 0, 1122, 21)) self.menuBar.setObjectName("menuBar") self.menuTools = QtWidgets.QMenu(self.menuBar) self.menuTools.setObjectName("menuTools") @@ -5381,7 +5395,7 @@ def setupUi(self, metaX_main): self.retranslateUi(metaX_main) self.stackedWidget.setCurrentIndex(0) - self.tabWidget_TaxaFuncAnalyzer.setCurrentIndex(5) + self.tabWidget_TaxaFuncAnalyzer.setCurrentIndex(6) self.toolBox_2.setCurrentIndex(0) self.tabWidget_4.setCurrentIndex(1) self.tabWidget_3.setCurrentIndex(3) @@ -5427,6 +5441,7 @@ def setupUi(self, metaX_main): self.checkBox_7.toggled['bool'].connect(self.groupBox_taxa_func_link_net_plot_settings.setVisible) # type: ignore self.checkBox_set_taxa_func_split_func.clicked['bool'].connect(self.lineEdit_set_taxa_func_split_func_sep.setEnabled) # type: ignore self.checkBox_set_taxa_func_split_func.clicked['bool'].connect(self.checkBox_set_taxa_func_split_func_share_intensity.setEnabled) # type: ignore + self.checkBox_tflink_plot_mean.clicked['bool'].connect(self.comboBox_tflink_sub_meta.setDisabled) # type: ignore QtCore.QMetaObject.connectSlotsByName(metaX_main) metaX_main.setTabOrder(self.comboBox_taxa_level_to_stast, self.toolButton_meta_table_help) metaX_main.setTabOrder(self.toolButton_meta_table_help, self.comboBox_function_to_stast) @@ -5956,22 +5971,15 @@ def retranslateUi(self, metaX_main): self.checkBox_5.setText(_translate("metaX_main", "Show Plotting Parameter")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_15), _translate("metaX_main", "Expression Trends")) self.tabWidget_TaxaFuncAnalyzer.setTabText(self.tabWidget_TaxaFuncAnalyzer.indexOf(self.tab_diff_stats), _translate("metaX_main", "Expression Analysis")) - self.label_others_func_num.setText(_translate("metaX_main", "Linked Number: -")) - self.pushButton_others_show_linked_taxa.setText(_translate("metaX_main", "Show Linked Taxa Only")) - self.label_others_taxa_num.setText(_translate("metaX_main", "Linked Number: -")) - self.pushButton_others_show_linked_func.setText(_translate("metaX_main", "Show Linked Func Only")) - self.radioButton_tflink_sample.setText(_translate("metaX_main", "Sample")) - self.radioButton_tflink_group.setText(_translate("metaX_main", "Group")) self.pushButton_others_plot_heatmap.setText(_translate("metaX_main", "Plot Heatmap")) self.pushButton_others_plot_line.setText(_translate("metaX_main", "Plot Bar")) - self.pushButton_others_get_intensity_matrix.setText(_translate("metaX_main", "Get Intensity Table")) + self.pushButton_others_get_intensity_matrix.setText(_translate("metaX_main", "Get Heatmap Table")) self.checkBox_6.setText(_translate("metaX_main", "Show Plotting Parameter")) - self.label_149.setText(_translate("metaX_main", "Meta")) - self.label_18.setText(_translate("metaX_main", "Function")) - self.checkBox_tflink_in_condition.setText(_translate("metaX_main", "In Condition")) + self.radioButton_tflink_group.setText(_translate("metaX_main", "Group")) + self.label_others_func_num.setText(_translate("metaX_main", "Linked Number: -")) + self.pushButton_others_show_linked_taxa.setText(_translate("metaX_main", "Show Linked Taxa Only")) self.pushButton_others_fresh_taxa_func.setToolTip(_translate("metaX_main", "Restore both lists to their original full items")) self.pushButton_others_fresh_taxa_func.setText(_translate("metaX_main", "Reset List")) - self.label_19.setText(_translate("metaX_main", "Taxa")) self.label_75.setText(_translate("metaX_main", "Filter Top")) self.label_76.setText(_translate("metaX_main", "By")) self.comboBox_tflink_top_by.setItemText(0, _translate("metaX_main", "Total Intensity")) @@ -5988,6 +5996,8 @@ def retranslateUi(self, metaX_main): self.checkBox_tflink_top_filtered.setText(_translate("metaX_main", "Filter with threshold")) self.pushButton_tflink_filter.setToolTip(_translate("metaX_main", "Filter items in the two lists by condition")) self.pushButton_tflink_filter.setText(_translate("metaX_main", "Filter")) + self.label_19.setText(_translate("metaX_main", "Taxa")) + self.label_149.setText(_translate("metaX_main", "Meta")) self.groupBox_taxa_func_link_plot_settings.setTitle(_translate("metaX_main", "Plotting Parameter")) self.label_178.setText(_translate("metaX_main", "Bar")) self.checkBox_tflink_hetatmap_col_cluster.setText(_translate("metaX_main", "Col Cluster")) @@ -6012,6 +6022,12 @@ def retranslateUi(self, metaX_main): self.comboBox_tflink_hetatmap_scale.setItemText(2, _translate("metaX_main", "column")) self.comboBox_tflink_hetatmap_scale.setItemText(3, _translate("metaX_main", "all")) self.label_61.setText(_translate("metaX_main", "Theme")) + self.label_others_taxa_num.setText(_translate("metaX_main", "Linked Number: -")) + self.pushButton_others_show_linked_func.setText(_translate("metaX_main", "Show Linked Func Only")) + self.radioButton_tflink_sample.setText(_translate("metaX_main", "Sample")) + self.checkBox_tflink_in_condition.setText(_translate("metaX_main", "In Condition")) + self.label_18.setText(_translate("metaX_main", "Function")) + self.label_196.setText(_translate("metaX_main", "Sub Meta ")) self.tabWidget_2.setTabText(self.tabWidget_2.indexOf(self.tab_8), _translate("metaX_main", "Taxa-Func Link")) self.radioButton_network_bysample.setText(_translate("metaX_main", "Sample")) self.label_49.setText(_translate("metaX_main", "Table")) diff --git a/metax/taxafunc_ploter/bar_plot_js.py b/metax/taxafunc_ploter/bar_plot_js.py index b04e374..5be6694 100644 --- a/metax/taxafunc_ploter/bar_plot_js.py +++ b/metax/taxafunc_ploter/bar_plot_js.py @@ -42,10 +42,10 @@ def _add_group_name_to_sample(self, df): return df - def plot_intensity_bar(self, taxon_name:str=None, sample_list:list = None, - func_name:str=None, peptide_seq=None, + def plot_intensity_bar(self, taxon_name:str|None=None, sample_list:list|None = None, + func_name:str|None =None, peptide_seq=None, width:int=1200, height:int=800, df= None, - title:str=None, rename_taxa:bool=False, + title:str|None =None, rename_taxa:bool=False, show_legend:bool=True, font_size:int=10, rename_sample:bool=True, plot_mean:bool=False, plot_percent:bool=False, sub_meta:str="None", @@ -83,16 +83,25 @@ def plot_intensity_bar(self, taxon_name:str=None, sample_list:list = None, df = df.div(df.sum(axis=0), axis=1) * 100 - # create title + # Create title if title is None: - if taxon_name is None: - title = f'{func_name}' - elif func_name is None: - title = f'{taxon_name}' - elif peptide_seq is not None: + if peptide_seq is not None: title = f'The intensity of {peptide_seq}' else: - title = f'{taxon_name}\n{func_name}' + renamed_taxon = taxon_name.split('|')[-1] if rename_taxa and taxon_name else taxon_name + + # If only taxon_name exists + if renamed_taxon and not func_name: + title = f'{renamed_taxon}' + + # If only func_name exists + elif func_name and not renamed_taxon: + title = f'{func_name}' + + # If both taxon_name and func_name exist + elif renamed_taxon and func_name: + title = f'{renamed_taxon}\n{func_name}' + global_opts_params = { "legend_opts": opts.LegendOpts( diff --git a/metax/taxafunc_ploter/heatmap_plot.py b/metax/taxafunc_ploter/heatmap_plot.py index 839262f..63de651 100644 --- a/metax/taxafunc_ploter/heatmap_plot.py +++ b/metax/taxafunc_ploter/heatmap_plot.py @@ -357,7 +357,7 @@ def plot_basic_heatmap(self, df, title = 'Heatmap',fig_size:tuple|None = None, scale = None, col_cluster:bool = True, row_cluster:bool = True, cmap:str|None = None, rename_taxa:bool = True, font_size:int = 10, show_all_labels:tuple = (False, False), rename_sample:bool = True, plot_mean:bool = False, - sub_meta: str = "None", scale_method:str = 'maxmin' + sub_meta: str = "None", scale_method:str = 'maxmin', return_type:str = 'fig' ): ''' sub_meta is higher plot_mean, if sub_meta provided, plot_mean is False @@ -365,6 +365,7 @@ def plot_basic_heatmap(self, df, title = 'Heatmap',fig_size:tuple|None = None, if plot_mean and sub_meta == 'None': # if sub_meta is not None, plot_mean is False df = self.tfa.BasicStats.get_stats_mean_df_by_group(df) + print('Plot the mean of the data, set rename_sample to False') rename_sample = False @@ -381,22 +382,40 @@ def plot_basic_heatmap(self, df, title = 'Heatmap',fig_size:tuple|None = None, if rename_taxa: mat = self.rename_taxa(mat) - if cmap is None: - cmap = 'YlOrRd' - if fig_size is None: - fig_size = (30,30) - mat, group_list = self.tfa.BasicStats.get_combined_sub_meta_df(df=mat, sub_meta=sub_meta, rename_sample=rename_sample, plot_mean=plot_mean) - color_list = self.assign_colors(group_list) - - # if only one column, remove col_cluster, set scale to None if len(mat.columns) < 2: col_cluster = False # scale = None + + if return_type == 'table': + sns_params = { + "col_cluster": col_cluster, + "row_cluster": row_cluster, + "method": self.linkage_method, + "metric": self.distance_metric, + } + fig = sns.clustermap(mat, **sns_params) + # get the sorted dataframe + if row_cluster and not col_cluster: + sorted_df = mat.iloc[fig.dendrogram_row.reordered_ind, :] + elif col_cluster and not row_cluster: + sorted_df = mat.iloc[:, fig.dendrogram_col.reordered_ind] + elif row_cluster and col_cluster: + sorted_df = mat.iloc[fig.dendrogram_row.reordered_ind, fig.dendrogram_col.reordered_ind] + else: + sorted_df = mat + plt.close(fig.figure) + return sorted_df + + # else plot heatmap figure + if cmap is None: + cmap = 'YlOrRd' + if fig_size is None: + fig_size = (30,30) - + color_list = self.assign_colors(group_list) sns_params = { # "center": 0, "cmap": cmap, diff --git a/metax/taxafunc_ploter/line_plot.py b/metax/taxafunc_ploter/line_plot.py index cac6257..2830c98 100644 --- a/metax/taxafunc_ploter/line_plot.py +++ b/metax/taxafunc_ploter/line_plot.py @@ -4,31 +4,48 @@ class LinePlot: def __init__(self, tfobj): - self.tfobj = tfobj + self.tfa = tfobj # plot intensity line for each sample # Example: plot_intensity_line(sw, func_name=func_name, taxon_name=taxon_name, fig_size=(30,20)) - def plot_intensity_line(self, taxon_name:str=None, sample_list:list = None, func_name:str=None, peptide_seq=None, width:int=20, height:int=12): - fig_size = (width, height) + def plot_intensity_line( + self, + taxon_name: str|None = None, + sample_list: list|None = None, + func_name: str|None = None, + peptide_seq=None, + fig_size: tuple = (20, 12), + plot_mean: bool = False, + rename_taxa: bool = True, + rename_sample: bool = True, + ): + sns.set_theme() + plt.style.use('bmh') - df = self.tfobj.GetMatrix.get_intensity_matrix(taxon_name=taxon_name, func_name=func_name, peptide_seq=peptide_seq, sample_list= sample_list) + df = self.tfa.GetMatrix.get_intensity_matrix( + taxon_name=taxon_name, + func_name=func_name, + peptide_seq=peptide_seq, + sample_list=sample_list, + ) if df.empty: raise ValueError('No data to plot') - # create color list for groups & rename columns - col_names = df.columns.tolist() - meta_df = self.tfobj.meta_df - meta_name = self.tfobj.meta_name - groups_list = [] - new_col_names = [] - for i in col_names: - group = meta_df[meta_df['Sample'] == i] - group = group[meta_name].values[0] - new_col_names.append(f'{i} ({group})') - groups_list.append(group) - df.columns = new_col_names + + + if plot_mean: + df = self.tfa.BasicStats.get_stats_mean_df_by_group(df) + rename_sample = False + + if rename_sample: + df, _ = self.tfa.add_group_name_for_sample(df) + index_name = df.index.name + color_list = "tab10" if len(df) <= 10 else "tab20" # create title + if rename_taxa and taxon_name is not None: + taxon_name = taxon_name.split('|')[-1] + if taxon_name is None: title = f'{func_name}' elif func_name is None: @@ -41,12 +58,35 @@ def plot_intensity_line(self, taxon_name:str=None, sample_list:list = None, func dfp = pd.melt(df.reset_index(), id_vars=index_name, var_name='Samples', value_name='Intensity') plt.figure(figsize=fig_size) - fig = sns.lineplot(x='Samples', y='Intensity', hue=index_name, data=dfp, palette='Set1', legend = True) - + fig = sns.lineplot( + x="Samples", + y="Intensity", + hue=index_name, + data=dfp, + palette=color_list, + legend=True, + ) + fig.set_title(title, fontsize=15) - fig.set_xlabel('Samples', fontsize=15) + fig.set_xlabel('') fig.set_ylabel('Intensity', fontsize=15) - fig.tick_params(axis='x', rotation=90, labelsize=8) + fig.tick_params(axis='x', rotation=0, labelsize=15) + + + # set legend out of the plot + plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.) + plt.show() return fig + + +# LinePlot(sw).plot_intensity_line(taxon_name="d__Bacteria|p__Proteobacteria|c__Gammaproteobacteria|o__Enterobacterales|f__Enterobacteriaceae|g__Citrobacter_B|s__Citrobacter_B koseri", +# sample_list=sample_list, +# func_name="ko00061:Fatty acid biosynthesis", +# plot_mean=True, +# rename_taxa=True, +# rename_sample=True, +# fig_size=(10,8) + +# ) \ No newline at end of file diff --git a/metax/utils/version.py b/metax/utils/version.py index d09d4cc..c0b048d 100644 --- a/metax/utils/version.py +++ b/metax/utils/version.py @@ -1,2 +1,2 @@ -__version__ = '1.111.8' +__version__ = '1.112.0' API_version = '2' \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 95cd768..000827b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "MetaXTools" -version = "1.111.8" +version = "1.112.0" description = "MetaXTools is a novel tool for linking peptide sequences with taxonomic and functional information in Metaproteomics." readme = "README_PyPi.md" license = { text = "NorthOmics" } From 11c490abb3b3108867e2b1b7941a4fbebee41f94 Mon Sep 17 00:00:00 2001 From: Qing <44231502+byemaxx@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:08:41 -0400 Subject: [PATCH 3/4] modified: metax/taxafunc_analyzer/analyzer_utils/cross_test.py --- .../analyzer_utils/cross_test.py | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/metax/taxafunc_analyzer/analyzer_utils/cross_test.py b/metax/taxafunc_analyzer/analyzer_utils/cross_test.py index bd25fe6..35d3bd3 100644 --- a/metax/taxafunc_analyzer/analyzer_utils/cross_test.py +++ b/metax/taxafunc_analyzer/analyzer_utils/cross_test.py @@ -58,7 +58,7 @@ def _get_df_primary_secondary(self, df_type: str): - def get_stats_anova(self, group_list: list = None, df_type:str = 'taxa-func', condition:list =None) -> pd.DataFrame: + def get_stats_anova(self, group_list: list|None = None, df_type:str = 'taxa-func', condition:list|None =None) -> pd.DataFrame: df_type = self.convert_df_name_to_simple_name(df_type) group_list_all = sorted(set(self.tfa.get_meta_list(self.tfa.meta_name))) @@ -108,7 +108,7 @@ def get_stats_anova(self, group_list: list = None, df_type:str = 'taxa-func', co res_all = res_all[['P-value', 'f-statistic'] + all_sample_list] return res_all - def get_stats_ttest(self, group_list: list = None, df_type: str = 'taxa-func', condition:list =None) -> pd.DataFrame: + def get_stats_ttest(self, group_list: list|None = None, df_type: str = 'taxa-func', condition:list|None =None) -> pd.DataFrame: df_type = self.convert_df_name_to_simple_name(df_type) group_list_all = sorted(set(self.tfa.get_meta_list(self.tfa.meta_name))) @@ -163,7 +163,7 @@ def get_stats_ttest(self, group_list: list = None, df_type: str = 'taxa-func', c res_all = res_all[['P-value', 't-statistic'] + all_sample_list] return res_all - def get_stats_dunnett_test_against_control_with_conditon(self, control_group, condition, group_list:list =None, df_type: str = 'taxa-func') -> pd.DataFrame: + def get_stats_dunnett_test_against_control_with_conditon(self, control_group, condition, group_list:list|None =None, df_type: str = 'taxa-func') -> pd.DataFrame: df_type = self.convert_df_name_to_simple_name(df_type) meta_df = self.tfa.meta_df.copy() @@ -183,7 +183,7 @@ def get_stats_dunnett_test_against_control_with_conditon(self, control_group, co return res_df # a dataframe with 3 level columns index - def get_stats_dunnett_test(self, control_group, group_list: list = None, df_type: str = 'taxa-func', condition:list =None) -> pd.DataFrame: + def get_stats_dunnett_test(self, control_group, group_list: list|None = None, df_type: str = 'taxa-func', condition:list|None =None) -> pd.DataFrame: df_type = self.convert_df_name_to_simple_name(df_type) group_list_all = sorted(set(self.tfa.get_meta_list(self.tfa.meta_name))) @@ -327,7 +327,7 @@ def get_stats_deseq2_against_control_with_conditon(self, df, control_group, cond return res_df # a dataframe with 3 level columns index - def get_stats_deseq2_against_control(self, df, control_group, group_list: list = None, concat_sample_to_result: bool = False, quiet: bool = False, condition: list = None) -> pd.DataFrame: + def get_stats_deseq2_against_control(self, df, control_group, group_list: list|None = None, concat_sample_to_result: bool = False, quiet: bool = False, condition: list|None = None) -> pd.DataFrame: all_group_list = sorted(set(self.tfa.group_list)) if group_list is None: group_list = all_group_list @@ -359,7 +359,7 @@ def get_stats_deseq2_against_control(self, df, control_group, group_list: list = - def get_stats_deseq2(self, df, group1, group2, concat_sample_to_result: bool = True, quiet: bool = False, condition: list = None) -> pd.DataFrame: + def get_stats_deseq2(self, df, group1, group2, concat_sample_to_result: bool = True, quiet: bool = False, condition: list|None = None) -> pd.DataFrame: print(f'\n--Running Deseq2 [{group1}] vs [{group2}] with condition: [{condition}]--') group1_sample = self.tfa.get_sample_list_in_a_group(group1, condition=condition) @@ -447,7 +447,7 @@ def get_stats_deseq2(self, df, group1, group2, concat_sample_to_result: bool = T return res_merged # Get the Tukey test result of a taxon or a function - def get_stats_tukey_test(self, taxon_name: str=None, func_name: str=None, sum_all: bool=True, condition:list =None): + def get_stats_tukey_test(self, taxon_name: str|None =None, func_name: str|None =None, sum_all: bool=True, condition:list|None =None): # :param taxon_name: the taxon name # :param func_name: the function name # :return: the Tukey test result @@ -459,7 +459,7 @@ def get_stats_tukey_test(self, taxon_name: str=None, func_name: str=None, sum_al return tukey_df - def get_stats_tukey_test_each(self, taxon_name: str = None, func_name: str = None, condition:list =None): + def get_stats_tukey_test_each(self, taxon_name: str|None = None, func_name: str|None = None, condition:list|None =None): # Copy the dataframe and reset index df = self.tfa.taxa_func_df.copy() df = df.reset_index() @@ -515,7 +515,7 @@ def get_stats_tukey_test_each(self, taxon_name: str = None, func_name: str = Non # Return the combined Tukey test results return tukey_results - def get_stats_tukey_test_sum(self, taxon_name: str=None, func_name: str=None, condition:list =None): + def get_stats_tukey_test_sum(self, taxon_name: str|None=None, func_name: str|None=None, condition:list|None =None): # :param taxon_name: the taxon name # :param func_name: the function name # :return: the Tukey test result @@ -569,10 +569,10 @@ def get_stats_tukey_test_sum(self, taxon_name: str=None, func_name: str=None, co # find out the items that are not significant in taxa but significant in function, and vice versa - def get_stats_diff_taxa_but_func(self, group_list: list = None, p_value: float = 0.05, - taxa_res_df: pd.DataFrame =None, - func_res_df: pd.DataFrame=None, - taxa_func_res_df: pd.DataFrame=None, condition:list =None) -> tuple: + def get_stats_diff_taxa_but_func(self, group_list: list|None = None, p_value: float = 0.05, + taxa_res_df: pd.DataFrame|None =None, + func_res_df: pd.DataFrame|None =None, + taxa_func_res_df: pd.DataFrame|None =None, condition:list|None =None) -> tuple: # calculate the test result if not given if taxa_res_df is None or func_res_df is None or taxa_func_res_df is None: @@ -605,10 +605,11 @@ def get_stats_diff_taxa_but_func(self, group_list: list = None, p_value: float = # check the p_value is between 0 and 1 if p_value < 0 or p_value > 1: raise ValueError("p_value must be between 0 and 1") - # 获取p-value大于0.05的Taxon条目 + # 获取p-value大于0.05的Taxon items not_significant_taxa = df_taxa_test_res[df_taxa_test_res['P-value'] >= p_value].index.get_level_values('Taxon').tolist() print(f"Under P-value = {p_value}: \n \ Significant Taxa: [{len(df_taxa_test_res) - len(not_significant_taxa)}], Not Significant Taxa: [{len(not_significant_taxa)}]") + # 获取p-value小于0.05的Function items not_significant_func = df_func_test_res[df_func_test_res['P-value'] >= p_value].index.get_level_values(self.tfa.func_name).tolist() print(f"Under P-value = {p_value}: \n \ Significant Function: [{len(df_func_test_res) - len(not_significant_func)}], Not Significant Function: [{len(not_significant_func)}]") From 893cdbddf2ffe8c3e6185f46568315f94f01a8dd Mon Sep 17 00:00:00 2001 From: Qing <44231502+byemaxx@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:47:44 -0400 Subject: [PATCH 4/4] update README.md --- Docs/MetaX_Cookbook.assets/OTF_Structure.png | Bin 0 -> 59889 bytes README.md | 10 +++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Docs/MetaX_Cookbook.assets/OTF_Structure.png diff --git a/Docs/MetaX_Cookbook.assets/OTF_Structure.png b/Docs/MetaX_Cookbook.assets/OTF_Structure.png new file mode 100644 index 0000000000000000000000000000000000000000..72a301cf22d4bb0c2228ef554e7a7b2286a8a08f GIT binary patch literal 59889 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 35ec7da..a325cdc 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,15 @@ MetaX also features statistical modules and plotting tools for ana ![abstract](https://github.com/byemaxx/MetaX/raw/main/Docs/MetaX_Cookbook.assets/abstract.png) -## Taxa-Functions Linkage + +## Operational Taxon-Function (OTF) + +**Operational Taxon-Function (OTF) Unit**: An operational unit which represents the association between specific taxa and biological functions. + +![OTF_Structure](https://github.com/byemaxx/MetaX/raw/main/Docs/MetaX_Cookbook.assets/OTF_Structure.png) + +## OTFs Network + Linking Taxa and Functions in different levels of the hierarchy, and different functional categories. e.g., **Species-KO**, **Genus-CAZy**, **Phylum-EC**, etc. - ![OTF](https://github.com/byemaxx/MetaX/raw/main/Docs/MetaX_Cookbook.assets/tf_link_net.png)