source: orange-bioinformatics/_bioinformatics/widgets/prototypes/OWDifferentiationScale.py @ 1636:10d234fdadb9

Revision 1636:10d234fdadb9, 20.9 KB checked in by mitar, 2 years ago (diff)

Restructuring because we will not be using namespaces.

Line 
1"""<name>Differentiation Scale</name>
2<description></description>
3"""
4
5from __future__ import absolute_import
6
7import os, sys
8import random
9from collections import defaultdict
10from operator import itemgetter, add
11
12import numpy
13
14import Orange
15from Orange.OrangeWidgets import OWGUI
16from Orange.OrangeWidgets.OWWidget import *
17
18from ... import obiDifscale
19
20class OWDifferentiationScale(OWWidget):
21    def __init__(self, parent=None, signalManager=None, title="Differentiation Scale"):
22        OWWidget.__init__(self, parent, signalManager, title, wantGraph=True)
23       
24        self.inputs = [("Gene Expression Samples", Orange.data.Table, self.set_data), ("Additional Expression Samples", Orange.data.Table, self.set_additional_data)]
25        self.outputs = [("Selected Time Points", Orange.data.Table), ("Additional Selected Time Points", Orange.data.Table)]
26       
27        self.selected_time_label = 0
28        self.auto_commit = 0
29       
30        self.loadSettings()
31       
32        self.selection_changed_flag = False
33       
34        #####
35        # GUI
36        #####
37        box = OWGUI.widgetBox(self.controlArea, "Info")
38        self.info_label = OWGUI.widgetLabel(box, "No data on input")
39        self.info_label.setWordWrap(True)
40        self.info_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
41       
42        OWGUI.rubber(self.controlArea)
43       
44        box = OWGUI.widgetBox(self.controlArea, "Selection")
45       
46        cb = OWGUI.checkBox(box, self, "auto_commit", "Commit on any change",
47                            tooltip="Send updated selections automatically",
48                            callback=self.commit_if)
49       
50        b = OWGUI.button(box, self, "Commit",
51                         callback=self.commit,
52                         tooltip="Send selections on output signals")
53       
54        OWGUI.setStopper(self, b, cb, "selection_changed_flag",
55                         callback=self.commit)
56       
57        self.connect(self.graphButton, SIGNAL("pressed()"), self.save_graph)
58       
59        self.scene = QGraphicsScene()
60        self.scene_view = DiffScaleView(self.scene, self.mainArea)
61        self.scene_view.setRenderHint(QPainter.Antialiasing)
62        self.scene_view.setMinimumWidth(300)
63        self.mainArea.layout().addWidget(self.scene_view)
64        self.connect(self.scene, SIGNAL("selectionChanged()"), self.on_selection_changed)
65        self.connect(self.scene_view, SIGNAL("view_resized(QSize)"), lambda size: self.on_view_resized())
66       
67        self.data = None
68        self.additional_data = None
69        self.projections1 = []
70        self.projections2 = []
71        self.labels1 = []
72        self.labels2 = []
73       
74        self.selected_time_samples = [], []
75       
76        self.controlArea.setMaximumWidth(300)
77        self.resize(600, 480)
78       
79    def clear(self):
80        """ Clear the widget state
81        """
82        self.projections1 = []
83        self.projections2 = []
84        self.labels1 = []
85        self.labels2 = []
86        self.clear_selection()
87        self.scene.clear()
88       
89    def clear_selection(self):
90        """ Clear the time point selection.
91        """
92        self.selected_time_samples = [], []
93       
94    def set_data(self, data = None):
95        """ Set the data for the widget.
96        """
97        self.clear()
98        self.data = data
99       
100    def set_additional_data(self, data=None):
101        """ Set an additional data set.
102        """
103        self.clear()
104        self.additional_data = data
105       
106    def handleNewSignals(self):
107        if self.data is not None:
108            self.run_projections()
109            self.projection_layout()
110            self.update_graph()
111           
112            info_text = """\
113Data with {0} genes
114and {1} samples on input.\n""".format(len(self.data),
115                 len(self.data.domain.attributes))
116            if self.additional_data is not None:
117                info_text += """\
118Additional data with {0} genes
119and  {1} samples on input.""".format(len(self.additional_data),
120                                                    len(self.additional_data.domain.attributes))
121            self.info_label.setText(info_text)
122        else:
123            self.send("Selected Time Points", None)
124            self.send("Additional Selected Time Points", None)
125            self.info_label.setText("No data on input\n")
126           
127    def run_projections(self):
128        """ Run obiDifscale.get_projections with the current inputs.
129        """
130        self.error()
131#        try:
132#            attr_set = list(set(a.attributes['time'] for a in data.domain.attributes))
133#            self.time_points = obiDifscale.conv(attr_set, ticks=False)
134#        except KeyError, ex:
135#            self.error("Could not extract time data")
136#            self.clear()
137#            return
138       
139        try:
140            (self.projections1, self.labels1,
141             self.projections2, self.labels2) = \
142                obiDifscale.get_projections(self.data, data2=self.additional_data)
143        except Exception, ex:
144            self.error("Failed to obtain the projections due to: %r" % ex)
145            self.clear()
146            return
147       
148    def projection_layout(self):
149        """ Compute the layout for the projections.
150        """
151        if self.projections1: 
152            projections = self.projections1 + self.projections2
153            projections = numpy.array(projections)
154           
155            x_min = numpy.min(projections)
156            x_max = numpy.max(projections)
157           
158            # Scale projections
159            projections = (projections - x_min) / ((x_max - x_min) or 1.0)
160            projections = list(projections)
161           
162            labels = self.labels1 + self.labels2
163           
164            samples = [(attr, self.data) for attr in self.data.domain.attributes] + \
165                      ([(attr, self.additional_data) for attr in self.additional_data.domain.attributes] \
166                       if self.additional_data is not None else [])
167           
168            # TODO: handle samples with the same projection
169            # the point_layout should return the proj to sample mapping instead
170            proj_to_sample = dict([((label, proj), sample) for label, proj, sample \
171                                   in zip(labels, projections, samples)])
172            self.proj_to_sample = proj_to_sample
173           
174            time_points = point_layout(labels, projections)
175            self.time_points = time_points
176            level_height = 20
177            all_points = numpy.array(reduce(add, [p for _, p in time_points], []))
178            self.all_points = all_points
179           
180#            all_points[:, 1] *= -level_height
181            self.time_samples = [] # samples for time label (same order as in self.time_points)
182           
183            point_i = 0
184            for label, points, in time_points:
185                samples = [] 
186                for x, y in points:
187                    samples.append(proj_to_sample.get((label, x), None))
188                self.time_samples.append((label, samples))
189           
190    def update_graph(self):
191        """ Populate the Graphics Scene with the current projections.
192        """
193        scene_size_hint = self.scene_view.viewport().size()
194        scene_size_hint = QSizeF(max(scene_size_hint.width() - 50, 100),
195                                 scene_size_hint.height())
196        self.scene.clear()
197       
198        if self.projections1:
199            level_height = 20
200            all_points = self.all_points.copy()
201            all_points[:, 0] *= scene_size_hint.width()
202            all_points[:, 1] *= -level_height
203           
204            point_i = 0
205            centers = []
206            z_value = 0
207            for label, samples in self.time_samples:
208                # Points
209                p1 = all_points[point_i]
210                points = all_points[point_i: point_i + len(samples), :]
211                for (x, y), sample in zip(points, samples):
212                    item = GraphicsTimePoint(QRectF(QPointF(x-3, y-3), QSizeF(6, 6)))
213                    item.setBrush(QBrush(Qt.black))
214                    item.sample = sample
215                    item.setToolTip(sample[0].name if sample else "")
216                    item.setZValue(z_value)
217                    self.scene.addItem(item)
218                    point_i += 1
219                p2 = all_points[point_i - 1]
220               
221                # Line over all points
222                line = QGraphicsLineItem(QLineF(*(tuple(p1) + tuple(p2))))
223                line.setPen(QPen(Qt.black, 2))
224                line.setZValue(z_value - 1)
225                self.scene.addItem(line)
226               
227                # Time label on top of the median
228                n_points = len(points)
229                if n_points % 2:
230                    center = points[n_points / 2]
231                else:
232                    center = (points[n_points / 2] + points[n_points / 2 + 1]) / 2.0
233                centers.append(center)
234                x, y = center
235                text = QGraphicsSimpleTextItem(label)
236                w = text.boundingRect().width()
237                text.setPos(x - w / 2.0, y - 17.5)
238                self.scene.addItem(text)
239           
240            self.scene.addLine(QLineF(0.0, 0.0, scene_size_hint.width(), 0.0))
241           
242            polygon = QPolygonF([QPointF(3.0, 0.0),
243                                 QPointF(-2.0, -2.0),
244                                 QPointF(0.0, 0.0),
245                                 QPointF(-2.0, 2.0),
246                                 QPointF(3.0, 0.0)])
247           
248            arrow = QGraphicsPolygonItem(polygon)
249            arrow.setBrush(QBrush(Qt.black))
250            arrow.setPos(scene_size_hint.width(), 0.0)
251            arrow.scale(2, 2)
252            self.scene.addItem(arrow)
253           
254            title = QGraphicsSimpleTextItem("Development (time)")
255            font = self.font()
256            font.setPointSize(10)
257            title.setFont(font)
258            w = title.boundingRect().width()
259            title.setPos(scene_size_hint.width() - w, -15)
260            self.scene.addItem(title)
261           
262            rects = []
263            ticks = []
264            axis_label_items = []
265            labels = [(center, label) for center, (label, _) in zip(centers, self.time_samples)]
266            labels = sorted(labels, key=lambda (c, l): c[0])
267            for center, label in labels:
268                x, y = center
269                item = QGraphicsSimpleTextItem(label)
270                w = item.boundingRect().width()
271                item.setPos(x - w / 2.0, 4.0)
272                rects.append(item.sceneBoundingRect().normalized())
273                ticks.append(QPointF(x - w / 2.0, 4.0))
274                axis_label_items.append(item)
275           
276#            rects = SA_axis_label_layout(ticks, rects, max_time=0.5,
277#                                         x_factor=scene_size_hint.width() / 50.0,
278#                                         y_factor=10,
279#                                         random=random.Random(0))
280
281            rects = greedy_scale_label_layout(ticks, rects, spacing=5)
282           
283            for (tick, label), rect, item in zip(labels, rects, axis_label_items):
284                x, y = tick
285                self.scene.addLine(x, -2, x, 2)
286                if rect.top() - item.pos().y() > 5:
287                    self.scene.addLine(x, 2, rect.center().x(), 14.0)
288                if rect.top() - item.pos().y() > 15:
289                    self.scene.addLine(rect.center().x(), 14.0, rect.center().x(), rect.top())
290#                item.setPos(rect.topLeft())
291               
292#                text = QGraphicsSimpleTextItem(label)
293#            for tick, rect, item in zip(ticks, rects, axis_label_items):
294                item.setPos(rect.topLeft())
295                self.scene.addItem(item)
296#                w = text.boundingRect().width()
297#                text.setPos(x - w / 2.0, 4)
298                # Need to compute axis label layout.
299#                self.scene.addItem(text)
300
301            self.scene.setSceneRect(self.scene.itemsBoundingRect().adjusted(-10, -10, 10, 10))
302
303    def on_view_resized(self):
304        self.update_graph()
305       
306    def on_selection_changed(self):
307        try:
308            selected = self.scene.selectedItems()
309        except RuntimeError:
310            return
311       
312        selected_attrs1 = []
313        selected_attrs2  =[]
314        for point in selected:
315            attr, data = point.sample if point.sample else (None, None)
316            if data is self.data:
317                selected_attrs1.append(attr)
318            elif data is self.additional_data:
319                selected_attrs2.append(attr)
320               
321        self.selected_time_samples = selected_attrs1, selected_attrs2
322        print self.selected_time_samples
323        self.commit_if()
324           
325    def commit_if(self):
326        if self.auto_commit:
327            self.commit()
328        else:
329            self.selection_changed_flag = True
330   
331    def commit(self):
332        if self.data is not None:
333            selected1, selected2 = self.selected_time_samples
334            attrs1 = [a for a in self.data.domain.attributes \
335                      if a in selected1]
336            domain = Orange.data.Domain(attrs1, self.data.domain.class_var)
337            domain.add_metas(self.data.domain.get_metas())
338            data = Orange.data.Table(domain, self.data)
339            self.send("Selected Time Points", data)
340           
341            if self.additional_data is not None:
342                attrs2 = [a for a in self.additional_data.domain.attributes \
343                          if a in selected2]
344                domain = Orange.data.Domain(attrs2, self.additional_data.domain.class_var)
345                domain.add_metas(self.additional_data.domain.get_metas())
346                data = Orange.data.Table(domain, self.additional_data)
347                self.send("Additional Selected Time Points", data)
348        else:
349            self.send("Selected Time Points", None)
350            self.send("Additional Selected Time Points", None)
351        self.selection_changed_flag = False
352       
353    def save_graph(self):
354        from Orange.OrangeWidgets.OWDlgs import OWChooseImageSizeDlg
355        dlg = OWChooseImageSizeDlg(self.scene, parent=self)
356        dlg.exec_()
357   
358   
359class GraphicsTimePoint(QGraphicsEllipseItem):
360    def __init__(self, *args):
361        QGraphicsEllipseItem.__init__(self, *args)
362        self.setFlags(QGraphicsItem.ItemIsSelectable)
363        self.setAcceptsHoverEvents(True)
364        self._is_hovering = False
365       
366    def paint(self, painter, option, widget=0):
367        if self.isSelected():
368            brush = QBrush(Qt.red)
369            pen = QPen(Qt.red, 1)
370        else:
371            brush = QBrush(Qt.darkGray)
372            pen = QPen(Qt.black, 1)
373        if self._is_hovering:
374            brush = QBrush(brush.color().darker(200))
375        painter.save()
376        painter.setBrush(brush)
377        painter.setPen(pen)
378        painter.drawEllipse(self.rect())
379        painter.restore()
380       
381    def hoverEnterEvent(self, event):
382        self._is_hovering = True
383        self.update()
384        return QGraphicsEllipseItem.hoverEnterEvent(self, event)
385   
386    def hoverLeaveEvent(self, event):
387        self._is_hovering = False
388        self.update()
389        return QGraphicsEllipseItem.hoverLeaveEvent(self, event)
390       
391   
392class DiffScaleView(QGraphicsView):
393    def resizeEvent(self, event):
394        QGraphicsView.resizeEvent(self, event)
395        self.emit(SIGNAL("view_resized(QSize)"), event.size())
396       
397
398def point_layout(labels, points, label_size_hints=None):
399    groups = defaultdict(list)
400    for label, point in zip(labels, points):
401        groups[label].append(point)
402       
403    for label, points in list(groups.items()):
404        points = sorted(points)
405        # TODO: Use label_size_hints for min, max
406        groups[label] = (points, (points[0], points[-1]))
407   
408    sorted_groups = sorted(groups.items(), key=itemgetter(1), reverse=True)
409    levels = {}
410    curr_level = 1
411    label_levels = {}
412    while sorted_groups:
413        label, (points, (x_min, x_max)) = sorted_groups.pop(-1)
414        max_level_pos = levels.get(curr_level, x_min)
415        if x_min < max_level_pos:
416            curr_level += 1
417            sorted_groups.append((label, (points, (x_min, x_max))))
418        else:
419            label_levels[label] = curr_level
420            levels[curr_level] = x_max
421            curr_level = 1
422           
423    for label, (points, _) in list(groups.items()):
424        level = float(label_levels[label])
425        groups[label] = [(x, level) for x in points]
426       
427    return list(groups.items())
428
429   
430def greedy_scale_label_layout(ticks, rects, spacing=3):
431    """ Layout the labels at ticks on a linear scale, by raising the
432    overlapping labels.
433   
434    """
435    def adjust_interval(start, end, min_v, max_v):
436        """ Adjust (start, end) interval to fit inside the (min_v, max_v).
437        """
438        if start < min_v:
439            return (min_v, min_v + (end - start))
440        elif max_v > end:
441            return (max_v - (end - start), max_v)
442        else:
443            return (start, end)
444       
445    def center_interval(start, end, center):
446        """ Center the interval on `center`
447        """
448        span = end - start
449        return centered(center, span)
450   
451    def centered(center, span):
452        """ Return an centered interval with span.
453        """
454        return (center - span / 2.0, center + span / 2.0)
455   
456    def contains((start, end), (start1, end1)):
457        return start <= start1  and end >= end1
458   
459    def fit(work, ticks, min_x, max_x):
460        """ Fit the work set between min_x and max_x  and centered on the
461        ticks, if possible.
462        """
463        fits = False
464        work_set = map(QRectF, work)
465        tick_center = sum([r.center().x() for r in work_set]) / len(work_set)
466        if len(work_set) == 1:
467            if work_set[0].left() >= min_x and work_set[0].right() <= max_x:
468                return work_set
469            else:
470                return []
471       
472        elif len(work_set) == 2: # TODO: MErge this with the > 2
473            w_sum = sum([r.width() for r in work_set]) + spacing
474            if w_sum < max_x - min_x:
475                r1, r2 = work_set
476                interval = centered(tick_center, w_sum)
477               
478                if not contains((min_x, max_x), interval):
479                    interval = adjust_interval(*(interval + (min_x, max_x)))
480                   
481                if contains((min_x, max_x), interval):
482                    r1.moveLeft(interval[0])
483                    r2.moveLeft(interval[1] - r2.width())
484                    r1.moveTop(r1.top() + 10)
485                    r2.moveTop(r2.top() + 10)
486                    return work_set
487                else:
488                    return []
489            else:
490                return []
491       
492        elif len(work_set) > 2:
493            center = (work_set[0].center().x() + work_set[-1].center().x()) / 2.0
494            w_sum = work_set[0].width() / 2.0 + work_set[-1].width() / 2.0 + spacing
495            for i, r in enumerate(work_set[1:-1]):
496                w_sum += r.width() + spacing
497            interval = centered(center, w_sum)
498           
499            if not contains((min_x, max_x), interval):
500                interval = adjust_interval(*(interval + (min_x, max_x)))
501               
502            if contains((min_x, max_x), interval):
503                istart, iend = interval
504                rstart, rend = work_set[0], work_set[-1]
505                rstart.moveLeft(istart)
506                rstart.moveTop(rstart.top() + 10)
507                rend.moveLeft(iend - rend.width())
508                rend.moveTop(rend.top() + 10)
509                istart += rstart.width() / 2.0
510                iend -= rend.width() / 2.0
511                for r in work_set[1: -1]:
512                    r.moveLeft(istart)
513                    r.moveTop(r.top() + 20)
514                    istart += r.width() + spacing
515                return work_set
516            else:
517                return []
518           
519    queue = sorted(zip(ticks, rects),
520                   key=lambda (t, _): t.x(),
521                   reverse=True)
522    done = False
523    rects = []
524   
525    min_x = -1e30
526    max_x = 1e30
527   
528    while queue:
529        work_set = [queue.pop(-1)]
530        set_fits = False
531        max_x = queue[-1][1].left() if queue else 1e30
532        while not set_fits:
533            new_rects = fit(map(itemgetter(1), work_set),
534                            map(itemgetter(0), work_set),
535                            min_x, max_x)
536            if new_rects: # Can the work set be fit.
537                set_fits = True
538                rects.extend(new_rects)
539                min_x = work_set[-1][1].right()
540               
541            else:
542                # Extend the work set with one more label rect
543                work_set.append(queue.pop(-1))
544                max_x = queue[-1][1].left() if queue else 1e30
545    return rects
546       
547   
548if __name__ == "__main__":
549    app = QApplication(sys.argv)
550    w = OWDifferentiationScale()
551    data = Orange.data.Table(os.path.expanduser("~/Documents/GDS2666n"))
552    w.show()
553    w.set_data(data)
554    w.handleNewSignals()
555    app.exec_()
556    w.saveSettings()
Note: See TracBrowser for help on using the repository browser.