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