source: orange-bioinformatics/orangecontrib/bio/widgets/OWGeneInfo.py @ 1874:b3e32cc5cf6f

Revision 1874:b3e32cc5cf6f, 23.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Added new style widget meta descriptions.

Line 
1"""
2<name>Gene Info</name>
3<description>Displays gene information from NCBI and other sources.</description>
4<priority>2010</priority>
5<contact>Ales Erjavec (ales.erjavec(@at@)fri.uni-lj.si)</contact>
6<icon>icons/GeneInfo.svg</icon>
7"""
8
9from __future__ import absolute_import, with_statement
10
11from collections import defaultdict
12from functools import partial
13
14import orange
15from Orange.orng import orngServerFiles
16from Orange.orng.orngDataCaching import data_hints
17from Orange.OrangeWidgets import OWGUI
18from Orange.OrangeWidgets.OWWidget import *
19
20from .. import obiGene, obiTaxonomy
21
22
23NAME = "Gene Info"
24DESCRIPTION = "Displays gene information from NCBI and other sources."
25ICON = "icons/GeneInfo.svg"
26PRIORITY = 2010
27
28INPUTS = [("Examples", Orange.data.Table, "setData")]
29OUTPUTS = [("Selected Examples", Orange.data.Table)]
30
31REPLACES = ["_bioinformatics.widgets.OWGeneInfo.OWGeneInfo"]
32
33
34class TreeModel(QAbstractItemModel):
35    def __init__(self, data, header, parent):
36        QAbstractItemModel.__init__(self, parent)
37        self._data = [[QVariant(s) for s in row] for row in data]
38        self._dataDict = {}
39        self._header = header
40        self._roleData = {Qt.DisplayRole:self._data}
41        self._roleData = partial(defaultdict, partial(defaultdict, partial(defaultdict, QVariant)))(self._roleData)
42   
43    def setColumnLinks(self, column, links):
44        font =QFont()
45        font.setUnderline(True)
46        font = QVariant(font)
47        for i, link in enumerate(links):
48            self._roleData[LinkRole][i][column] = QVariant(link)
49            self._roleData[Qt.FontRole][i][column] = font
50            self._roleData[Qt.ForegroundRole][i][column] = QVariant(QColor(Qt.blue))
51   
52    def setRoleData(self, role, row, col, data):
53        self._roleData[role][row][col] = data
54       
55    def data(self, index, role=Qt.DisplayRole):
56        row, col = index.row(), index.column()
57        return self._roleData[role][row][col]
58       
59    def index(self, row, col, parent=QModelIndex()):
60        return self.createIndex(row, col, 0)
61   
62    def parent(self, index):
63        return QModelIndex()
64   
65    def rowCount(self, index=QModelIndex()):
66        if index.isValid():
67            return 0
68        else:
69            return len(self._data)
70       
71    def columnCount(self, index=QModelIndex()):
72        return len(self._header)
73
74    def headerData(self, section, orientation, role=Qt.DisplayRole):
75        if role==Qt.DisplayRole:
76            return QVariant(self._header[section])
77        return QVariant()
78
79from Orange.OrangeWidgets.OWGUI import LinkStyledItemDelegate, LinkRole
80
81def lru_cache(maxsize=100):
82    """ A least recently used cache function decorator.
83    """
84   
85    def decorating_function(func):
86        import functools
87        cache = {}
88       
89        functools.wraps(func)
90        def wrapped(*args, **kwargs):
91            key = args + tuple(sorted(kwargs.items()))
92            if key not in cache:
93                res = func(*args, **kwargs)
94                cache[key] = (time.time(), res)
95                if len(cache) > maxsize:
96                    key, (_, _) = min(cache.iteritems(), key=lambda item: item[1][0])
97                    del cache[key]
98            else:
99                _, res = cache[key]
100                cache[key] = (time.time(), res) # update the time
101               
102            return res
103       
104        def clear():
105            cache.clear()
106       
107        wrapped.clear = clear
108       
109        return wrapped
110    return decorating_function
111               
112class LinkFmt(object):
113    def __init__(self, link_fmt, name):
114        self.link_fmt = link_fmt
115        self.name = name
116       
117    def format(self, *args, **kwargs):
118        return Link(self.link_fmt.format(*args, **kwargs), **kwargs)
119   
120    def __repr__(self):
121        return "<LinkFmt " + repr(self.name) + " >"
122   
123    def __str__(self):
124        return self.name
125   
126class Link(object):
127    def __init__(self, link, text=None, **kwargs):
128        self.link = link
129        self.text = text if text is not None else "link"
130        self.__dict__.update(kwargs)
131       
132    def str(self):
133        return link
134   
135   
136@lru_cache(maxsize=2)
137def get_ncbi_info(taxid):
138    return obiGene.NCBIGeneInfo(taxid)
139
140def ncbi_info(taxid, genes):
141    info = get_ncbi_info(taxid)
142    schema_link = LinkFmt("http://www.ncbi.nlm.nih.gov/sites/entrez?Db=gene&Cmd=ShowDetailView&TermToSearch={gene_id}", name="NCBI ID")
143    schema = [schema_link, "Symbol", "Locus Tag", "Chromosome",
144              "Description", "Synonyms", "Nomenclature"]
145    ret = []
146    for gene in genes:
147        gi = info.get_info(gene)
148        if gi:
149            ret.append([schema_link.format(gene_id=gi.gene_id, text=gi.gene_id),
150                        gi.symbol + " (%s)" % gene if gene != gi.symbol else gi.symbol,
151                        gi.locus_tag or "",
152                        gi.chromosome or "",
153                        gi.description or "",
154                        ", ".join(gi.synonyms),
155                        gi.symbol_from_nomenclature_authority or ""
156                        ])
157        else:
158            ret.append(None)
159    return schema, ret
160   
161def dicty_info(taxid, genes):
162    from .. import obiDicty
163    info = obiDicty.DictyBase()
164    name_matcher = obiGene.GMDicty()
165    name_matcher.set_targets(info.info.keys())
166    schema_link = LinkFmt("http://dictybase.org/db/cgi-bin/gene_page.pl?dictybaseid={gene_id}", name="Dicty Base Id")
167    schema = [schema_link, "Name", "Synonyms", "Gene Products"]
168   
169    ret = []
170    for gene in genes:
171        gene = name_matcher.umatch(gene)
172        gi = info.info.get(gene, None)
173        if gi:
174            ret.append([schema_link.format(gene_id=gene, text=gene),
175                        gi[0] + " (%s)" % gene if gene != gi[0] else gi[0], # Gene Name
176                        ", ".join(gi[1]), # Synonyms
177                        gi[2] or "", # Gene Products
178                        ])
179           
180        else:
181            ret.append(None)
182   
183    return schema, ret
184   
185   
186INFO_SOURCES = {"default": [("NCBI Info", ncbi_info)],
187                "352472": [("NCBI Info", ncbi_info),
188                           ("Dicty Base", dicty_info)
189                           ]
190                }
191
192class OWGeneInfo(OWWidget):
193    settingsList = ["organismIndex", "geneAttr", "useAttr", "autoCommit"]
194    contextHandlers = {"":DomainContextHandler("", ["organismIndex",
195                                "geneAttr", "useAttr", "useAltSource"])}
196    def __init__(self, parent=None, signalManager=None, name="Gene Info"):
197        OWWidget.__init__(self, parent, signalManager, name)
198
199        self.inputs = [("Examples", ExampleTable, self.setData)]
200        self.outputs = [("Selected Examples", ExampleTable)]
201
202        self.organismIndex = 0
203        self.geneAttr = 0
204        self.useAttr = False
205        self.autoCommit = False
206        self.searchString = ""
207        self.selectionChangedFlag = False
208        self.useAltSource = 0
209        self.loadSettings()
210       
211        self.infoLabel = OWGUI.widgetLabel(OWGUI.widgetBox(self.controlArea,
212                                                    "Info", addSpace=True),
213                                           "No data on input\n")
214        self.organisms = sorted(set([name.split(".")[-2] for name in \
215                            orngServerFiles.listfiles("NCBI_geneinfo")] + \
216                            obiGene.NCBIGeneInfo.essential_taxids()))
217   
218        self.organismBox = OWGUI.widgetBox(self.controlArea, "Organism",
219                                           addSpace=True)
220        self.organismComboBox = OWGUI.comboBox(self.organismBox, self,
221                                "organismIndex", "Organism",
222                                items=[obiTaxonomy.name(id) for id in self.organisms],
223                                callback=self.setItems,
224                                debuggingEnabled=0)
225       
226        # For now only support one alt source, with a checkbox
227        # In the future this can be extended to multiple selections
228        self.altSourceCheck = OWGUI.checkBox(self.organismBox, self,
229                            "useAltSource", "Show information from dictyBase",
230                            callback=self.onAltSourceChange,
231#                            debuggingEnabled=0,
232                            )
233        self.altSourceCheck.hide()
234       
235        box = OWGUI.widgetBox(self.controlArea, "Gene names", addSpace=True)
236        self.geneAttrComboBox = OWGUI.comboBox(box, self, "geneAttr",
237                                "Gene atttibute", callback=self.setItems)
238       
239        c = OWGUI.checkBox(box, self, "useAttr", "Use attribute names",
240                           callback=self.setItems,
241                           disables=[(-1, self.geneAttrComboBox)])
242       
243        self.geneAttrComboBox.setDisabled(bool(self.useAttr))
244
245        box = OWGUI.widgetBox(self.controlArea, "Commit", addSpace=True)
246        b = OWGUI.button(box, self, "Commit", callback=self.commit)
247        c = OWGUI.checkBox(box, self, "autoCommit", "Commit on change")
248        OWGUI.setStopper(self, b, c, "selectionChangedFlag",
249                         callback=self.commit)
250       
251        ## A label for dictyExpress link
252        self.dictyExpressBox = OWGUI.widgetBox(self.controlArea, "Dicty Express")
253        self.linkLabel = OWGUI.widgetLabel(self.dictyExpressBox, "")
254        self.linkLabel.setOpenExternalLinks(False)
255        self.connect(self.linkLabel, SIGNAL("linkActivated(QString)"),
256                     self.onDictyExpressLink)
257        self.dictyExpressBox.hide()
258       
259        OWGUI.rubber(self.controlArea)
260
261        OWGUI.lineEdit(self.mainArea, self, "searchString", "Filter",
262                       callbackOnType=True, callback=self.searchUpdate)
263       
264        self.treeWidget = QTreeView(self.mainArea)
265        self.treeWidget.setRootIsDecorated(False)
266        self.treeWidget.setSelectionMode(QAbstractItemView.ExtendedSelection)
267        self.treeWidget.setItemDelegate(LinkStyledItemDelegate(self.treeWidget))
268        #self.connect(self.treeWidget, SIGNAL("itemSelectionChanged()"), self.commitIf)
269        self.treeWidget.viewport().setMouseTracking(True)
270        self.treeWidget.setSortingEnabled(True)
271        self.mainArea.layout().addWidget(self.treeWidget)
272       
273        box = OWGUI.widgetBox(self.mainArea, "",
274                              orientation="horizontal")
275        OWGUI.button(box, self, "Select Filtered",
276                     callback=self.selectFiltered)
277        OWGUI.button(box, self, "Clear Selection",
278                     callback=self.treeWidget.clearSelection)
279       
280        self.resize(1000, 700)       
281
282        self.geneinfo = []
283        self.cells = []
284        self.row2geneinfo = {}
285        self.data = None
286        self.currentLoaded = None, None
287        self.selectionUpdateInProgress = False
288       
289    def setData(self, data=None):
290        self.closeContext()
291        self.data = data
292        if data:
293            self.geneAttrComboBox.clear()
294            self.attributes = [attr for attr in self.data.domain.variables + \
295                               self.data.domain.getmetas().values() \
296                               if attr.varType in [orange.VarTypes.String,
297                                                   orange.VarTypes.Discrete]]
298            self.geneAttrComboBox.addItems([attr.name for attr in self.attributes])
299            self.openContext("", data)
300            self.geneAttr = min(self.geneAttr, len(self.attributes) - 1)
301           
302            taxid = data_hints.get_hint(self.data, "taxid", "")
303            if taxid in self.organisms:
304                self.organismIndex = self.organisms.index(taxid)
305               
306            self.useAttr = data_hints.get_hint(self.data, "genesinrows",  self.useAttr)
307           
308            self.setItems()
309        else:
310            self.clear()
311
312    def infoSource(self):
313        """ Return the current selected info source getter function from
314        INFO_SOURCES
315        """
316        org = self.organisms[min(self.organismIndex, len(self.organisms) - 1)]
317        if org not in INFO_SOURCES:
318            org = "default"
319        sources = INFO_SOURCES[org]
320        name, func =  sources[min(self.useAltSource, len(sources) - 1)]
321        return name, func
322       
323    def setItems(self):
324        self.warning(0)
325        if not self.data:
326            return
327        if self.useAttr:
328            genes = [attr.name for attr in self.data.domain.attributes]
329        elif self.attributes:
330            attr = self.attributes[self.geneAttr]
331            genes = [str(ex[attr]) for ex in self.data if not ex[attr].isSpecial()]
332        else:
333            genes = []
334        if not genes:
335            self.warning(0, "Could not extract genes from input dataset.")
336        self.warning(1)
337        org = self.organisms[min(self.organismIndex, len(self.organisms) - 1)]
338        source_name, info_getter = self.infoSource()
339        info , currorg = self.currentLoaded
340        self.error(0)
341       
342        self.updateDictyExpressLink(genes, show=org == "352472")
343        self.altSourceCheck.setVisible(org == "352472")
344       
345        # get the info for the genes in a separate thread
346        self.progressBarInit()
347#        call = self.asyncCall(info_getter, (org, genes),
348#                              name="Load NCBI Gene Info",
349#                              blocking=False)
350#        call.connect(call, SIGNAL("progressChanged(float)"), self.progressBarSet, Qt.QueuedConnection)
351#        with orngServerFiles.DownloadProgress.setredirect(call.emitProgressChanged):
352#            call.__call__()
353#            schema, geneinfo = call.get_result()
354#        call.__call__()
355#        schema, geneinfo = call.get_result()
356        with orngServerFiles.DownloadProgress.setredirect(self.progressBarSet):
357            schema, geneinfo = info_getter(org, genes)
358        self.progressBarFinished()
359        # schema, geneinfo = info_getter(org, genes)
360
361        self.geneinfo = geneinfo = list(zip(genes, geneinfo))
362
363        self.progressBarInit()
364        milestones = set([i for i in range(0, len(geneinfo), max(len(geneinfo)/100, 1))])
365        self.cells = cells = []
366        self.row2geneinfo = {}
367        links = []
368        for i, (gene, gi) in enumerate(geneinfo):
369            if gi:
370                row = []
371                for sch, item in zip(schema, gi):
372                    if isinstance(item, Link): # TODO: This should be handled by delegates
373                        row.append(item.text)
374                        links.append(item.link)
375                    else:
376                        row.append(item)
377                cells.append(row)
378                self.row2geneinfo[len(cells) - 1] = i
379#                cells.append([gi.gene_id, gi.symbol + " (%s)" % gene if gene != gi.symbol else gi.symbol,
380#                            gi.locus_tag or "", gi.chromosome or "", gi.description or "",
381#                            ", ".join(gi.synonyms), gi.symbol_from_nomenclature_authority or ""])
382#                links.append("http://www.ncbi.nlm.nih.gov/sites/entrez?Db=gene&Cmd=ShowDetailView&TermToSearch=%s" % gi.gene_id)
383               
384
385            if i in milestones:
386                self.progressBarSet(100.0*i/len(geneinfo))
387        model = TreeModel(cells, [str(col) for col in schema], self.treeWidget)
388       
389        model.setColumnLinks(0, links)
390        proxyModel = QSortFilterProxyModel(self)
391        proxyModel.setSourceModel(model)
392        self.treeWidget.setModel(proxyModel)
393        self.connect(self.treeWidget.selectionModel(), SIGNAL("selectionChanged(QItemSelection , QItemSelection )"), self.commitIf)
394        for i in range(7):
395            self.treeWidget.resizeColumnToContents(i)
396            self.treeWidget.setColumnWidth(i, min(self.treeWidget.columnWidth(i), 200))
397        self.treeWidget.update()
398        self.progressBarFinished()
399
400        self.infoLabel.setText("%i genes\n%i matched NCBI's IDs" % (len(genes), len(cells)))
401        self.matchedInfo = len(genes), len(cells)
402
403    def clear(self):
404        self.infoLabel.setText("No data on input\n")
405        self.treeWidget.setModel(TreeModel([], ["NCBI ID", "Symbol", "Locus Tag",
406                                            "Chromosome", "Description", "Synonyms",
407                                            "Nomenclature"], self.treeWidget))
408        self.geneAttrComboBox.clear()
409        self.send("Selected Examples", None)
410
411    def commitIf(self, *args):
412        if self.autoCommit and not self.selectionUpdateInProgress:
413            self.commit()
414        else:
415            self.selectionChangedFlag = True
416
417    def commit(self):
418        if not self.data:
419            return
420        model = self.treeWidget.model()
421        mapToSource = model.mapToSource
422        selectedIds = [self.cells[mapToSource(index).row()][0] for index in self.treeWidget.selectedIndexes()]
423        selectedRows = self.treeWidget.selectedIndexes()
424        selectedRows = [mapToSource(index).row() for index in selectedRows]
425        model = model.sourceModel()
426       
427        selectedGeneids = [self.row2geneinfo[row] for row in selectedRows]
428        selectedIds = [self.geneinfo[i][0] for i in selectedGeneids]
429        selectedIds = set(selectedIds)
430        gene2row = dict((self.geneinfo[self.row2geneinfo[row]][0], row) \
431                        for row in selectedRows)
432       
433        if self.useAttr:
434            def is_selected(attr):
435                return attr.name in selectedIds
436            attrs = [attr for attr in self.data.domain.attributes if is_selected(attr)]
437            domain = orange.Domain(attrs, self.data.domain.classVar)
438            domain.addmetas(self.data.domain.getmetas())
439            newdata = orange.ExampleTable(domain, self.data)
440            self.send("Selected Examples", newdata)
441        elif self.attributes:
442            attr = self.attributes[self.geneAttr]
443            geneinfo = dict(self.geneinfo)
444            examples = [ex for ex in self.data if str(ex[attr]) in selectedIds]
445            if True:  # Add gene info
446                domain = orange.Domain(self.data.domain, self.data.domain.classVar)
447                domain.addmetas(self.data.domain.getmetas())
448                n_columns = model.columnCount()
449
450                headers = [str(model.headerData(i, Qt.Horizontal, Qt.DisplayRole).toString()) \
451                           for i in range(n_columns)]
452                new_meta_attrs = [(orange.newmetaid(), orange.StringVariable(name)) \
453                                  for name in headers]
454                domain.addmetas(dict(new_meta_attrs))
455                examples = [orange.Example(domain, ex) for ex in examples]
456                for ex in examples:
457                    for i, (_, meta) in enumerate(new_meta_attrs):
458                        row = gene2row[str(ex[attr])]
459                        ex[meta] = str(model.data(model.index(row, i), Qt.DisplayRole).toString())
460
461            if examples:
462                newdata = orange.ExampleTable(examples)
463            else:
464                newdata = None
465            self.send("Selected Examples", newdata)
466        else:
467            self.send("Selected Examples", None)
468           
469    def rowFiltered(self, row):
470        searchStrings = self.searchString.lower().split()
471        row = unicode(" ".join(self.cells[row]).lower(), errors="ignore")
472        return not all([s in row for s in searchStrings])
473   
474    def searchUpdate(self):
475        if not self.data:
476            return
477        searchStrings = self.searchString.lower().split()
478        index = self.treeWidget.model().sourceModel().index
479        mapFromSource = self.treeWidget.model().mapFromSource
480        for i, row in enumerate(self.cells):
481            row = unicode(" ".join(row).lower(), errors="ignore")
482            self.treeWidget.setRowHidden(mapFromSource(index(i, 0)).row(), QModelIndex(), not all([s in row for s in searchStrings]))
483        #self.treeWidget.model().setFilterRegExp(QRegExp(self.searchString, Qt.CaseInsensitive, QRegExp.FixedString))
484           
485    def selectFiltered(self):
486        if not self.data:
487            return
488        itemSelection = QItemSelection()
489       
490        index = self.treeWidget.model().sourceModel().index
491        mapFromSource = self.treeWidget.model().mapFromSource
492        for i, row in enumerate(self.cells):
493            if not self.rowFiltered(i):
494                itemSelection.select(mapFromSource(index(i, 0)), mapFromSource(index(i, 0)))
495        self.treeWidget.selectionModel().select(itemSelection, QItemSelectionModel.Select | QItemSelectionModel.Rows)
496       
497    def sendReport(self):
498        from Orange.OrangeWidgets import OWReport
499        genes, matched = self.matchedInfo
500        info, org = self.currentLoaded
501        self.reportRaw("<p>Input: %i genes of which %i (%.1f%%) matched NCBI synonyms<br>Organism: %s<br>Filter: %s</p>" % (genes, matched, 100.0 * matched / genes, obiTaxonomy.name(org), self.searchString))
502        self.reportSubsection("Gene list")
503        self.reportRaw(reportItemView(self.treeWidget))
504       
505    def updateDictyExpressLink(self, genes, show=False):
506        def fix(ddb):
507            if ddb.startswith("DDB"): 
508                if not ddb.startswith("DDB_G"):
509                    ddb = ddb.replace("DDB", "DDB_G")
510                return ddb
511            return None 
512        if show:
513            genes = [fix(gene) for gene in genes if fix(gene)]
514            link1 = '<a href="http://dictyexpress.biolab.si/run/index.php?gene=%s">Microarray profile</a>'
515            link2 = '<a href="http://dictyexpress.biolab.si/run/index.php?gene=%s&db=rnaseq">RNA-Seq profile</a>'
516            self.linkLabel.setText(link1 + "<br/>" + link2)
517           
518            show = any(genes)
519               
520        if show:
521            self.dictyExpressBox.show()
522        else:
523            self.dictyExpressBox.hide()
524
525    def onDictyExpressLink(self, link):
526        if not self.data:
527            return
528
529        selectedIndexes = self.treeWidget.selectedIndexes()
530        if not len(selectedIndexes):
531            QMessageBox.information(self, "No gene ids selected", "Please select some genes and try again.")
532            return
533        model = self.treeWidget.model()
534        mapToSource = model.mapToSource
535        selectedIds = [self.cells[mapToSource(index).row()][0] for index in selectedIndexes]
536        selectedRows = self.treeWidget.selectedIndexes()
537        selectedRows = [mapToSource(index).row() for index in selectedRows]
538        model = model.sourceModel()
539
540        selectedGeneids = [self.row2geneinfo[row] for row in selectedRows]
541        selectedIds = [self.geneinfo[i][0] for i in selectedGeneids]
542        selectedIds = set(selectedIds)
543
544        def fix(ddb):
545            if ddb.startswith("DDB"):
546                if not ddb.startswith("DDB_G"):
547                    ddb = ddb.replace("DDB", "DDB_G")
548                return ddb
549            return None
550
551        genes = [fix(gene) for gene in selectedIds if fix(gene)]
552        url = str(link) % " ".join(genes)
553        QDesktopServices.openUrl(QUrl(url))
554           
555    def onAltSourceChange(self):
556        self.setItems()
557       
558def reportItemView(view):
559    model = view.model()
560    return reportItemModel(view, model)
561   
562def reportItemModel(view, model, index=QModelIndex()):
563    if not index.isValid() or model.hasChildren(index):
564        columnCount, rowCount = model.columnCount(index), model.rowCount(index)
565        if not index.isValid():
566            text = '<table>\n<tr>' + ''.join('<th>%s</th>' % model.headerData(i, Qt.Horizontal, Qt.DisplayRole).toString() for i in range(columnCount)) +'</tr>\n'
567        else:
568#            variant = model.data(index, Qt.DisplayRole)
569#            text = '<table' + (' caption="%s"' % variant.toString() if variant.isValid() else '') + '>\n'
570            pass
571        text += ''.join('<tr>' + ''.join('<td>' + reportItemModel(view, model, model.index(row, column, index)) + '</td>' for column in range(columnCount)) + '</tr>\n' for row in range(rowCount) if not view.isRowHidden(row, index))
572        text += '</table>'
573        return text
574    else:
575        variant = model.data(index, Qt.DisplayRole)
576        return str(variant.toString()) if variant.isValid() else ""
577       
578if __name__ == "__main__":
579    app = QApplication(sys.argv)
580    data = orange.ExampleTable("brown-selected.tab")
581    w = OWGeneInfo()
582    w.show()
583    w.setData(data)
584    app.exec_()
585    w.saveSettings()
586       
587       
588       
589       
Note: See TracBrowser for help on using the repository browser.