source: orange-bioinformatics/orangecontrib/bio/widgets/OWHeatMap.py @ 1878:8b1e95b04793

Revision 1878:8b1e95b04793, 78.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 7 months ago (diff)

Merged biolab/orange-bioinformatics into default

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