1""" GeneSets """ 2from enum import IntEnum 3from types import SimpleNamespace 4from typing import Set, List, Tuple, Optional 5from urllib.parse import urlparse 6 7from AnyQt.QtGui import QColor, QStandardItem, QStandardItemModel 8from AnyQt.QtCore import Qt, QSize 9from AnyQt.QtWidgets import QTreeView, QHBoxLayout, QHeaderView 10 11from Orange.data import Table, Domain 12from Orange.data import filter as table_filter 13from Orange.widgets.gui import LinkRole, LinkStyledItemDelegate, spin, vBox, lineEdit, widgetBox, auto_commit 14from Orange.widgets.widget import Msg, OWWidget 15from Orange.widgets.settings import Setting, SettingProvider 16from Orange.widgets.utils.signals import Input, Output 17from Orange.widgets.utils.concurrent import TaskState, ConcurrentWidgetMixin 18 19from orangecontrib.bioinformatics.geneset import GeneSets 20from orangecontrib.bioinformatics.widgets.utils.gui import FilterProxyModel, NumericalColumnDelegate 21from orangecontrib.bioinformatics.widgets.components import GeneSetSelection 22from orangecontrib.bioinformatics.widgets.utils.data import TableAnnotation, check_table_annotation 23 24 25class Results(SimpleNamespace): 26 items: List[QStandardItem] = [] 27 28 29def run(gene_sets: GeneSets, selected_gene_sets: List[Tuple[str, ...]], genes, state: TaskState) -> Results: 30 results = Results() 31 items = [] 32 step, steps = 0, len(gene_sets) 33 34 if not genes: 35 return results 36 37 state.set_status('Calculating...') 38 39 for gene_set in sorted(gene_sets): 40 41 step += 1 42 if step % (steps / 10) == 0: 43 state.set_progress_value(100 * step / steps) 44 45 if gene_set.hierarchy not in selected_gene_sets: 46 continue 47 48 if state.is_interruption_requested(): 49 return results 50 51 matched_set = gene_set.genes & genes 52 if len(matched_set) > 0: 53 category_column = QStandardItem() 54 term_column = QStandardItem() 55 count_column = QStandardItem() 56 genes_column = QStandardItem() 57 58 category_column.setData(", ".join(gene_set.hierarchy), Qt.DisplayRole) 59 term_column.setData(gene_set.name, Qt.DisplayRole) 60 term_column.setData(gene_set.name, Qt.ToolTipRole) 61 62 # there was some cases when link string was not empty string but not valid (e.g. "_") 63 if gene_set.link and urlparse(gene_set.link).scheme: 64 term_column.setData(gene_set.link, LinkRole) 65 term_column.setForeground(QColor(Qt.blue)) 66 67 count_column.setData(matched_set, Qt.UserRole) 68 count_column.setData(len(matched_set), Qt.DisplayRole) 69 70 genes_column.setData(len(gene_set.genes), Qt.DisplayRole) 71 genes_column.setData(set(gene_set.genes), Qt.UserRole) # store genes to get then on output on selection 72 73 items.append([count_column, genes_column, category_column, term_column]) 74 75 results.items = items 76 return results 77 78 79class Header(IntEnum): 80 count = 0 81 genes = 1 82 category = 2 83 term = 3 84 85 @staticmethod 86 def labels(): 87 return ['Count', 'Genes In Set', 'Category', 'Term'] 88 89 90class OWGeneSets(OWWidget, ConcurrentWidgetMixin): 91 name = 'Gene Sets' 92 description = "" 93 icon = 'icons/OWGeneSets.svg' 94 priority = 80 95 want_main_area = True 96 97 organism = Setting(None, schema_only=True) 98 stored_gene_sets_selection = Setting([], schema_only=True) 99 selected_rows = Setting([], schema_only=True) 100 101 min_count: int 102 min_count = Setting(5) 103 104 use_min_count: bool 105 use_min_count = Setting(True) 106 107 auto_commit: bool 108 auto_commit = Setting(False) 109 110 search_pattern: str 111 search_pattern = Setting('') 112 113 # component settings 114 gs_selection_component: SettingProvider = SettingProvider(GeneSetSelection) 115 116 class Inputs: 117 data = Input('Data', Table) 118 custom_gene_sets = Input('Custom Gene Sets', Table) 119 120 class Outputs: 121 matched_genes = Output('Matched Genes', Table) 122 123 class Warning(OWWidget.Warning): 124 all_sets_filtered = Msg('All sets were filtered out.') 125 126 class Error(OWWidget.Error): 127 organism_mismatch = Msg('Organism in input data and custom gene sets does not match') 128 cant_reach_host = Msg('Host orange.biolab.si is unreachable.') 129 cant_load_organisms = Msg('No available organisms, please check your connection.') 130 custom_gene_sets_table_format = Msg('Custom gene sets data must have genes represented as rows.') 131 132 def __init__(self): 133 super().__init__() 134 # OWWidget.__init__(self) 135 ConcurrentWidgetMixin.__init__(self) 136 137 # Control area 138 box = vBox(self.controlArea, True, margin=0) 139 self.gs_selection_component: GeneSetSelection = GeneSetSelection(self, box) 140 self.gs_selection_component.selection_changed.connect(self._on_selection_changed) 141 142 # Main area 143 self.filter_proxy_model = FilterProxyModel() 144 self.filter_proxy_model.setFilterKeyColumn(Header.term) 145 146 self.tree_view = QTreeView() 147 self.tree_view.setAlternatingRowColors(True) 148 self.tree_view.setSortingEnabled(True) 149 self.tree_view.sortByColumn(Header.count, Qt.DescendingOrder) 150 151 self.tree_view.setSelectionMode(QTreeView.ExtendedSelection) 152 self.tree_view.setEditTriggers(QTreeView.NoEditTriggers) 153 self.tree_view.viewport().setMouseTracking(True) 154 self.tree_view.setItemDelegateForColumn(Header.term, LinkStyledItemDelegate(self.tree_view)) 155 self.tree_view.setItemDelegateForColumn(Header.genes, NumericalColumnDelegate(self)) 156 self.tree_view.setItemDelegateForColumn(Header.count, NumericalColumnDelegate(self)) 157 self.tree_view.setModel(self.filter_proxy_model) 158 159 h_layout = QHBoxLayout() 160 h_layout.setSpacing(100) 161 h_widget = widgetBox(self.mainArea, orientation=h_layout) 162 163 spin( 164 h_widget, 165 self, 166 'min_count', 167 0, 168 1000, 169 label='Count', 170 tooltip='Minimum genes count', 171 checked='use_min_count', 172 callback=self.filter_view, 173 callbackOnReturn=True, 174 checkCallback=self.filter_view, 175 ) 176 177 self.line_edit_filter = lineEdit(h_widget, self, 'search_pattern') 178 self.line_edit_filter.setPlaceholderText('Filter gene sets ...') 179 self.line_edit_filter.textChanged.connect(self.filter_view) 180 181 self.mainArea.layout().addWidget(self.tree_view) 182 self.tree_view.header().setSectionResizeMode(QHeaderView.ResizeToContents) 183 184 self.commit_button = auto_commit(self.controlArea, self, 'auto_commit', '&Commit', box=False) 185 186 self.input_data: Optional[Table] = None 187 self.num_of_selected_genes: int = 0 188 189 @property 190 def tax_id(self) -> Optional[str]: 191 if self.input_data: 192 return self.input_data.attributes[TableAnnotation.tax_id] 193 194 @property 195 def gene_as_attr_name(self) -> Optional[bool]: 196 if self.input_data: 197 return self.input_data.attributes[TableAnnotation.gene_as_attr_name] 198 199 @property 200 def gene_location(self) -> Optional[str]: 201 if not self.input_data: 202 return 203 204 if self.gene_as_attr_name: 205 return self.input_data.attributes[TableAnnotation.gene_id_attribute] 206 else: 207 return self.input_data.attributes[TableAnnotation.gene_id_column] 208 209 @property 210 def input_genes(self) -> Set[str]: 211 if not self.input_data: 212 return set() 213 214 if self.gene_as_attr_name: 215 return { 216 str(variable.attributes.get(self.gene_location, '?')) for variable in self.input_data.domain.attributes 217 } 218 else: 219 return {str(g) for g in self.input_data.get_column_view(self.gene_location)[0]} 220 221 def on_partial_result(self, _): 222 pass 223 224 def on_done(self, result: Results): 225 model = QStandardItemModel() 226 for item in result.items: 227 model.appendRow(item) 228 229 model.setSortRole(Qt.UserRole) 230 model.setHorizontalHeaderLabels(Header.labels()) 231 232 self.filter_proxy_model.setSourceModel(model) 233 self.tree_view.selectionModel().selectionChanged.connect(self.commit) 234 self.filter_view() 235 self.update_info_box() 236 237 def on_exception(self, ex): 238 # TODO: handle possible exceptions 239 raise ex 240 241 def onDeleteWidget(self): 242 self.shutdown() 243 super().onDeleteWidget() 244 245 def _on_selection_changed(self): 246 self.start(run, self.gs_selection_component.gene_sets, self.gs_selection_component.selection, self.input_genes) 247 248 @Inputs.data 249 @check_table_annotation 250 def set_data(self, input_data: Table): 251 self.Outputs.matched_genes.send(None) 252 self.input_data = None 253 self.num_of_selected_genes = 0 254 255 if input_data: 256 self.input_data = input_data 257 self.gs_selection_component.initialize(self.tax_id) 258 259 self.update_info_box() 260 261 @Inputs.custom_gene_sets 262 def handle_custom_gene_sets_input(self, custom_data): 263 self.Outputs.matched_genes.send(None) 264 265 if custom_data: 266 self.gs_selection_component.initialize_custom_gene_sets(custom_data) 267 else: 268 self.gs_selection_component.initialize_custom_gene_sets(None) 269 270 self.update_info_box() 271 272 def commit(self): 273 selection_model = self.tree_view.selectionModel() 274 self.num_of_selected_genes = 0 275 276 if selection_model: 277 selection = selection_model.selectedRows(Header.count) 278 self.selected_rows = [self.filter_proxy_model.mapToSource(sel).row() for sel in selection] 279 280 if selection and self.input_genes: 281 genes = [model_index.data(Qt.UserRole) for model_index in selection] 282 output_genes = list(set.union(*genes)) 283 self.num_of_selected_genes = len(output_genes) 284 285 if self.gene_as_attr_name: 286 selected = [ 287 column 288 for column in self.input_data.domain.attributes 289 if self.gene_location in column.attributes 290 and str(column.attributes[self.gene_location]) in output_genes 291 ] 292 293 domain = Domain(selected, self.input_data.domain.class_vars, self.input_data.domain.metas) 294 new_data = self.input_data.from_table(domain, self.input_data) 295 self.Outputs.matched_genes.send(new_data) 296 else: 297 # create filter from selected column for genes 298 only_known = table_filter.FilterStringList(self.gene_location, output_genes) 299 # apply filter to the data 300 data_table = table_filter.Values([only_known])(self.input_data) 301 self.Outputs.matched_genes.send(data_table) 302 303 self.update_info_box() 304 305 def update_info_box(self): 306 input_string = '' 307 input_number = '' 308 309 if self.input_genes: 310 input_string += '{} unique gene names on input.\n'.format(len(self.input_genes)) 311 input_number += str(len(self.input_genes)) 312 self.info.set_output_summary( 313 str(self.num_of_selected_genes), '{} genes on output.\n'.format(self.num_of_selected_genes) 314 ) 315 else: 316 self.info.set_output_summary(self.info.NoOutput) 317 318 if self.gs_selection_component.data: 319 num_of_genes = self.gs_selection_component.num_of_genes 320 num_of_sets = self.gs_selection_component.num_of_custom_sets 321 input_number += f"{'' if input_number else '0'}|{num_of_genes}" 322 input_string += '{} marker genes in {} sets\n'.format(num_of_genes, num_of_sets) 323 324 if not input_number: 325 self.info.set_input_summary(self.info.NoInput) 326 else: 327 self.info.set_input_summary(input_number, input_string) 328 329 def create_filters(self): 330 search_term: List[str] = self.search_pattern.lower().strip().split() 331 332 filters = [ 333 FilterProxyModel.Filter( 334 Header.term, Qt.DisplayRole, lambda value: all(fs in value.lower() for fs in search_term) 335 ) 336 ] 337 338 if self.use_min_count: 339 filters.append( 340 FilterProxyModel.Filter(Header.count, Qt.DisplayRole, lambda value: value >= self.min_count) 341 ) 342 343 return filters 344 345 def filter_view(self): 346 filter_proxy: FilterProxyModel = self.filter_proxy_model 347 model: QStandardItemModel = filter_proxy.sourceModel() 348 349 if isinstance(model, QStandardItemModel): 350 351 # apply filtering rules 352 filter_proxy.set_filters(self.create_filters()) 353 354 if model.rowCount() and not filter_proxy.rowCount(): 355 self.Warning.all_sets_filtered() 356 else: 357 self.Warning.clear() 358 359 def sizeHint(self): 360 return QSize(800, 600) 361 362 363if __name__ == "__main__": 364 from Orange.widgets.utils.widgetpreview import WidgetPreview 365 366 widget = WidgetPreview(OWGeneSets) 367 widget.run() 368