source: orange-bioinformatics/orangecontrib/bio/widgets/prototypes/OWDifferentiationScale.py @ 1873:0810c5708cc5

Revision 1873:0810c5708cc5, 20.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Moved '_bioinformatics' into orangecontrib namespace.

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