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

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

Restructuring because we will not be using namespaces.

Line 
1"""
2<name>Heat Map</name>
3<description>Heatmap visualization.</description>
4<contact>Ales Erjavec, Blaz Zupan, Janez Demsar</contact>
5<icon>icons/HeatMap.png</icon>
6<priority>1040</priority>
7"""
8
9from __future__ import absolute_import
10
11from collections import defaultdict
12import itertools
13import math
14
15import orange
16import orangene
17from Orange.orng import orngClustering
18from Orange.OrangeWidgets import OWColorPalette
19from Orange.OrangeWidgets import OWGUI
20from Orange.OrangeWidgets.ColorPalette import signedPalette
21from Orange.OrangeWidgets.OWClustering import HierarchicalClusterItem, DendrogramWidget, DendrogramItem
22from Orange.OrangeWidgets.OWDlgs import OWChooseImageSizeDlg
23from Orange.OrangeWidgets.OWWidget import *
24
25DEBUG = False
26
27#BUG: OWHeatMap does not support heatmaps which need a image which is larger than maxint X maxint pixels!
28#and no warning appears
29
30import warnings
31warnings.filterwarnings("ignore", "'strain'", orange.AttributeWarning)
32
33def split_domain(domain, split_label):
34    """ Split the domain based on values of `split_label` value.
35    """
36    groups = defaultdict(list)
37    for attr in domain.attributes:
38        groups[attr.attributes.get(split_label)].append(attr)
39       
40    attr_values = [attr.attributes.get(split_label) for attr in domain.attributes]
41   
42    domains = []
43    for value, attrs in groups.items():
44        group_domain = orange.Domain(attrs, domain.class_var)
45        group_domain.add_metas(domain.get_metas())
46        domains.append((value, group_domain))
47       
48    if domains:
49        assert(all(len(dom) == len(domains[0][1]) for _, dom in domains))
50       
51    return sorted(domains, key=lambda t: attr_values.index(t[0]))
52   
53def vstack_by_subdomain(data, sub_domains):
54    domain = sub_domains[0]
55    newtable = orange.ExampleTable(domain)
56   
57    for sub_dom in sub_domains:
58        for ex in data:
59            vals = [ex[a].native() for a in sub_dom]
60            newtable.append(orange.Example(domain, vals))
61   
62    return newtable
63   
64   
65def select_by_class(data, class_):
66    indices = select_by_class_indices(data, class_)
67    return data.select(indices)
68
69def select_by_class_indices(data, class_):
70    return [1 if class_ == ex.getclass() else 0 for ex in data]
71
72def group_by_unordered(iterable, key):
73    groups = defaultdict(list)
74    for item in iterable:
75        groups[key(item)].append(item)
76    return groups.items()
77
78def hierarchical_cluster_ordering(data, group_domains=None, opt_order=False, progress_callback=None):
79    classVar = data.domain.classVar
80    if classVar and isinstance(classVar, orange.EnumVariable):
81        class_data = [select_by_class_indices(data, val) for val in data.domain.classVar.values]
82    else:
83        class_data = [[1] * len(data)]
84       
85    parts = len(class_data) + 1 
86   
87    def pp_callback(part):
88        def callback(value):
89            return progress_callback(value / parts + 100.0 * part / parts)
90        if progress_callback:
91            callback(100.0 * part / parts)
92            return callback
93        else:
94            return progress_callback
95       
96    if group_domains is not None and len(group_domains) > 1:
97        stacked = vstack_by_subdomain(data, group_domains)
98    else:
99        stacked = data   
100       
101    attr_cluster = orngClustering.hierarchicalClustering_attributes(stacked, order=opt_order, progressCallback=pp_callback(0))
102    attr_ordering = list(attr_cluster.mapping)
103       
104    def indices_map(indices):
105        map = zip(range(len(indices)), indices)
106        map = [i for i, test in map if test]
107        return dict(enumerate(map))
108   
109    data_ordering = []
110    data_clusters = []
111    for i, indices in enumerate(class_data):
112        sub_data = data.select(indices)
113        cluster = orngClustering.hierarchicalClustering(sub_data, order=opt_order, progressCallback=pp_callback(i + 1))
114        ind_map = indices_map(indices)
115        data_ordering.append([ind_map[m] for m in cluster.mapping])
116        data_clusters.append(cluster)
117       
118    return attr_ordering, attr_cluster, data_ordering, data_clusters 
119       
120
121##############################################################################
122# parameters that determine the canvas layout
123
124c_offsetX = 10; c_offsetY = 10  # top and left border
125c_spaceY = 10                   # space btw graphical elements
126c_spaceAverageX = 5             # space btw stripe with average and microarray
127c_legendHeight = 15             # height of the legend
128c_averageStripeWidth = 12       # width of the stripe with averages
129
130z_heatmap = 5                   # layer with heatmaps
131
132##############################################################################
133# main class
134
135class ExampleTableContextHandler(ContextHandler):
136    def match(self, context, imperfect, examples):
137        return context.checksum == examples.checksum() and 2
138   
139    def findOrCreateContext(self, widget, examples):
140        context, isNew = ContextHandler.findOrCreateContext(self, widget, examples)
141        if not context:
142            return None, False
143        context.checksum = examples.checksum()
144        return context, isNew
145   
146from .OWGenotypeDistances import SetContextHandler
147
148class OWHeatMap(OWWidget):
149    contextHandlers = {"": DomainContextHandler("", ["CellWidth", "CellHeight"]),
150                       "Selection": ExampleTableContextHandler("Selection", contextDataVersion=2)}
151   
152    settingsList = ["CellWidth", "CellHeight", "SpaceX", "Merge",
153                    "Gamma", "CutLow", "CutHigh", "CutEnabled", 
154                    "ShowAnnotation", "LegendOnTop", "LegendOnBottom",
155                    "ShowAverageStripe", "ShowGroupLabel",
156                    "MaintainArrayHeight",
157                    "GShowToolTip", "BShowColumnID", "BShowSpotIndex",
158                    "BShowAnnotation", 'BShowGeneExpression',
159                    "BSpotVar", "ShowGeneAnnotations",
160                    "ShowDataFileNames", "BAnnotationVar",
161                    "SelectionType",
162                    "CurrentPalette", "SortGenes", "colorSettings", "selectedSchemaIndex",
163                    "palette", "ShowColumnLabels", "ColumnLabelPosition"]
164
165    def __init__(self, parent=None, signalManager = None):
166        OWWidget.__init__(self, parent, signalManager, 'HeatMap', TRUE)
167       
168        self.inputs = [("Examples", ExampleTable, self.set_dataset)]
169        self.outputs = [("Examples", ExampleTable, Default)]
170
171        #set default settings
172        self.CellWidth = 3; self.CellHeight = 3
173        self.SpaceX = 10
174        self.Merge = 1; self.savedMerge = self.Merge
175        self.Gamma = 1
176        self.CutLow = self.CutHigh = self.CutEnabled = 0
177        self.ShowAnnotation = 0
178        self.LegendOnTop = 0           # legend stripe on top (bottom)?
179        self.LegendOnBottom = 1
180        self.ShowLegend = 1
181        self.ShowGroupLabel = 1        # show class names in case of classified data?
182        self.ShowAverageStripe = 0     # show the stripe with the evarage
183        self.MaintainArrayHeight = 0   # adjust cell height while changing the merge factor
184        self.GShowToolTip = 1          # balloon help
185        self.ShowGeneAnnotations = 1   # show annotations for genes
186        self.ShowColumnLabels = 1
187        self.ColumnLabelPosition = 0
188        self.ShowDataFileNames = 1     # show the names of the data sets (only in case of multiple files)
189        self.BShowColumnID = 1; self.BShowSpotIndex = 1; self.BShowAnnotation = 1; self.BShowGeneExpression = 1
190        self.BSpotVar = None; self.BAnnotationVar = None  # these are names of variables
191        self.BSpotIndx = None; self.BAnnotationIndx = None # these are id's of the combo boxes
192        self.SortGenes = 1
193        self.ShowClustering = 1
194        self.SelectionType = 0         # selection on a single data set
195        self.setColorPalette()
196        self.refFile = 0               # position index of a reference file
197        self.selectedFile = None       # position of the selected file in the list box
198
199        self.colorSettings =None
200        self.selectedSchemaIndex = 0
201        self.auto_commit = True
202        self.selection_changed_flag = False
203
204        self.palette = self.ColorPalettes[0]
205       
206        self.loadSettings()
207        self.data = []
208        self.maxHSize = 30; self.maxVSize = 15
209
210
211        # GUI definition
212        self.connect(self.graphButton, SIGNAL("clicked()"), self.saveFig)
213        self.tabs = OWGUI.tabWidget(self.controlArea)
214
215        # SETTINGS TAB
216        settingsTab = OWGUI.createTabPage(self.tabs, "Settings")
217        box = OWGUI.widgetBox(settingsTab, "Cell Size (Pixels)", addSpace=True)
218        OWGUI.qwtHSlider(box, self, "CellWidth", label='Width: ',
219                         labelWidth=38, minValue=1, maxValue=self.maxHSize,
220                         step=1, precision=0, callback=self.update_cell_size)
221       
222#        OWGUI.hSlider(box, self, "CellWidth", label="Width:", minValue=1,
223#                      maxValue=self.maxHSize, step=1, ticks=5,
224#                      callback=self.update_cell_size,
225#                      tooltip="Width of each heatmap cell.")
226       
227        self.sliderVSize = OWGUI.qwtHSlider(box, self, "CellHeight",
228                                label='Height: ', labelWidth=38,
229                                minValue=1, maxValue=self.maxVSize,
230                                step=1, precision=0,
231                                callback=self.update_cell_size)
232#        self.sliderVSize = OWGUI.hSlider(box, self, "CellHeight",
233#                      label='Height:',
234#                      minValue=1, maxValue=50, ticks=5,
235#                      callback = self.update_cell_size)
236       
237        OWGUI.qwtHSlider(box, self, "SpaceX", label='Space: ', 
238                         labelWidth=38, minValue=0, maxValue=50,
239                         step=2, precision=0,
240                         callback=self.update_grid_spacing)
241#                         callback=self.drawHeatMap)
242       
243        OWGUI.qwtHSlider(settingsTab, self, "Gamma", box="Gamma",
244                         minValue=0.1, maxValue=1, step=0.1,
245                         callback=self.update_color_schema)
246#                         callback=self.drawHeatMap)
247       
248        OWGUI.separator(settingsTab)
249
250        # define the color stripe to show the current palette
251        box = OWGUI.widgetBox(settingsTab, "Color", orientation = "horizontal")
252        self.colorCombo = OWColorPalette.PaletteSelectorComboBox(self)
253        try:
254            self.colorCombo.setPalettes("palette", self.createColorDialog())
255        except Exception, ex:
256            print >> sys.stderr, ex, "Error loading saved color palettes!\nCreating new default palette!"
257            self.colorSettings = None
258            self.colorCombo.setPalettes("palette", self.createColorDialog())
259        self.colorCombo.setCurrentIndex(self.selectedSchemaIndex)
260        self.setColor(self.selectedSchemaIndex, update=False)
261        self.connect(self.colorCombo, SIGNAL("activated(int)"), self.setColor)
262        box.layout().addWidget(self.colorCombo, 2)
263        button = OWGUI.button(box, self, "Edit colors", callback=self.openColorDialog, tooltip="Edit the heatmap color palette", debuggingEnabled=0)
264       
265        OWGUI.separator(settingsTab)
266
267        OWGUI.comboBox(settingsTab, self, "SortGenes", "Sort genes",
268                       items=["No sorting", "Sort genes", "Clustering",
269                              "Clustering with leaf ordering"],
270                               callback=self.update_sorting)
271        OWGUI.rubber(settingsTab)
272       
273        # FILTER TAB
274        tab = OWGUI.createTabPage(self.tabs, "Filter")
275        box = OWGUI.widgetBox(tab, "Threshold Values", addSpace=True)
276        OWGUI.checkBox(box, self, 'CutEnabled', "Enabled",
277                       callback=self.update_thresholds)
278       
279        self.sliderCutLow = OWGUI.qwtHSlider(box, self, 'CutLow', label='Low:', 
280                            labelWidth=40, minValue=-100, maxValue=0, step=0.1,
281                            precision=1, ticks=0, maxWidth=80,
282                            callback=self.update_thresholds)
283       
284        self.sliderCutHigh = OWGUI.qwtHSlider(box, self, 'CutHigh', label='High:', 
285                            labelWidth=40, minValue=0, maxValue=100, step=0.1,
286                            precision=1, ticks=0, maxWidth=80,
287                            callback=self.update_thresholds)
288       
289        if not self.CutEnabled:
290            self.sliderCutLow.box.setDisabled(1)
291            self.sliderCutHigh.box.setDisabled(1)
292
293        box = OWGUI.widgetBox(tab, "Merge", addSpace=True)
294##        OWGUI.qwtHSlider(box, self, "Merge", label='Rows:', labelWidth=33, minValue=1, maxValue=500, step=1, callback=self.mergeChanged, precision=0, ticks=0)
295        OWGUI.spin(box, self, "Merge", min=1, max=500, step=1, label='Rows:',
296#                   callback=self.mergeChanged,
297                   callback=self.on_merge_changed,
298                   callbackOnReturn=True)
299        OWGUI.checkBox(box, self, 'MaintainArrayHeight', "Maintain array height")
300        OWGUI.rubber(tab)
301
302        # INFO TAB
303        tab = OWGUI.createTabPage(self.tabs, "Info")
304
305        box = OWGUI.widgetBox(tab,'Annotation && Legends')
306        OWGUI.checkBox(box, self, 'ShowLegend', 'Show legend', 
307                       callback=self.update_legend)
308        OWGUI.checkBox(box, self, 'ShowAverageStripe', 'Stripes with averages', 
309                       callback=self.update_averages_stripe)
310        self.geneAnnotationsCB = OWGUI.checkBox(box, self, 'ShowGeneAnnotations', 'Gene annotations', 
311                                                callback=self.update_annotations)
312       
313        self.annotationCombo = OWGUI.comboBox(box, self, "BAnnotationIndx", items=[],
314                                              callback=self.update_annotations) 
315                                              #callback=lambda x='BAnnotationVar', y='BAnnotationIndx': self.setMetaID(x, y))
316
317        box = OWGUI.widgetBox(tab, 'Column Labels')
318        columnLabelCB = OWGUI.checkBox(box, self, "ShowColumnLabels", "Display column labels",
319                                       callback=self.update_column_annotations)
320        posbox = OWGUI.widgetBox(OWGUI.indentedBox(box), "Position", flat=True)
321        comboBox = OWGUI.comboBox(posbox, self, "ColumnLabelPosition", 
322                                  items=["Top", "Bottom"], 
323                                  callback=self.update_column_annotations)
324        columnLabelCB.disables.append(comboBox.box)
325        columnLabelCB.makeConsistent()
326       
327        box = OWGUI.widgetBox(tab, "Tool Tips")
328        cb = OWGUI.checkBox(box, self, 'GShowToolTip', "Show tool tips")
329        box = OWGUI.widgetBox(OWGUI.indentedBox(box), "Tool Tip Info")
330        box.setFlat(True)
331        OWGUI.checkBox(box, self, 'BShowColumnID', "Column ID")
332        self.spotIndxCB = OWGUI.checkBox(box, self, 'BShowSpotIndex', "Spot Index", \
333            callback=lambda: self.spotCombo.setDisabled(not self.BShowSpotIndex))
334        self.spotCombo = OWGUI.comboBox(box, self, "BSpotIndx", items=[], \
335            callback=lambda x='BSpotVar', y='BSpotIndx': self.setMetaID(x, y))
336        OWGUI.checkBox(box, self, 'BShowGeneExpression', "Gene expression")
337        OWGUI.checkBox(box, self, 'BShowAnnotation', "Annotation")
338        self.toolTipInfoBox = box
339        cb.disables.append(box)
340        cb.makeConsistent()
341        OWGUI.rubber(tab)
342
343        # SPLIT TAB
344        self.splitTab = OWGUI.createTabPage(self.tabs, "Split Data")
345        box = OWGUI.widgetBox(self.splitTab, "Split By")
346        self.splitLB = QListWidget(box)
347        box.layout().addWidget(self.splitLB)
348        self.connect(self.splitLB, SIGNAL("itemSelectionChanged()"), self.split_changed)
349
350        # Scene with microarray
351        self.heatmap_scene = self.scene = HeatmapScene()
352        self.selection_manager = HeatmapSelectionManager(self)
353        self.connect(self.selection_manager, SIGNAL("selection_changed()"), self.on_selection_changed)
354        self.connect(self.selection_manager, SIGNAL("selection_finished()"), self.on_selection_finished)
355        self.heatmap_scene.set_selection_manager(self.selection_manager)
356        item = QGraphicsRectItem(0, 0, 10, 10, None, self.heatmap_scene)
357        self.heatmap_scene.itemsBoundingRect()
358        self.heatmap_scene.removeItem(item)
359       
360        self.sceneView = QGraphicsView(self.scene)
361        self.currentHighlightedCluster = None
362        self.selectedClusters = []
363        self.mainArea.layout().addWidget(self.sceneView)
364        self.heatmap_scene.widget = None
365        self.heatmap_widget_grid = [[]]
366        self.attr_annotation_widgets = []
367        self.attr_dendrogram_widgets = []
368        self.gene_annotation_widgets = []
369        self.gene_dendrogram_widgets = []
370       
371        self.heatmaps = []
372       
373        self.selection_rects = []
374       
375        self.attr_cluster = None
376        self.data_clusters = []
377        self.sorted_data = None
378       
379        self._ordering_cache = {}
380       
381        self.resize(800,400)
382
383    def createColorStripe(self, palette):
384        dx = 104; dy = 18
385        bmp = chr(252)*dx*2 + reduce(lambda x,y:x+y, \
386           [chr(i*250/dx) for i in range(dx)] * (dy-4)) + chr(252)*dx*2 
387       
388        image = QImage(bmp, dx, dy, QImage.Format_Indexed8)
389        image.setColorTable(signedPalette(self.ColorPalettes[palette]))
390
391        pm = QPixmap.fromImage(image, Qt.AutoColor);
392        return pm
393
394    # set the default palettes used in the program
395    # palette defines 256 colors, 250 are used for heat map, remaining 6 are extra
396    # color indices for unknown is 255, underflow 253, overflow 254, white 252
397    def setColorPalette(self):
398        white = qRgb(255,255,255)
399        gray = qRgb(200,200,200)
400        self.ColorPalettes = \
401          ([qRgb(255.*i/250., 255.*i/250., 255-(255.*i/250.)) \
402            for i in range(250)] + [white]*3 + [qRgb(0., 0., 255.), qRgb(255., 255., 0.), gray],
403           [qRgb(0, 255.*i*2/250., 0) for i in range(125, 0, -1)] \
404           + [qRgb(255.*i*2/250., 0, 0) for i in range(125)] + [white]*3 \
405           + [qRgb(0, 255., 0), qRgb(255., 0, 0), gray],
406           [qRgb(255.*i/250., 0, 0) for i in range(250)] + [white]*3 \
407           + [qRgb(0., 0, 0), qRgb(255., 0, 0), gray])
408        self.SelectionColors = [QColor(0,0,0), QColor(255,255,128), QColor(0,255,255)]
409        self.CurrentPalette = 0
410       
411    def getGammaCorrectedPalette(self):
412        return [QColor(*self.contPalette.getRGB(float(i)/250, gamma=self.Gamma)).rgb() for i in range(250)] + self.palette[-6:]
413
414    def setColor(self, index, dialog=None, update=True):
415        self.selectedSchemaIndex = index
416        if not dialog:
417            dialog = self.createColorDialog()
418
419        self.colorCombo.setPalettes("palette", dialog)
420        self.colorCombo.setCurrentIndex(self.selectedSchemaIndex)
421        self.contPalette = palette = dialog.getExtendedContinuousPalette("palette")
422        unknown = dialog.getColor("unknown").rgb()
423        underflow = dialog.getColor("underflow").rgb()
424        overflow = dialog.getColor("overflow").rgb()
425        self.palette = [QColor(*palette.getRGB(float(i)/250, gamma=self.Gamma)).rgb() for i in range(250)] + [qRgb(255, 255, 255)]*3 +[underflow, overflow, unknown]
426
427        if update:
428            self.update_color_schema()
429#            self.drawHeatMap()
430       
431    def openColorDialog(self):
432        dialog = self.createColorDialog()
433        if dialog.exec_():
434            self.colorSettings = dialog.getColorSchemas()
435            self.selectedSchemaIndex = dialog.selectedSchemaIndex
436            self.colorCombo.setCurrentIndex(self.selectedSchemaIndex)
437            self.setColor(self.selectedSchemaIndex, dialog)
438
439    def createColorDialog(self):
440        c = OWColorPalette.ColorPaletteDlg(self, "Color Palette")
441        c.createExtendedContinuousPalette("palette", "Continuous Palette", initialColor1=QColor(Qt.blue), initialColor2=QColor(255, 255, 0).rgb(), extendedPassThroughColors = ((Qt.red, 1), (Qt.darkYellow, 1), (Qt.black, 1), (Qt.magenta, 1), (Qt.green, 1)))
442        box = c.createBox("otherColors", "Other Colors")
443        c.createColorButton(box, "unknown", "Unknown", Qt.gray)
444        box.layout().addSpacing(5)
445        c.createColorButton(box, "overflow", "Overflow", Qt.black)
446        box.layout().addSpacing(5)
447        c.createColorButton(box, "underflow", "Underflow", Qt.white)
448        c.setColorSchemas(self.colorSettings, self.selectedSchemaIndex)
449        return c
450   
451    # any time the data changes, the two combo boxes showing meta attributes
452    # have to be adjusted
453    def setMetaCombo(self, cb, value, enabled=1, default=None):
454        cb.clear()
455        if len(self.meta)==0:
456            cb.setDisabled(True)
457            self.spotIndxCB.setDisabled(1); self.geneAnnotationsCB.setDisabled(1)
458            return (None, None)
459        cb.setDisabled(not enabled)
460        self.spotIndxCB.setEnabled(1); self.geneAnnotationsCB.setEnabled(1)
461        for m in self.meta:
462            cb.addItem(m)
463       
464        if not (value in self.meta):
465            if default in self.meta:
466                value = default
467            else:
468                value = None
469
470        if value in self.meta:
471            cb.setCurrentIndex(self.meta.index(value))
472            indx = self.meta.index(value)
473        else:
474            cb.setCurrentIndex(0)
475            value = self.meta[0]; indx = 0
476        return (value, indx)
477
478    def setMetaID(self, val, valIndx):
479        setattr(self, val, self.meta[getattr(self, valIndx)])
480#        if val=='BAnnotationVar':
481#            self.drawHeatMap()
482
483    def setMetaCombos(self):
484        self.meta = [m.name for m in self.data.domain.getmetas().values()]
485        self.BSpotVar, self.BSpotIndx = self.setMetaCombo(self.spotCombo, self.BSpotVar, \
486            enabled=self.BShowSpotIndex, default='RMI')
487        self.BAnnotationVar, self.BAnnotationIndx = self.setMetaCombo(self.annotationCombo, \
488            self.BAnnotationVar, enabled=self.BShowAnnotation, default='xannotation')
489       
490    def set_meta_combos(self):
491        self.spotCombo.clear()
492        self.annotationCombo.clear()
493       
494        self.meta = self.data.domain.getmetas().values()
495        names = [m.name for m in self.meta]
496       
497        self.spotCombo.addItems(names)
498        self.annotationCombo.addItems(names)
499        enabled = bool(self.meta)
500       
501        self.spotIndxCB.setEnabled(enabled)
502        self.geneAnnotationsCB.setEnabled(enabled)
503       
504        self.spotCombo.setEnabled(enabled and self.BShowSpotIndex)
505        self.annotationCombo.setEnabled(enabled and self.BShowAnnotation)
506       
507        self.BSpotIndx = 0
508        self.BSpotVar = self.meta[0] if self.meta else None
509        self.BAnnotationIndx = 0
510        self.BAnnotationVar = self.meta[0] if self.meta else None
511
512    def get_candidate_splits(self):
513        """ Return candidate labels on which we can split the data.
514        """
515        if self.data is not None:
516            groups = defaultdict(list)
517            for attr in self.data.domain.attributes:
518                for item in attr.attributes.items():
519                    groups[item].append(attr)
520               
521            by_keys = defaultdict(list)
522            for (key, value), attrs in groups.items():
523                by_keys[key].append(attrs)
524           
525            # Find the keys for which all values have the same number of attributes.
526            candidates = []
527            for key, groups in by_keys.items():
528                count = len(groups[0])
529                if all(len(attrs) == count for attrs in groups) and len(groups) > 1 and count > 1:
530                    candidates.append(key)
531                   
532            return candidates
533        else:
534            return []
535           
536    def set_split_labels(self):
537        """ Set the list view in Split tab.
538        """
539        self.splitLB.addItems(self.get_candidate_splits())
540       
541    def selected_split_label(self):
542        item = self.splitLB.currentItem()
543        return str(item.text()) if item else None
544       
545    def clear(self):
546        self.data = None
547        self.spotCombo.clear()
548        self.annotationCombo.clear()
549        self.splitLB.clear()
550        self.meta = []
551       
552        self.clear_scene()
553       
554    def clear_scene(self):
555        self.selection_manager.set_heatmap_widgets([[]])
556        self.heatmap_scene.clear()
557        self.heatmap_scene.widget = None
558        self.heatmap_widget_grid = [[]]
559        self.attr_annotation_widgets = []
560        self.attr_dendrogram_widgets = []
561        self.gene_annotation_widgets = []
562        self.gene_dendrogram_widgets = []
563       
564        self.selection_rects = []
565       
566       
567    def saveFig(self):
568        sizeDlg = OWChooseImageSizeDlg(self.scene, parent=self)
569        sizeDlg.exec_()
570
571    ##########################################################################
572    # handling of input/output signals
573       
574    def set_dataset(self, data=None, id=None):
575        self.closeContext("Selection")
576        self._ordering_cache.clear()
577        self.clear()
578        self.data = data
579        if data is not None:
580#            self.setMetaCombos()
581            self.set_meta_combos()
582            self.set_split_labels()
583           
584        self.unorderedData = None
585        self.groupClusters = None
586       
587    def handleNewSignals(self):
588        self.send('Examples', None)
589        if self.data:
590            self.update_heatmaps()
591        else:
592            self.clear()
593       
594        if self.data:
595            self.openContext("Selection", self.data)
596           
597    def construct_heatmaps(self, data, split_label=None):
598        if split_label is not None:
599            groups = split_domain(data.domain, split_label)
600        else:
601            groups = [("", data.domain)]
602           
603        group_domains = [dom for _, dom in groups]
604       
605        if self.SortGenes > 1:
606            self.progressBarInit()
607           
608            args_key = tuple(tuple(d) for d in group_domains), self.SortGenes == 3
609            cluster_ordering = self._ordering_cache.get(args_key, None)
610            if cluster_ordering is None:
611                attr_ordering, attr_cluster, data_ordering, data_clusters = \
612                        hierarchical_cluster_ordering(data, group_domains,
613                                      opt_order=self.SortGenes == 3,
614                                      progress_callback=self.progressBarSet)
615                # Cache the clusters
616                self._ordering_cache[args_key] = (attr_ordering, attr_cluster,
617                                                  data_ordering, data_clusters)
618            else:
619                 attr_ordering, attr_cluster, data_ordering, data_clusters = cluster_ordering
620                   
621            sorted_data = [data[i] for i in itertools.chain(*data_ordering)]
622            self.progressBarFinished()
623           
624        else:
625            attr_ordering = range(len(group_domains[0][1].attributes))
626            attr_cluster = None
627            data_ordering = []
628            data_clusters = [None]
629            sorted_data = data
630           
631        self.heatmapconstructor = []
632        self._group_data = []
633       
634        for name, group_domain in groups:
635            if attr_ordering != sorted(attr_ordering):
636                domain = orange.Domain([group_domain[i] for i in attr_ordering], group_domain.classVar)
637                domain.addmetas(group_domain.getmetas())
638                group_domain = domain
639               
640            group_data = orange.ExampleTable(group_domain, sorted_data)
641            self._group_data.append((group_data, group_domain)) # Crashes at accessing the heatmap.examples[0] without this
642            if self.SortGenes == 1:
643                hc = orangene.HeatmapConstructor(group_data)
644            else:
645                hc = orangene.HeatmapConstructor(group_data, None)
646           
647            self.heatmapconstructor.append(hc)
648           
649        self.attr_cluster = attr_cluster
650        self.data_clusters = data_clusters
651        self.sorted_data = sorted_data
652        self.group_domains = groups
653           
654    def create_heatmaps(self, constructors):
655        self.lowerBound = 1000
656        self.upperBound = -1000
657        squeeze = 1.0 / self.Merge
658        self.heatmaps = []
659        for hmc in constructors:
660            hm, lb, ub = hmc(squeeze)
661           
662            self.lowerBound = min(self.lowerBound, lb)
663            self.upperBound = max(self.upperBound, ub)
664               
665            self.heatmaps.append(hm)
666           
667        for cluster, heatmap in zip(self.data_clusters, self.heatmaps[0]):
668            if cluster is not None:
669                cluster._heatmap = heatmap
670           
671        self.sliderCutLow.setRange(self.lowerBound, 0, 0.1)
672        self.sliderCutHigh.setRange(1e-10, self.upperBound, 0.1)
673        self.CutLow = max(self.CutLow, self.lowerBound)
674        self.CutHigh = min(self.CutHigh, self.upperBound)
675        self.sliderCutLow.setValue(self.CutLow)
676        self.sliderCutHigh.setValue(self.CutHigh)
677           
678    def point_size_hint(self, height):
679        font = QFont(self.font())
680        font.setPointSize(height)
681        fix = 0
682        while QFontMetrics(font).lineSpacing() > height and height - fix > 1:
683            fix += 1
684            font.setPointSize(height - fix)
685        return height - fix
686   
687    def construct_heatmaps_scene(self, heatmaps, data, attr_cluster=None, data_clusters=None):
688        self.heatmap_scene.clear()
689        widget = GridWidget()
690        self.heatmap_scene.addItem(widget)
691        layout = QGraphicsGridLayout()
692        layout.setSpacing(self.SpaceX)
693        widget.setLayout(layout)
694       
695        classVar = data.domain.classVar
696        if classVar and isinstance(classVar, orange.EnumVariable):
697            classes = classVar.values
698        else:
699            classes = [None]
700       
701        if self.CutEnabled:
702            cut_low, cut_high = self.CutLow, self.CutHigh
703        else:
704            cut_low, cut_high = self.lowerBound, self.upperBound
705           
706        palette = self.getGammaCorrectedPalette() if self.Gamma !=0 else self.palette
707       
708        class_dendrograms = []
709        attr_dendrograms = []
710        heatmap_widgets = []
711        attr_annotation_widgets = []
712        gene_annotation_widgets = []
713        attr_annotation_widgets_top = []
714        attr_annotation_widgets_bottom = []
715       
716        # Dendrograms on the left side
717        if data_clusters and any(data_clusters):
718            for i, cluster in enumerate(data_clusters):
719                class_dendrogram = DendrogramWidget(cluster, parent=widget, orientation=Qt.Vertical)
720                class_dendrogram.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
721                left, top, right, bottom = class_dendrogram.layout().getContentsMargins()
722                class_dendrogram.layout().setContentsMargins(left, self.CellHeight / 2.0 / self.Merge, 0.0, self.CellHeight / 2.0 / self.Merge)
723                class_dendrogram.setMinimumWidth(100)
724                class_dendrogram.setMaximumWidth(100)
725               
726                layout.addItem(class_dendrogram, i*2 + 5, 0)
727                class_dendrograms.append(class_dendrogram)
728           
729        # Class labels   
730        for i, class_ in enumerate(classes):
731            if class_ is not None:
732                item = GtI(class_, widget)
733                item = GraphicsSimpleTextLayoutItem(item, parent=widget)
734                item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
735                layout.addItem(item, i*2 + 4, 2)
736                layout.setRowSpacing(i*2 + 4, 2)
737                layout.setAlignment(item, Qt.AlignLeft | Qt.AlignVCenter)
738               
739        font = QFont()
740        font.setPointSize(self.point_size_hint(self.CellHeight))
741       
742        class_row_labels = [map(str, hm.exampleIndices) for hm in heatmaps[0]]
743        group_column_labels = [[a.name for a in hm[0].examples.domain.attributes] for hm in heatmaps]
744       
745        # Gene annotations on the right side
746        for i, labels in enumerate(class_row_labels):
747            list = GraphicsSimpleTextList(labels, parent=widget, orientation=Qt.Vertical)
748            list.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
749            list.setFont(font)
750            list.setContentsMargins(0.0, 0.0, 0.0, 0.0)
751            list.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
752           
753            layout.addItem(list, i*2 + 5, len(self.heatmaps) + 2)
754            layout.setAlignment(list, Qt.AlignLeft)
755            gene_annotation_widgets.append(list)
756           
757        font = QFont()
758        font.setPointSizeF(self.point_size_hint(self.CellWidth))
759       
760        if self.ShowAverageStripe:
761            stripe_offset = c_averageStripeWidth + 2
762        else:
763            stripe_offset = 0
764           
765        for column, (hm, labels, group) in enumerate(zip(heatmaps, group_column_labels, self.group_domains)):
766            column_heatmap_widgets = []
767           
768            # Top group label
769            if len(heatmaps) > 1:
770                item = GtI(group[0], widget)
771                item = GraphicsSimpleTextLayoutItem(item, parent=widget)
772                layout.addItem(item, 1, column + 2)
773                layout.setRowSpacing(1, 2)
774                layout.setRowMaximumHeight(1, item.geometry().height())
775                layout.setAlignment(item, Qt.AlignLeft | Qt.AlignVCenter)
776            else:
777                layout.setRowMaximumHeight(1, 0)
778                layout.setRowSpacing(1, 0)
779               
780            # Top dendrogram
781            if attr_cluster is not None:
782                attr_dendrogram = DendrogramWidget(attr_cluster, parent=widget, orientation=Qt.Horizontal)
783                attr_dendrogram.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
784                attr_dendrogram.translate(0.0, attr_dendrogram.size().height())
785                attr_dendrogram.scale(1.0, -1.0)
786               
787                left, top, right, bottom = attr_dendrogram.layout().getContentsMargins()
788                attr_dendrogram.layout().setContentsMargins(stripe_offset + self.CellWidth / 2.0, 0.0, self.CellWidth / 2.0, bottom)
789                attr_dendrogram.setMinimumHeight(100)
790                attr_dendrogram.setMaximumHeight(100)
791               
792                layout.addItem(attr_dendrogram, 2, column + 2)
793                layout.setRowMaximumHeight(2, 100)
794                attr_dendrograms.append(attr_dendrogram)
795           
796           
797            # Heatmap widget for each class
798            for i, (class_, chm) in enumerate(zip(classes, hm)): 
799                hm_widget = GraphicsHeatmapWidget(heatmap=chm, parent=widget)
800                hm_widget.set_cell_size(int(self.CellWidth), int(self.CellHeight))
801                hm_widget.set_cuts(cut_low, cut_high)
802                hm_widget.set_color_table(palette)
803                hm_widget.set_show_averages(self.ShowAverageStripe)
804                hm_widget.cell_tool_tip = lambda row, col, hm=hm_widget: self.cell_tool_tip(hm, row, col)
805                layout.addItem(hm_widget, i*2 + 5, column + 2)
806                column_heatmap_widgets.append(hm_widget)
807            heatmap_widgets.append(column_heatmap_widgets)
808           
809            # Top attr annotations
810            list = GraphicsSimpleTextList(labels, parent=widget, orientation=Qt.Horizontal)
811            list.setAlignment(Qt.AlignBottom | Qt.AlignLeft)
812           
813            list.setFont(font)
814            list.layout().setContentsMargins(stripe_offset, 0, 0, 0)
815            list.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
816           
817            layout.addItem(list, 3, column + 2, Qt.AlignBottom | Qt.AlignLeft)
818            attr_annotation_widgets.append(list)
819            attr_annotation_widgets_top.append(list)
820           
821            # Bottom attr annotations
822            list = GraphicsSimpleTextList(labels, parent=widget, orientation=Qt.Horizontal)
823            list.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
824           
825            list.setFont(font)
826            list.layout().setContentsMargins(stripe_offset, 0, 0, 0)
827            list.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
828           
829            layout.addItem(list, len(hm)*2 + 5, column + 2)
830            attr_annotation_widgets.append(list)
831            attr_annotation_widgets_bottom.append(list)
832           
833            # Legend
834            if column == 0:
835                item = GraphicsLegendWidget(self.heatmapconstructor[0], self.lowerBound, self.upperBound, parent=widget)
836                item.set_color_table(palette)
837                item.setVisible(self.ShowLegend)
838                layout.addItem(item, 0, 2, 1, len(self.heatmaps) + 1)
839                layout.setRowSpacing(0, 2)
840#                layout.setRowMaximumHeight(0, item.geometry().height())
841           
842        self.heatmap_scene.addItem(widget)
843        self.heatmap_scene.widget = widget
844        self.heatmap_widget_grid = heatmap_widgets
845        self.gene_annotation_widgets = gene_annotation_widgets
846        self.attr_annotation_widgets = attr_annotation_widgets
847        self.attr_annotation_widgets_top = attr_annotation_widgets_top
848        self.attr_annotation_widgets_bottom = attr_annotation_widgets_bottom
849        self.attr_dendrogram_widgets = attr_dendrograms
850       
851        self.update_annotations()
852        self.update_column_annotations()
853       
854        self.fix_grid_layout()
855       
856        self.selection_manager.set_heatmap_widgets(heatmap_widgets)
857       
858    def fix_grid_layout(self):
859        """ Fix grid layout when cell size changes or average
860        stripes are shown/hiddens.
861        """
862        if self.heatmap_scene.widget:
863            layout = self.heatmap_scene.widget.layout()
864            layout.invalidate()
865            layout.activate()
866           
867            for i, hw in enumerate(self.heatmap_widget_grid[0]):
868                max_h = hw.size().height()
869                layout.setRowMaximumHeight(i*2 + 5, max_h)
870               
871            for i, hw in enumerate(self.heatmap_widget_grid):
872                max_w = hw[0].size().width()
873                layout.setColumnMaximumWidth(i + 2, max_w)
874                if self.attr_dendrogram_widgets:
875                    dendrogram = self.attr_dendrogram_widgets[i]
876#                    dendrogram.resize(max_w, -1)
877                    dendrogram.setMaximumWidth(max_w)
878                self.attr_annotation_widgets_top[i].setMaximumWidth(max_w)
879                self.attr_annotation_widgets_bottom[i].setMaximumWidth(max_w)
880               
881#            for i, (hw, dend) in enumerate(zip(self.heatmap_widget_grid, self.attr_dendrogram_widgets)):
882#                max_w = hw[0].size().width()
883           
884#            self.update_widget_margins()   
885            self.heatmap_scene.widget.resize(self.heatmap_scene.widget.sizeHint(Qt.PreferredSize))
886           
887            self.on_selection_changed()
888            self.update_scene_rect()
889       
890    def update_scene_rect(self):
891        rect = QRectF()
892        for item in self.heatmap_scene.items():
893            rect |= item.sceneBoundingRect()
894        self.heatmap_scene.setSceneRect(rect)
895           
896    def heatmap_widgets(self):
897        """ Iterate over heatmap widgets.
898        """
899        for item in self.heatmap_scene.items():
900            if isinstance(item, GraphicsHeatmapWidget):
901                yield item
902               
903    def label_widgets(self):
904        """ Iterate over GraphicsSimpleTextList widgets.
905        """
906        for item in self.heatmap_scene.items():
907            if isinstance(item, GraphicsSimpleTextList):
908                yield item
909               
910    def dendrogram_widgets(self):
911        """ Iterate over dendrogram widgets
912        """
913        for item in self.heatmap_scene.items():
914            if isinstance(item, DendrogramWidget):
915                yield item
916               
917    def legend_widgets(self):
918        for item in self.heatmap_scene.items():
919            if isinstance(item, GraphicsLegendWidget):
920                yield item
921               
922    def update_cell_size(self):
923        """ Update cell sizes (by user request - height/width sliders)
924        """ 
925        for heatmap in self.heatmap_widgets():
926            heatmap.set_cell_size(self.CellWidth, self.CellHeight)
927           
928        hor_font = QFont(self.font())
929        hor_font.setPointSize(self.point_size_hint(self.CellWidth))
930        vert_font = QFont(self.font())
931        vert_font.setPointSize(self.point_size_hint(self.CellHeight))
932       
933        # Also update the annotation items font.
934        for labels in self.label_widgets():
935            if labels.orientation == Qt.Vertical:
936                labels.setFont(vert_font)
937            else:
938                labels.setFont(hor_font)
939
940        self.update_widget_margins()
941        ## To hide the annotations if cell sizes to small.
942        self.update_annotations()
943        self.update_column_annotations()
944           
945        self.fix_grid_layout()
946       
947    def update_widget_margins(self):
948        """ Update dendrogram and text list widgets margins to incude the
949        space for average stripe.
950        """
951        if self.ShowAverageStripe:
952            stripe_offset = c_averageStripeWidth + 2
953        else:
954            stripe_offset = 0
955        right = self.CellWidth / 2.0
956       
957        top = self.CellHeight / 2.0 / self.Merge
958        bottom = self.CellHeight / 2.0 / self.Merge
959       
960        for dendrogram in self.dendrogram_widgets():
961            layout = dendrogram.layout() 
962            if dendrogram.orientation == Qt.Horizontal:
963#                index = self.attr_dendrogram_widgets.index(dendrogram)
964#                heatmap = self.heatmap_widget_grid[index][0]
965#                h_w = heatmap.size().width()
966#                d_w = dendrogram.size().width()
967#                right_ = d_w - stripe_offset - self.CellWidth - h_w
968                _, top_h, _, bottom_h = layout.getContentsMargins()
969                layout.setContentsMargins(stripe_offset + self.CellWidth / 2.0, top_h, right, bottom_h)
970            else:
971                left_v, _, right_v, _ = layout.getContentsMargins()
972                layout.setContentsMargins(left_v, top, right_v, bottom)
973               
974        for widget in self.label_widgets():
975            layout = widget.layout()
976            if widget.orientation == Qt.Horizontal:
977                left_h, top, right, bottom = layout.getContentsMargins()
978                layout.setContentsMargins(stripe_offset, top, right, bottom)
979       
980    def update_averages_stripe(self):
981        """ Update the visibility of the averages stripe.
982        """
983        if self.data:
984            for widget in self.heatmap_widgets():
985                widget.set_show_averages(self.ShowAverageStripe)
986               
987            self.update_widget_margins()
988            self.fix_grid_layout()
989           
990    def update_grid_spacing(self):
991        """ Update layout spacing.
992        """
993        if self.scene.widget:
994            layout = self.scene.widget.layout()
995            layout.setSpacing(self.SpaceX)
996            self.fix_grid_layout()
997       
998    def update_color_schema(self):
999        palette = self.getGammaCorrectedPalette() if self.Gamma !=0 else self.palette
1000        for heatmap in self.heatmap_widgets():
1001            heatmap.set_color_table(palette)
1002           
1003        for legend in self.legend_widgets():
1004            legend.set_color_table(palette)
1005           
1006    def update_thresholds(self):
1007        self.sliderCutLow.box.setDisabled(not self.CutEnabled)
1008        self.sliderCutHigh.box.setDisabled(not self.CutEnabled)
1009           
1010        if self.data:
1011            if self.CutEnabled:
1012                low, high = self.CutLow, self.CutHigh
1013            else:
1014                low, high = self.lowerBound, self.upperBound
1015            for heatmap in self.heatmap_widgets():
1016                heatmap.set_cuts(low, high)
1017   
1018    def update_sorting(self):
1019        if self.data:
1020            self.update_heatmaps()
1021           
1022    def update_legend(self):
1023        for item in self.heatmap_scene.items():
1024            if isinstance(item, GraphicsLegendWidget):
1025                item.setVisible(self.ShowLegend)
1026       
1027    def update_annotations(self):
1028        if self.data:
1029            if self.meta:
1030                attr = self.meta[self.BAnnotationIndx]
1031            else:
1032                attr = None
1033           
1034            show = self.ShowGeneAnnotations and attr and self.Merge == 1
1035            show = show and self.CellHeight > 3
1036            for list_widget, hm in zip(self.gene_annotation_widgets, self.heatmap_widget_grid[0]):
1037                list_widget.setVisible(bool(show))
1038                if show:
1039                    hm = hm.heatmap
1040                    examples = hm.examples
1041                    indices = hm.exampleIndices[:-1]
1042                    labels = [str(examples[i][attr]) for i in indices]
1043                    list_widget.set_labels(labels)
1044
1045    def update_column_annotations(self):
1046        if self.data:
1047            show = self.CellWidth > 3
1048            show_top = self.ShowColumnLabels and self.ColumnLabelPosition == 0 and show
1049            show_bottom = self.ShowColumnLabels and self.ColumnLabelPosition == 1 and show
1050           
1051            for list_widget in self.attr_annotation_widgets_top:
1052                list_widget.setVisible(show_top)
1053               
1054            layout = self.heatmap_scene.widget.layout()
1055            layout.setRowMaximumHeight(3,  -1 if show_top else 0)
1056            layout.setRowSpacing(3, -1 if show_top else 0)
1057               
1058            for list_widget in self.attr_annotation_widgets_bottom:
1059                list_widget.setVisible(show_bottom)
1060               
1061            layout.setRowMaximumHeight(len(self.heatmap_widget_grid[0]) + 4, -1 if show_top else 0)
1062               
1063            self.fix_grid_layout()
1064           
1065    def update_heatmaps(self):
1066        if self.data:
1067            self.construct_heatmaps(self.data, self.selected_split_label())
1068            self.create_heatmaps(self.heatmapconstructor)
1069            self.clear_scene()
1070            self.construct_heatmaps_scene(self.heatmaps, self.data,
1071                                          attr_cluster=self.attr_cluster,
1072                                          data_clusters=self.data_clusters)
1073        else:
1074            self.clear()
1075       
1076    def update_heatmaps_stage2(self):
1077        if self.data:
1078            self.create_heatmaps(self.heatmapconstructor)
1079            self.clear_scene()
1080            self.construct_heatmaps_scene(self.heatmaps, self.data,
1081                                          attr_cluster=self.attr_cluster,
1082                                          data_clusters=self.data_clusters)
1083       
1084    def cell_tool_tip(self, heatmap_widget, row, column):
1085        if not self.GShowToolTip:
1086            return ""
1087        hm = heatmap_widget.heatmap
1088        examples = hm.examples[hm.exampleIndices[row] : hm.exampleIndices[row+1]]
1089        domain = hm.examples.domain
1090        if hm.getCellIntensity(row, column) != None:
1091            head = "%6.4f" % hm.getCellIntensity(row, column)
1092        else:
1093            head = "Missing Data"
1094        if self.BShowColumnID:
1095            head += "\n" + domain.attributes[column].name
1096        # tool tip, construct body
1097        body = ""
1098        if (self.BShowSpotIndex and self.BSpotVar) or \
1099                (self.BShowAnnotation and self.BAnnotationVar) or \
1100                 self.BShowGeneExpression:
1101            for (i, e) in enumerate(examples):
1102                if i > 5:
1103                    body += "\n... (%d more)" % (len(examples) - 5)
1104                    break
1105                else:
1106                    s = []
1107                    if self.BShowSpotIndex and self.BSpotVar:
1108                        s.append(str(e[self.BSpotVar]))
1109                    if self.BShowGeneExpression:
1110                        s.append(str(e[column]))
1111                    if self.BShowAnnotation and self.BAnnotationVar:
1112                        s.append(str(e[self.BAnnotationVar]))
1113           
1114                body += "\n"
1115                body += " | ".join(s)
1116        return head + body
1117   
1118    def on_merge_changed(self):
1119        self.oldMerge = self.savedMerge
1120        if self.MaintainArrayHeight and self.oldMerge != self.Merge:
1121            k = float(self.Merge) / self.oldMerge
1122            l = max(1, min(int(self.CellHeight * k), self.maxVSize))
1123            if l != self.CellHeight:
1124                self.CellHeight = l
1125                self.sliderVSize.setValue(self.CellHeight)
1126
1127        self.update_heatmaps_stage2()
1128        self.savedMerge = self.Merge
1129           
1130    def on_selection_changed(self):
1131        for item in self.selection_rects:
1132            item.hide()
1133            item.setParentItem(None)
1134            item.update()
1135            self.heatmap_scene.removeItem(item)
1136        self.selection_rects = []
1137        self.selection_manager.update_selection_rects()
1138        rects = self.selection_manager.selection_rects
1139        for rect in rects:
1140            item = QGraphicsRectItem(rect, None, self.heatmap_scene)
1141            item.setPen(QPen(Qt.black, 2))
1142            self.selection_rects.append(item)
1143           
1144    def on_selection_finished(self):
1145        self.selected_rows = self.selection_manager.selections
1146        self.commit_if()
1147       
1148    def commit_if(self):
1149        if self.auto_commit:
1150            self.commit()
1151        else:
1152            self.selection_changed_flag = True
1153       
1154    def commit(self):
1155        data = None
1156        if self.sorted_data:
1157            if self.selected_rows:
1158                examples = [self.sorted_data[i] for i in self.selected_rows]
1159                data = orange.ExampleTable(examples)
1160            else:
1161                data = None
1162       
1163        self.send("Examples", data)
1164        self.selection_changed_flag
1165           
1166    ## handle saved selections
1167    def settingsFromWidgetCallbackSelection(self, handler, context):
1168        context.selection = self.selection_manager.selections
1169
1170    def settingsToWidgetCallbackSelection(self, handler, context):
1171        selection = getattr(context, "selection", None)
1172        if selection:
1173            try:
1174                self.selection_manager.select_rows(selection)
1175            except Exception, ex:
1176                pass
1177#                self.warning(3, "Could not restore selection")
1178               
1179    def split_changed(self):
1180        if self.data:
1181            self.clear_scene()
1182            self.construct_heatmaps(self.data, self.selected_split_label())
1183            self.create_heatmaps(self.heatmapconstructor)
1184            self.construct_heatmaps_scene(self.heatmaps, self.data,
1185                                          attr_cluster=self.attr_cluster,
1186                                          data_clusters=self.data_clusters)
1187       
1188
1189class GraphicsPixmapLayoutItem(QGraphicsLayoutItem):
1190    """ A layout item wraping a QGraphicsPixmapItem
1191    """
1192    def __init__(self, pixmap_item, parent=None):
1193        QGraphicsLayoutItem.__init__(self, parent)
1194        self.pixmap_item = pixmap_item
1195        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1196       
1197    def setGeometry(self, rect):
1198        QGraphicsLayoutItem.setGeometry(self, rect)
1199        self.pixmap_item.setPos(rect.topLeft())
1200       
1201    def sizeHint(self, which, constraint=QSizeF()):
1202        return QSizeF(self.pixmap_item.pixmap().size())
1203   
1204    def setPixmap(self, pixmap):
1205        self.pixmap_item.setPixmap(pixmap)
1206        self.updateGeometry()
1207       
1208       
1209class GraphicsHeatmapWidget(QGraphicsWidget):
1210    def __init__(self, heatmap=None, parent=None, scene=None):
1211        QGraphicsWidget.__init__(self, parent)
1212        self.setAcceptHoverEvents(True)
1213        layout = QGraphicsLinearLayout(Qt.Horizontal)
1214        layout.setContentsMargins(0, 0, 0, 0)
1215        item = QGraphicsPixmapItem(self)
1216        item.setShapeMode(QGraphicsPixmapItem.BoundingRectShape)
1217        self.heatmap_item = GraphicsPixmapLayoutItem(item, self)
1218       
1219        item = QGraphicsPixmapItem(self)
1220        item.setShapeMode(QGraphicsPixmapItem.BoundingRectShape)
1221        self.averages_item = GraphicsPixmapLayoutItem(item, self)
1222       
1223        layout.addItem(self.averages_item)
1224        layout.addItem(self.heatmap_item)
1225        layout.setItemSpacing(0, 2)
1226       
1227        self.setLayout(layout)
1228       
1229        self.heatmap = None
1230        self.show_averages = True
1231        self._pixmap_args = None
1232        self.color_table = None
1233        self.selection_manager = None
1234        self.set_cell_size(4, 4)
1235        self.set_cuts(0, 255)
1236        self.set_heatmap(heatmap)
1237       
1238        if scene is not None:
1239            scene.addItem(self)
1240           
1241    def clear(self):
1242        """ Clear the current heatmap.
1243        """
1244        self.heatmap = None
1245        self._pixmap_args = None
1246        self.heatmap_item.setPixmap(QPixmap())
1247        self.averages_item.setPixmap(QPixmap())
1248        self.show_averages = True
1249        self.layout().invalidate()
1250           
1251    def set_heatmap(self, heatmap):
1252        """ Set the heatmap for display.
1253        """
1254        self.clear()
1255        self.heatmap = heatmap
1256        self.update()
1257       
1258    def set_cell_size(self, width, height):
1259        self.cell_width = width
1260        self.cell_height = height
1261        self.update()
1262       
1263    def set_cuts(self, low, high):
1264        self.cut_low = low
1265        self.cut_high = high
1266        self.update()
1267       
1268    def set_show_averages(self, show):
1269        self.show_averages = show
1270        self._pixmap_args = None
1271        self.update()
1272
1273    def set_color_table(self, color_table):
1274        if qVersion() <= "4.5":
1275            self.color_table = signedPalette(color_table)
1276        else:
1277            self.color_table = color_table
1278       
1279        self._pixmap_args = None
1280        self.update()
1281           
1282    def _update_pixmap(self):
1283        """ Update the pixmap if its construction arguments changed.
1284        """
1285        if self.heatmap:
1286            args = (int(self.cell_width), int(self.cell_height),
1287                    self.cut_low, self.cut_high, 1.0)
1288           
1289            if args != self._pixmap_args:
1290                bitmap, width, height = self.heatmap.getBitmap(*args)
1291                image = QImage(bitmap, width, height, QImage.Format_Indexed8)
1292                color_table = self.color_table
1293                if color_table:
1294                    image.setColorTable(color_table)
1295                self.pixmap = QPixmap.fromImage(image)
1296               
1297                bitmap, width, height = self.heatmap.getAverages(*((c_averageStripeWidth,) + args[1:]))
1298                image = QImage(bitmap, width, height, QImage.Format_Indexed8)
1299                if self.color_table:
1300                    image.setColorTable(color_table)
1301                self.averages_pixmap = QPixmap.fromImage(image)
1302               
1303                self._pixmap_args = args
1304               
1305                self.layout().invalidate()
1306        else:
1307            self.averages_pixmap = None
1308            self.pixmap = None
1309           
1310        self.heatmap_item.setPixmap(self.pixmap or QPixmap())
1311        if self.show_averages and self.averages_pixmap:
1312            self.averages_item.setPixmap(self.averages_pixmap)
1313        else:
1314            self.averages_item.setPixmap(QPixmap())
1315           
1316    def update(self):
1317        self._update_pixmap()
1318        QGraphicsWidget.update(self)
1319       
1320    def set_selection_manager(self, manager):
1321        self.selection_manager = manager
1322       
1323    def cell_at(self, pos):
1324        """ Return the cell row, column from a point `pos` in local
1325        coordinates.
1326       
1327        """
1328        pos = self.mapToItem(self.heatmap_item.pixmap_item, pos)
1329        x, y = pos.x(), pos.y()
1330        def clamp(i, m):
1331            return int(min(max(i, 0), m))
1332        return (clamp(math.floor(y / self.cell_height), self.heatmap.height),
1333                clamp(math.floor(x / self.cell_width), self.heatmap.width))
1334   
1335    def cell_rect(self, row, column):
1336        """ Return a QRectF in local coordinates containing the cell
1337        at `row` and `column`.
1338       
1339        """
1340        top = QPointF(column * self.cell_width, row * self.cell_height)
1341        top = self.mapFromItem(self.heatmap_item.pixmap_item, top)
1342        size = QSizeF(self.cell_width, self.cell_height)
1343        return QRectF(top, size)
1344
1345    def row_rect(self, row):
1346        """ Return a QRectF in local coordinates containing the entire row.
1347        """
1348        rect = self.cell_rect(row, 0).united(self.cell_rect(row, self.heatmap.width - 1))
1349        rect.setLeft(0) # To include the average stripe if show.
1350        return rect
1351   
1352    def cell_tool_tip(self, row, column):
1353        hm = self.heatmap
1354        start = int(hm.exampleIndices[row])
1355        end = int(hm.exampleIndices[row + 1])
1356        examples = [hm.examples[start]]
1357        attr = hm.examples.domain[column]
1358        val = "%i, %i: %f" % (row, column, float(examples[0][attr]))
1359        return val
1360   
1361    def hoverEnterEvent(self, event):
1362        row, col = self.cell_at(event.pos())
1363   
1364    def hoverMoveEvent(self, event):
1365        pos = event.pos()
1366        row, column = self.cell_at(pos)
1367        tooltip = self.cell_tool_tip(row, column)
1368        QToolTip.showText(event.screenPos(), tooltip)
1369        return QGraphicsWidget.hoverMoveEvent(self, event)
1370   
1371    def hoverLeaveEvent(self, event):
1372        row, col = self.cell_at(event.pos())
1373   
1374    if DEBUG:
1375        def paint(self, painter, option, widget=0):
1376            rect =  self.geometry()
1377            rect.translate(-self.pos())
1378            painter.drawRect(rect.adjusted(-1, -1, 1, 1))
1379   
1380   
1381class GridWidget(QGraphicsWidget):
1382    def __init__(self, parent=None):
1383        QGraphicsWidget.__init__(self, parent)
1384       
1385    if DEBUG:
1386        def paint(self, painter, option, widget=0):
1387            rect =  self.geometry()
1388            rect.translate(-self.pos())
1389            painter.drawRect(rect)
1390           
1391           
1392class HeatmapScene(QGraphicsScene):
1393    """ A Graphics Scene with heatmap widgets.
1394    """
1395    def __init__(self, parent=None):
1396        QGraphicsScene.__init__(self, parent)
1397        self.selection_manager = HeatmapSelectionManager()
1398       
1399    def set_selection_manager(self, manager):
1400        self.selection_manager = manager
1401       
1402    def _items(self, pos=None, cls=object):
1403        if pos is not None:
1404            items = self.items(QRectF(pos, QSizeF(3, 3)).translated(-1.5, -1.5))
1405        else:
1406            items = self.items()
1407           
1408        for item in items:
1409            if isinstance(item, cls):
1410                yield item
1411           
1412    def heatmap_at_pos(self, pos):
1413        items  = list(self._items(pos, GraphicsHeatmapWidget))
1414        if items:
1415            return items[0]
1416        else:
1417            return None
1418       
1419    def dendrogram_at_pos(self, pos):
1420        items  = list(self._items(pos, DendrogramItem))
1421        if items:
1422            return items[0]
1423        else:
1424            return None
1425       
1426    def heatmap_widgets(self):
1427        return self._items(None, GraphicsHeatmapWidget)
1428       
1429    def select_from_dendrogram(self, dendrogram, clear=True):
1430        """ Select all heatmap rows which belong to the dendrogram.
1431        """
1432        dendrogram_widget = dendrogram.parentWidget()
1433        anchors = list(dendrogram_widget.leaf_anchors())
1434        cluster = dendrogram.cluster
1435        start, end = anchors[cluster.first], anchors[cluster.last - 1]
1436        start, end = dendrogram_widget.mapToScene(start), dendrogram_widget.mapToScene(end)
1437        # Find a heatmap widget containing start and end y coordinates.
1438       
1439        heatmap = None
1440        for hm in self.heatmap_widgets():
1441            b_rect = hm.sceneBoundingRect()
1442            if b_rect.contains(QPointF(b_rect.center().x(), start.y())):
1443                heatmap = hm
1444                break
1445           
1446        if dendrogram:
1447            b_rect = hm.boundingRect()
1448            start, end = hm.mapFromScene(start), hm.mapFromScene(end)
1449            start, _ = hm.cell_at(QPointF(b_rect.center().x(), start.y()))
1450            end, _ = hm.cell_at(QPointF(b_rect.center().x(), end.y()))
1451            self.selection_manager.selection_add(start, end, hm, clear=clear)
1452        return
1453       
1454    def mousePressEvent(self, event):
1455        pos = event.scenePos()
1456        heatmap = self.heatmap_at_pos(pos)
1457        if heatmap and event.button() & Qt.LeftButton:
1458            row, _ = heatmap.cell_at(heatmap.mapFromScene(pos))
1459            self.selection_manager.selection_start(heatmap, event)
1460           
1461        dendrogram = self.dendrogram_at_pos(pos)
1462        if dendrogram and event.button() & Qt.LeftButton:
1463            if dendrogram.orientation == Qt.Vertical:
1464                self.select_from_dendrogram(dendrogram, clear=not event.modifiers() & Qt.ControlModifier)
1465            return 
1466       
1467        return QGraphicsScene.mousePressEvent(self, event)
1468   
1469    def mouseMoveEvent(self, event):
1470        pos = event.scenePos()
1471        heatmap = self.heatmap_at_pos(pos)
1472        if heatmap and event.buttons() & Qt.LeftButton:
1473            row, _ = heatmap.cell_at(heatmap.mapFromScene(pos))
1474            self.selection_manager.selection_update(heatmap, event)
1475           
1476        dendrogram = self.dendrogram_at_pos(pos)
1477        if dendrogram and dendrogram.orientation == Qt.Horizontal: # Filter mouse move events
1478            return
1479           
1480        return QGraphicsScene.mouseMoveEvent(self, event)
1481   
1482    def mouseReleaseEvent(self, event):
1483        pos = event.scenePos()
1484        heatmap = self.heatmap_at_pos(pos)
1485        if heatmap:
1486            row, _ = heatmap.cell_at(heatmap.mapFromScene(pos))
1487            self.selection_manager.selection_finish(heatmap, event)
1488       
1489        dendrogram = self.dendrogram_at_pos(pos)
1490        if dendrogram and dendrogram.orientation == Qt.Horizontal: # Filter mouse events
1491            return
1492       
1493        return QGraphicsScene.mouseReleaseEvent(self, event)
1494   
1495    def mouseDoubleClickEvent(self, event):
1496        pos = event.scenePos()
1497        dendrogram = self.dendrogram_at_pos(pos)
1498        if dendrogram: # Filter mouse events
1499            return
1500        return QGraphicsScene.mouseDoubleClickEvent(self, event)
1501       
1502       
1503class GtI(QGraphicsSimpleTextItem):
1504    if DEBUG:
1505        def paint(self, painter, option, widget =0):
1506            QGraphicsSimpleTextItem.paint(self, painter, option, widget)
1507            painter.drawRect(self.boundingRect())
1508           
1509   
1510class GraphicsSimpleTextLayoutItem(QGraphicsLayoutItem):
1511    """ A Graphics layout item wrapping a QGraphicsSimpleTextItem alowing it
1512    to be managed by a layout.
1513    """
1514    def __init__(self, text_item, orientation=Qt.Horizontal, parent=None):
1515        QGraphicsLayoutItem.__init__(self, parent)
1516        self.orientation = orientation
1517        self.text_item = text_item
1518        if orientation == Qt.Vertical:
1519            self.text_item.rotate(-90)
1520            self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
1521        else:
1522            self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
1523       
1524    def setGeometry(self, rect):
1525        QGraphicsLayoutItem.setGeometry(self, rect)
1526        if self.orientation == Qt.Horizontal:
1527            self.text_item.setPos(rect.topLeft())
1528        else:
1529            self.text_item.setPos(rect.bottomLeft())
1530       
1531    def sizeHint(self, which, constraint=QSizeF()):
1532        if which in [Qt.PreferredSize]:
1533            size = self.text_item.boundingRect().size()
1534            if self.orientation == Qt.Horizontal:
1535                return size
1536            else:
1537                return QSizeF(size.height(), size.width())
1538        else:
1539            return QSizeF()
1540   
1541    def setFont(self, font):
1542        self.text_item.setFont(font)
1543        self.updateGeometry()
1544       
1545    def setText(self, text):
1546        self.text_item.setText(text)
1547        self.updateGeometry()
1548       
1549       
1550class GraphicsSimpleTextList(QGraphicsWidget):
1551    """ A simple text list widget.
1552    """
1553    def __init__(self, labels=[], orientation=Qt.Vertical, parent=None, scene=None):
1554        QGraphicsWidget.__init__(self, parent)
1555        layout = QGraphicsLinearLayout(orientation)
1556        layout.setContentsMargins(0, 0, 0, 0)
1557        layout.setSpacing(0)
1558        self.setLayout(layout)
1559        self.orientation = orientation
1560        self.alignment = Qt.AlignCenter
1561        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
1562        self.set_labels(labels)
1563       
1564        if scene is not None:
1565            scene.addItem(self)
1566       
1567    def clear(self):
1568        """ Remove all text items.
1569        """
1570        layout = self.layout()
1571        for i in reversed(range(layout.count())):
1572            item = layout.itemAt(i)
1573            item.text_item.setParentItem(None)
1574            if self.scene():
1575                self.scene().removeItem(item.text_item)
1576            layout.removeAt(i)
1577       
1578        self.label_items = []
1579        self.updateGeometry()
1580       
1581    def set_labels(self, labels):
1582        """ Set the text labels to show in the widget.
1583        """
1584        self.clear()
1585        orientation = Qt.Horizontal if self.orientation == Qt.Vertical else Qt.Vertical
1586        for text in labels:
1587#            item = QGraphicsSimpleTextItem(text, self)
1588            item = GtI(text, self)
1589            item.setFont(self.font())
1590            item.setToolTip(text)
1591            item = GraphicsSimpleTextLayoutItem(item, orientation, parent=self)
1592            self.layout().addItem(item)
1593            self.layout().setAlignment(item, self.alignment)
1594            self.label_items.append(item)
1595           
1596        self.layout().activate()
1597        self.updateGeometry()
1598   
1599    def setAlignment(self, alignment):
1600        """ Set alignment of text items in the widget
1601        """
1602        self.alignment = alignment
1603        layout = self.layout()
1604        for i in range(layout.count()):
1605            layout.setAlignment(layout.itemAt(i), alignment)
1606           
1607    def setVisible(self, bool):
1608        QGraphicsWidget.setVisible(self, bool)
1609        self.updateGeometry()
1610           
1611    def setFont(self, font):
1612        """ Set the font for the text.
1613        """
1614        QGraphicsWidget.setFont(self, font)
1615        for item in self.label_items:
1616            item.setFont(font)
1617        self.layout().invalidate()
1618        self.updateGeometry()
1619       
1620    def sizeHint(self, which, constraint=QRectF()):
1621        if not self.isVisible():
1622            return QSizeF(0, 0)
1623        else:
1624            return QGraphicsWidget.sizeHint(self, which, constraint)
1625           
1626    if DEBUG:
1627        def paint(self, painter, options, widget=0):
1628            rect =  self.geometry()
1629            rect.translate(-self.pos())
1630            painter.drawRect(rect)
1631       
1632class GraphicsLegendWidget(QGraphicsWidget):
1633    def __init__(self, heatmap_constructor, low, high, parent=None, scene=None):
1634        QGraphicsWidget.__init__(self, parent)
1635        layout = QGraphicsLinearLayout(Qt.Vertical)
1636        self.setLayout(layout)
1637        layout.setContentsMargins(0, 0, 0, 0)
1638        layout.setSpacing(1)
1639       
1640        layout_labels = QGraphicsLinearLayout(Qt.Horizontal)
1641        layout.addItem(layout_labels)
1642        layout_labels.setContentsMargins(0, 0, 0, 0)
1643        label_lo = GtI("%.2f" % low, self)
1644        label_hi = GtI("%.2f" % high, self)
1645        self.item_low = GraphicsSimpleTextLayoutItem(label_lo, parent=self)
1646        self.item_high = GraphicsSimpleTextLayoutItem(label_hi, parent=self)
1647       
1648        layout_labels.addItem(self.item_low)
1649        layout.addStretch()
1650        layout_labels.addItem(self.item_high)
1651       
1652        self._pixmap = QPixmap(c_legendHeight, c_legendHeight)
1653        self.pixmap_item = QGraphicsPixmapItem(self._pixmap, self)
1654        self.pixmap_item = GraphicsPixmapLayoutItem(self.pixmap_item, parent=self)
1655        layout.addItem(self.pixmap_item)
1656       
1657        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
1658        self.set_legend(heatmap_constructor, low, high)
1659       
1660        if scene is not None:
1661            scene.addItem(self)
1662       
1663    def set_legend(self, heatmap_constructor, low, high):
1664        self.heatmap_constructor = heatmap_constructor
1665        self.low = low
1666        self.high = high
1667        self.color_table = None
1668        self._pixmap = None
1669        self._pixmap_args = None
1670        self.update()
1671        self.updateGeometry()
1672       
1673    def set_color_table(self, color_table):
1674        if qVersion() <= "4.5":
1675            self.color_table = signedPalette(color_table)
1676        else:
1677            self.color_table = color_table
1678       
1679        self._pixmap_args = None
1680        self.update()
1681       
1682    def update(self):
1683        crect = self.contentsRect()
1684        width = crect.width()
1685        height = c_legendHeight
1686        if not self.pixmap_item or self._pixmap_args != (width, height):
1687            bitmap = self.heatmap_constructor.getLegend(int(width), int(height), 1.0)
1688            image = QImage(bitmap, width, height, QImage.Format_Indexed8)
1689            color_table = self.color_table
1690            if color_table:
1691                image.setColorTable(color_table)
1692            self._pixmap = QPixmap.fromImage(image)
1693           
1694        self.pixmap_item.setPixmap(self._pixmap)
1695        self.item_low.setText("%.2f" % self.low)
1696        self.item_high.setText("%.2f" % self.high)
1697        self.layout().activate()
1698        QGraphicsWidget.update(self)
1699           
1700    if DEBUG:
1701        def paint(self, painter, options, widget=0):
1702            rect =  self.geometry()
1703            rect.translate(-self.pos())
1704            painter.drawRect(rect)
1705
1706       
1707class HeatmapSelectionManager(QObject):
1708    """ Selection manager for heatmap rows
1709    """
1710    def __init__(self, parent=None):
1711        QObject.__init__(self, parent)
1712        self.selections = []
1713        self.selection_ranges = []
1714        self.heatmap_widgets = []
1715        self.selection_rects = []
1716        self.heatmaps = []
1717        self._heatmap_ranges = {}
1718        self._start_row = 0
1719       
1720    def set_heatmap_widgets(self, widgets):
1721        self.remove_rows(self.selections)
1722        self.heatmaps = widgets
1723       
1724        # Compute row ranges for all heatmaps
1725        self._heatmap_ranges = {}
1726        for group in widgets:
1727            start = end = 0
1728            for heatmap in group:
1729                end += heatmap.heatmap.height
1730                self._heatmap_ranges[heatmap] = (start, end)
1731                start = end
1732       
1733    def select_rows(self, rows, heatmap=None, clear=True):
1734        """ Add `rows` to selection. If `heatmap` is provided the rows
1735        are mapped from the local indices to global heatmap indics. If `clear`
1736        then remove previous rows.
1737        """
1738        if heatmap is not None:
1739            start, end = self._heatmap_ranges[heatmap]
1740            rows = [start + r for r in rows]
1741           
1742        old_selection = list(self.selections)
1743        if clear:
1744            self.selections = rows
1745        else:
1746            self.selections = sorted(set(self.selections + rows))
1747        if self.selections != old_selection:
1748            self.update_selection_rects()
1749            self.emit(SIGNAL("selection_changed()"))
1750            self.emit(SIGNAL("selection_rects_changed"), self.selection_rects)
1751           
1752    def remove_rows(self, rows):
1753        """ Remove `rows` from the selection.
1754        """
1755        old_selection = list(self.selections)
1756        self.selections = sorted(set(self.selections) - set(rows))
1757        if old_selection != self.selections:
1758            self.update_selection_rects()
1759            self.emit(SIGNAL("selection_changed()"))
1760            self.emit(SIGNAL("selection_rects_changed"), self.selection_rects)
1761           
1762    def combined_ranges(self, ranges):
1763        combined_ranges = set()
1764        for start, end in ranges:
1765            if start <= end:
1766                rng = range(start, end + 1)
1767            else:
1768                rng = range(start, end - 1, -1)
1769            combined_ranges.update(rng)
1770        return sorted(combined_ranges)
1771       
1772    def selection_start(self, heatmap_widget, event):
1773        """ Selection  started by `heatmap_widget` due to `event`.
1774        """
1775        pos = heatmap_widget.mapFromScene(event.scenePos())
1776        row, column = heatmap_widget.cell_at(pos)
1777        start, _ = self._heatmap_ranges[heatmap_widget]
1778        row = start + row
1779        self._start_row = row
1780        range = (row, row)
1781        if event.modifiers() & Qt.ControlModifier:
1782            self.selection_ranges.append(range)
1783        else:
1784            self.selection_ranges = [range]
1785        self.select_rows(self.combined_ranges(self.selection_ranges))
1786       
1787    def selection_update(self, heatmap_widget, event):
1788        """ Selection updated by `heatmap_widget due to `event` (mouse drag).
1789        """
1790        pos = heatmap_widget.mapFromScene(event.scenePos())
1791        row, column = heatmap_widget.cell_at(pos)
1792        start, _ = self._heatmap_ranges[heatmap_widget]
1793        row = start + row
1794        if self.selection_ranges:
1795            self.selection_ranges[-1] = (self._start_row, row)
1796        else:
1797            self.selection_ranges = [(row, row)]
1798           
1799        self.select_rows(self.combined_ranges(self.selection_ranges))
1800       
1801    def selection_finish(self, heatmap_widget, event):
1802        """ Selection finished by `heatmap_widget due to `event`.
1803        """
1804        pos = heatmap_widget.mapFromScene(event.scenePos())
1805        row, column = heatmap_widget.cell_at(pos)
1806        start, _ = self._heatmap_ranges[heatmap_widget]
1807        row = start + row
1808        range = (self._start_row, row)
1809        self.selection_ranges[-1] = range
1810        self.select_rows(self.combined_ranges(self.selection_ranges),
1811                         clear=not event.modifiers() & Qt.ControlModifier)
1812        self.emit(SIGNAL("selection_finished()"))
1813       
1814    def selection_add(self, start, end, heatmap=None, clear=True):
1815        """ Add a selection range from `start` to `end`.
1816        """ 
1817        if heatmap is not None:
1818            _start, _ = self._heatmap_ranges[heatmap]
1819            start = _start + start
1820            end = _start + end
1821       
1822        if clear:
1823            self.selection_ranges = []
1824        self.selection_ranges.append((start, end))
1825        self.select_rows(self.combined_ranges(self.selection_ranges))
1826        self.emit(SIGNAL("selection_finished()"))
1827       
1828    def update_selection_rects(self):
1829        """ Update the selection rects.
1830        """
1831        def continuous_ranges(selections):
1832            """ Group continuous ranges
1833            """
1834            selections = iter(selections)
1835            start = end = selections.next()
1836            try:
1837                while True:
1838                    new_end = selections.next()
1839                    if new_end > end + 1:
1840                        yield start, end
1841                        start = end = new_end
1842                    else:
1843                        end = new_end
1844            except StopIteration:
1845                yield start, end
1846               
1847        def group_selections(selections):
1848            """ Group selections along with heatmaps.
1849            """
1850            rows2hm = self.rows_to_heatmaps()
1851            selections = iter(selections)
1852            start = end = selections.next()
1853            end_heatmaps= rows2hm[end]
1854            try:
1855                while True:
1856                    new_end = selections.next()
1857                    new_end_heatmaps = rows2hm[new_end]
1858                    if new_end > end + 1 or new_end_heatmaps != end_heatmaps:
1859                        yield start, end, end_heatmaps
1860                        start = end = new_end
1861                        end_heatmaps = new_end_heatmaps
1862                    else:
1863                        end = new_end
1864                       
1865            except StopIteration:
1866                yield start, end, end_heatmaps
1867               
1868        def selection_rect(start, end, heatmaps):
1869            rect = QRectF()
1870            for heatmap in heatmaps:
1871                h_start, _ = self._heatmap_ranges[heatmap] 
1872                rect |= heatmap.mapToScene(heatmap.row_rect(start - h_start)).boundingRect()
1873                rect |= heatmap.mapToScene(heatmap.row_rect(end - h_start)).boundingRect()
1874            return rect
1875                 
1876        self.selection_rects = []
1877        for start, end, heatmaps in group_selections(self.selections):
1878            rect = selection_rect(start, end, heatmaps)
1879            self.selection_rects.append(rect)
1880           
1881    def rows_to_heatmaps(self):
1882        heatmap_groups = zip(*self.heatmaps)
1883        rows2hm = {}
1884        for heatmaps in heatmap_groups:
1885            hm = heatmaps[0]
1886            start, end = self._heatmap_ranges[hm]
1887            rows2hm.update(dict.fromkeys(range(start, end), heatmaps))
1888        return rows2hm
1889   
1890         
1891##################################################################################################
1892# test script
1893
1894if __name__=="__main__":
1895    a=QApplication(sys.argv)
1896    ow = OWHeatMap()
1897   
1898#    fn = "/home/marko/pipa4.tab"
1899#    fn = "../../../doc/datasets/brown-selected.tab"
1900    fn = os.path.expanduser("~/Documents/abc-subset")
1901    ow.set_dataset(orange.ExampleTable(fn), 0)
1902    ow.handleNewSignals()
1903    ow.show()
1904   
1905    a.exec_()
1906    ow.saveSettings()
1907
Note: See TracBrowser for help on using the repository browser.