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

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

Moved '_bioinformatics' into orangecontrib namespace.

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