source: orange-bioinformatics/orangecontrib/bio/widgets/OWHeatMap.py @ 1898:ce804448cf39

Revision 1898:ce804448cf39, 78.5 KB checked in by markotoplak, 6 months ago (diff)

Fixed selection of the cell borders in OWHeatMap. Fixes #1336

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               
1242                #obtain examples directly from the heatmap, so their order does not matter
1243
1244                rd = self.selection_manager.rows_to_heatmaps()
1245                hr = self.selection_manager._heatmap_ranges
1246
1247                examples = []
1248                for row in self.selected_rows:
1249                    h = rd[row][0]
1250                    begin,_ = hr[h]
1251                    hm = h.heatmap
1252                    examples.extend(hm.examples[hm.exampleIndices[row-begin] : hm.exampleIndices[row+1-begin]])
1253                   
1254                data = orange.ExampleTable(examples)
1255
1256            else:
1257                data = None
1258       
1259        self.send("Examples", data)
1260        self.selection_changed_flag
1261           
1262    ## handle saved selections
1263    def settingsFromWidgetCallbackSelection(self, handler, context):
1264        context.selection = self.selection_manager.selections
1265
1266    def settingsToWidgetCallbackSelection(self, handler, context):
1267        selection = getattr(context, "selection", None)
1268        if selection:
1269            try:
1270                self.selection_manager.select_rows(selection)
1271            except Exception, ex:
1272                pass
1273#                self.warning(3, "Could not restore selection")
1274               
1275    def split_changed(self):
1276        if self.data:
1277            self.clear_scene()
1278            self.construct_heatmaps(self.data, self.selected_split_label())
1279            self.create_heatmaps(self.heatmapconstructor)
1280            self.construct_heatmaps_scene(self.heatmaps, self.data,
1281                                          attr_cluster=self.attr_cluster,
1282                                          data_clusters=self.data_clusters)
1283       
1284
1285class GraphicsPixmapLayoutItem(QGraphicsLayoutItem):
1286    """ A layout item wraping a QGraphicsPixmapItem
1287    """
1288    def __init__(self, pixmap_item, parent=None):
1289        QGraphicsLayoutItem.__init__(self, parent)
1290        self.pixmap_item = pixmap_item
1291        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1292       
1293    def setGeometry(self, rect):
1294        QGraphicsLayoutItem.setGeometry(self, rect)
1295        self.pixmap_item.setPos(rect.topLeft())
1296       
1297    def sizeHint(self, which, constraint=QSizeF()):
1298        return QSizeF(self.pixmap_item.pixmap().size())
1299   
1300    def setPixmap(self, pixmap):
1301        self.pixmap_item.setPixmap(pixmap)
1302        self.updateGeometry()
1303       
1304       
1305class GraphicsHeatmapWidget(QGraphicsWidget):
1306    def __init__(self, heatmap=None, parent=None, scene=None):
1307        QGraphicsWidget.__init__(self, parent)
1308        self.setAcceptHoverEvents(True)
1309        layout = QGraphicsLinearLayout(Qt.Horizontal)
1310        layout.setContentsMargins(0, 0, 0, 0)
1311        item = QGraphicsPixmapItem(self)
1312        item.setShapeMode(QGraphicsPixmapItem.BoundingRectShape)
1313        self.heatmap_item = GraphicsPixmapLayoutItem(item, self)
1314       
1315        item = QGraphicsPixmapItem(self)
1316        item.setShapeMode(QGraphicsPixmapItem.BoundingRectShape)
1317        self.averages_item = GraphicsPixmapLayoutItem(item, self)
1318       
1319        layout.addItem(self.averages_item)
1320        layout.addItem(self.heatmap_item)
1321        layout.setItemSpacing(0, 2)
1322       
1323        self.setLayout(layout)
1324       
1325        self.heatmap = None
1326        self.show_averages = True
1327        self._pixmap_args = None
1328        self.color_table = None
1329        self.selection_manager = None
1330        self.set_cell_size(4, 4)
1331        self.set_cuts(0, 255)
1332        self.set_heatmap(heatmap)
1333       
1334        if scene is not None:
1335            scene.addItem(self)
1336           
1337    def clear(self):
1338        """ Clear the current heatmap.
1339        """
1340        self.heatmap = None
1341        self._pixmap_args = None
1342        self.heatmap_item.setPixmap(QPixmap())
1343        self.averages_item.setPixmap(QPixmap())
1344        self.show_averages = True
1345        self.layout().invalidate()
1346           
1347    def set_heatmap(self, heatmap):
1348        """ Set the heatmap for display.
1349        """
1350        self.clear()
1351        self.heatmap = heatmap
1352        self.update()
1353       
1354    def set_cell_size(self, width, height):
1355        self.cell_width = width
1356        self.cell_height = height
1357        self.update()
1358       
1359    def set_cuts(self, low, high):
1360        self.cut_low = low
1361        self.cut_high = high
1362        self.update()
1363       
1364    def set_show_averages(self, show):
1365        self.show_averages = show
1366        self._pixmap_args = None
1367        self.update()
1368
1369    def set_color_table(self, color_table):
1370        if qVersion() <= "4.5":
1371            self.color_table = signedPalette(color_table)
1372        else:
1373            self.color_table = color_table
1374       
1375        self._pixmap_args = None
1376        self.update()
1377           
1378    def _update_pixmap(self):
1379        """ Update the pixmap if its construction arguments changed.
1380        """
1381        if self.heatmap:
1382            args = (int(self.cell_width), int(self.cell_height),
1383                    self.cut_low, self.cut_high, 1.0)
1384           
1385            if args != self._pixmap_args:
1386                bitmap, width, height = self.heatmap.getBitmap(*args)
1387                image = QImage(bitmap, width, height, QImage.Format_Indexed8)
1388                color_table = self.color_table
1389                if color_table:
1390                    image.setColorTable(color_table)
1391                self.pixmap = QPixmap.fromImage(image)
1392               
1393                bitmap, width, height = self.heatmap.getAverages(*((c_averageStripeWidth,) + args[1:]))
1394                image = QImage(bitmap, width, height, QImage.Format_Indexed8)
1395                if self.color_table:
1396                    image.setColorTable(color_table)
1397                self.averages_pixmap = QPixmap.fromImage(image)
1398               
1399                self._pixmap_args = args
1400               
1401                self.layout().invalidate()
1402        else:
1403            self.averages_pixmap = None
1404            self.pixmap = None
1405           
1406        self.heatmap_item.setPixmap(self.pixmap or QPixmap())
1407        if self.show_averages and self.averages_pixmap:
1408            self.averages_item.setPixmap(self.averages_pixmap)
1409        else:
1410            self.averages_item.setPixmap(QPixmap())
1411           
1412    def update(self):
1413        self._update_pixmap()
1414        QGraphicsWidget.update(self)
1415       
1416    def set_selection_manager(self, manager):
1417        self.selection_manager = manager
1418       
1419    def cell_at(self, pos):
1420        """ Return the cell row, column from a point `pos` in local
1421        coordinates.
1422       
1423        """
1424        pos = self.mapToItem(self.heatmap_item.pixmap_item, pos)
1425        x, y = pos.x(), pos.y()
1426        def clamp(i, m):
1427            return int(min(max(i, 0), m-1))
1428        return (clamp(math.floor(y / self.cell_height), self.heatmap.height),
1429                clamp(math.floor(x / self.cell_width), self.heatmap.width))
1430   
1431    def cell_rect(self, row, column):
1432        """ Return a QRectF in local coordinates containing the cell
1433        at `row` and `column`.
1434       
1435        """
1436        top = QPointF(column * self.cell_width, row * self.cell_height)
1437        top = self.mapFromItem(self.heatmap_item.pixmap_item, top)
1438        size = QSizeF(self.cell_width, self.cell_height)
1439        return QRectF(top, size)
1440
1441    def row_rect(self, row):
1442        """ Return a QRectF in local coordinates containing the entire row.
1443        """
1444        rect = self.cell_rect(row, 0).united(self.cell_rect(row, self.heatmap.width - 1))
1445        rect.setLeft(0) # To include the average stripe if show.
1446        return rect
1447   
1448    def cell_tool_tip(self, row, column):
1449        hm = self.heatmap
1450        start = int(hm.exampleIndices[row])
1451        end = int(hm.exampleIndices[row + 1])
1452        examples = [hm.examples[start]]
1453        attr = hm.examples.domain[column]
1454        val = "%i, %i: %f" % (row, column, float(examples[0][attr]))
1455        return val
1456   
1457    def hoverEnterEvent(self, event):
1458        row, col = self.cell_at(event.pos())
1459   
1460    def hoverMoveEvent(self, event):
1461        pos = event.pos()
1462        row, column = self.cell_at(pos)
1463        tooltip = self.cell_tool_tip(row, column)
1464        QToolTip.showText(event.screenPos(), tooltip)
1465        return QGraphicsWidget.hoverMoveEvent(self, event)
1466   
1467    def hoverLeaveEvent(self, event):
1468        row, col = self.cell_at(event.pos())
1469   
1470    if DEBUG:
1471        def paint(self, painter, option, widget=0):
1472            rect =  self.geometry()
1473            rect.translate(-self.pos())
1474            painter.drawRect(rect.adjusted(-1, -1, 1, 1))
1475   
1476   
1477class GridWidget(QGraphicsWidget):
1478    def __init__(self, parent=None):
1479        QGraphicsWidget.__init__(self, parent)
1480       
1481    if DEBUG:
1482        def paint(self, painter, option, widget=0):
1483            rect =  self.geometry()
1484            rect.translate(-self.pos())
1485            painter.drawRect(rect)
1486           
1487           
1488class HeatmapScene(QGraphicsScene):
1489    """ A Graphics Scene with heatmap widgets.
1490    """
1491    def __init__(self, parent=None):
1492        QGraphicsScene.__init__(self, parent)
1493        self.selection_manager = HeatmapSelectionManager()
1494       
1495    def set_selection_manager(self, manager):
1496        self.selection_manager = manager
1497       
1498    def _items(self, pos=None, cls=object):
1499        if pos is not None:
1500            items = self.items(QRectF(pos, QSizeF(3, 3)).translated(-1.5, -1.5))
1501        else:
1502            items = self.items()
1503           
1504        for item in items:
1505            if isinstance(item, cls):
1506                yield item
1507           
1508    def heatmap_at_pos(self, pos):
1509        items  = list(self._items(pos, GraphicsHeatmapWidget))
1510        if items:
1511            return items[0]
1512        else:
1513            return None
1514       
1515    def dendrogram_at_pos(self, pos):
1516        items  = list(self._items(pos, DendrogramItem))
1517        if items:
1518            return items[0]
1519        else:
1520            return None
1521       
1522    def heatmap_widgets(self):
1523        return self._items(None, GraphicsHeatmapWidget)
1524       
1525    def select_from_dendrogram(self, dendrogram, clear=True):
1526        """ Select all heatmap rows which belong to the dendrogram.
1527        """
1528        dendrogram_widget = dendrogram.parentWidget()
1529        anchors = list(dendrogram_widget.leaf_anchors())
1530        cluster = dendrogram.cluster
1531        start, end = anchors[cluster.first], anchors[cluster.last - 1]
1532        start, end = dendrogram_widget.mapToScene(start), dendrogram_widget.mapToScene(end)
1533        # Find a heatmap widget containing start and end y coordinates.
1534       
1535        heatmap = None
1536        for hm in self.heatmap_widgets():
1537            b_rect = hm.sceneBoundingRect()
1538            if b_rect.contains(QPointF(b_rect.center().x(), start.y())):
1539                heatmap = hm
1540                break
1541           
1542        if dendrogram:
1543            b_rect = hm.boundingRect()
1544            start, end = hm.mapFromScene(start), hm.mapFromScene(end)
1545            start, _ = hm.cell_at(QPointF(b_rect.center().x(), start.y()))
1546            end, _ = hm.cell_at(QPointF(b_rect.center().x(), end.y()))
1547            self.selection_manager.selection_add(start, end, hm, clear=clear)
1548        return
1549       
1550    def mousePressEvent(self, event):
1551        pos = event.scenePos()
1552        heatmap = self.heatmap_at_pos(pos)
1553        if heatmap and event.button() & Qt.LeftButton:
1554            row, _ = heatmap.cell_at(heatmap.mapFromScene(pos))
1555            self.selection_manager.selection_start(heatmap, event)
1556           
1557        dendrogram = self.dendrogram_at_pos(pos)
1558        if dendrogram and event.button() & Qt.LeftButton:
1559            if dendrogram.orientation == Qt.Vertical:
1560                self.select_from_dendrogram(dendrogram, clear=not event.modifiers() & Qt.ControlModifier)
1561            return 
1562       
1563        return QGraphicsScene.mousePressEvent(self, event)
1564   
1565    def mouseMoveEvent(self, event):
1566        pos = event.scenePos()
1567        heatmap = self.heatmap_at_pos(pos)
1568        if heatmap and event.buttons() & Qt.LeftButton:
1569            row, _ = heatmap.cell_at(heatmap.mapFromScene(pos))
1570            self.selection_manager.selection_update(heatmap, event)
1571           
1572        dendrogram = self.dendrogram_at_pos(pos)
1573        if dendrogram and dendrogram.orientation == Qt.Horizontal: # Filter mouse move events
1574            return
1575           
1576        return QGraphicsScene.mouseMoveEvent(self, event)
1577   
1578    def mouseReleaseEvent(self, event):
1579        pos = event.scenePos()
1580        heatmap = self.heatmap_at_pos(pos)
1581        if heatmap:
1582            row, _ = heatmap.cell_at(heatmap.mapFromScene(pos))
1583            self.selection_manager.selection_finish(heatmap, event)
1584       
1585        dendrogram = self.dendrogram_at_pos(pos)
1586        if dendrogram and dendrogram.orientation == Qt.Horizontal: # Filter mouse events
1587            return
1588       
1589        return QGraphicsScene.mouseReleaseEvent(self, event)
1590   
1591    def mouseDoubleClickEvent(self, event):
1592        pos = event.scenePos()
1593        dendrogram = self.dendrogram_at_pos(pos)
1594        if dendrogram: # Filter mouse events
1595            return
1596        return QGraphicsScene.mouseDoubleClickEvent(self, event)
1597       
1598       
1599class GtI(QGraphicsSimpleTextItem):
1600    if DEBUG:
1601        def paint(self, painter, option, widget =0):
1602            QGraphicsSimpleTextItem.paint(self, painter, option, widget)
1603            painter.drawRect(self.boundingRect())
1604           
1605   
1606class GraphicsSimpleTextLayoutItem(QGraphicsLayoutItem):
1607    """ A Graphics layout item wrapping a QGraphicsSimpleTextItem alowing it
1608    to be managed by a layout.
1609    """
1610    def __init__(self, text_item, orientation=Qt.Horizontal, parent=None):
1611        QGraphicsLayoutItem.__init__(self, parent)
1612        self.orientation = orientation
1613        self.text_item = text_item
1614        if orientation == Qt.Vertical:
1615            self.text_item.rotate(-90)
1616            self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
1617        else:
1618            self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
1619       
1620    def setGeometry(self, rect):
1621        QGraphicsLayoutItem.setGeometry(self, rect)
1622        if self.orientation == Qt.Horizontal:
1623            self.text_item.setPos(rect.topLeft())
1624        else:
1625            self.text_item.setPos(rect.bottomLeft())
1626       
1627    def sizeHint(self, which, constraint=QSizeF()):
1628        if which in [Qt.PreferredSize]:
1629            size = self.text_item.boundingRect().size()
1630            if self.orientation == Qt.Horizontal:
1631                return size
1632            else:
1633                return QSizeF(size.height(), size.width())
1634        else:
1635            return QSizeF()
1636   
1637    def setFont(self, font):
1638        self.text_item.setFont(font)
1639        self.updateGeometry()
1640       
1641    def setText(self, text):
1642        self.text_item.setText(text)
1643        self.updateGeometry()
1644       
1645       
1646class GraphicsSimpleTextList(QGraphicsWidget):
1647    """ A simple text list widget.
1648    """
1649    def __init__(self, labels=[], orientation=Qt.Vertical, parent=None, scene=None):
1650        QGraphicsWidget.__init__(self, parent)
1651        layout = QGraphicsLinearLayout(orientation)
1652        layout.setContentsMargins(0, 0, 0, 0)
1653        layout.setSpacing(0)
1654        self.setLayout(layout)
1655        self.orientation = orientation
1656        self.alignment = Qt.AlignCenter
1657        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
1658        self.set_labels(labels)
1659       
1660        if scene is not None:
1661            scene.addItem(self)
1662       
1663    def clear(self):
1664        """ Remove all text items.
1665        """
1666        layout = self.layout()
1667        for i in reversed(range(layout.count())):
1668            item = layout.itemAt(i)
1669            item.text_item.setParentItem(None)
1670            if self.scene():
1671                self.scene().removeItem(item.text_item)
1672            layout.removeAt(i)
1673       
1674        self.label_items = []
1675        self.updateGeometry()
1676       
1677    def set_labels(self, labels):
1678        """ Set the text labels to show in the widget.
1679        """
1680        self.clear()
1681        orientation = Qt.Horizontal if self.orientation == Qt.Vertical else Qt.Vertical
1682        for text in labels:
1683#            item = QGraphicsSimpleTextItem(text, self)
1684            item = GtI(text, self)
1685            item.setFont(self.font())
1686            item.setToolTip(text)
1687            item = GraphicsSimpleTextLayoutItem(item, orientation, parent=self)
1688            self.layout().addItem(item)
1689            self.layout().setAlignment(item, self.alignment)
1690            self.label_items.append(item)
1691           
1692        self.layout().activate()
1693        self.updateGeometry()
1694   
1695    def setAlignment(self, alignment):
1696        """ Set alignment of text items in the widget
1697        """
1698        self.alignment = alignment
1699        layout = self.layout()
1700        for i in range(layout.count()):
1701            layout.setAlignment(layout.itemAt(i), alignment)
1702           
1703    def setVisible(self, bool):
1704        QGraphicsWidget.setVisible(self, bool)
1705        self.updateGeometry()
1706           
1707    def setFont(self, font):
1708        """ Set the font for the text.
1709        """
1710        QGraphicsWidget.setFont(self, font)
1711        for item in self.label_items:
1712            item.setFont(font)
1713        self.layout().invalidate()
1714        self.updateGeometry()
1715       
1716    def sizeHint(self, which, constraint=QRectF()):
1717        if not self.isVisible():
1718            return QSizeF(0, 0)
1719        else:
1720            return QGraphicsWidget.sizeHint(self, which, constraint)
1721           
1722    if DEBUG:
1723        def paint(self, painter, options, widget=0):
1724            rect =  self.geometry()
1725            rect.translate(-self.pos())
1726            painter.drawRect(rect)
1727       
1728class GraphicsLegendWidget(QGraphicsWidget):
1729    def __init__(self, heatmap_constructor, low, high, parent=None, scene=None):
1730        QGraphicsWidget.__init__(self, parent)
1731        layout = QGraphicsLinearLayout(Qt.Vertical)
1732        self.setLayout(layout)
1733        layout.setContentsMargins(0, 0, 0, 0)
1734        layout.setSpacing(1)
1735       
1736        layout_labels = QGraphicsLinearLayout(Qt.Horizontal)
1737        layout.addItem(layout_labels)
1738        layout_labels.setContentsMargins(0, 0, 0, 0)
1739        label_lo = GtI("%.2f" % low, self)
1740        label_hi = GtI("%.2f" % high, self)
1741        self.item_low = GraphicsSimpleTextLayoutItem(label_lo, parent=self)
1742        self.item_high = GraphicsSimpleTextLayoutItem(label_hi, parent=self)
1743       
1744        layout_labels.addItem(self.item_low)
1745        layout.addStretch()
1746        layout_labels.addItem(self.item_high)
1747       
1748        self._pixmap = QPixmap(c_legendHeight, c_legendHeight)
1749        self.pixmap_item = QGraphicsPixmapItem(self._pixmap, self)
1750        self.pixmap_item = GraphicsPixmapLayoutItem(self.pixmap_item, parent=self)
1751        layout.addItem(self.pixmap_item)
1752       
1753        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
1754        self.set_legend(heatmap_constructor, low, high)
1755       
1756        if scene is not None:
1757            scene.addItem(self)
1758       
1759    def set_legend(self, heatmap_constructor, low, high):
1760        self.heatmap_constructor = heatmap_constructor
1761        self.low = low
1762        self.high = high
1763        self.color_table = None
1764        self._pixmap = None
1765        self._pixmap_args = None
1766        self.update()
1767        self.updateGeometry()
1768       
1769    def set_color_table(self, color_table):
1770        if qVersion() <= "4.5":
1771            self.color_table = signedPalette(color_table)
1772        else:
1773            self.color_table = color_table
1774       
1775        self._pixmap_args = None
1776        self.update()
1777       
1778    def update(self):
1779        crect = self.contentsRect()
1780        width = crect.width()
1781        height = c_legendHeight
1782        if not self.pixmap_item or self._pixmap_args != (width, height):
1783            bitmap = self.heatmap_constructor.getLegend(int(width), int(height), 1.0)
1784            image = QImage(bitmap, width, height, QImage.Format_Indexed8)
1785            color_table = self.color_table
1786            if color_table:
1787                image.setColorTable(color_table)
1788            self._pixmap = QPixmap.fromImage(image)
1789           
1790        self.pixmap_item.setPixmap(self._pixmap)
1791        self.item_low.setText("%.2f" % self.low)
1792        self.item_high.setText("%.2f" % self.high)
1793        self.layout().activate()
1794        QGraphicsWidget.update(self)
1795           
1796    if DEBUG:
1797        def paint(self, painter, options, widget=0):
1798            rect =  self.geometry()
1799            rect.translate(-self.pos())
1800            painter.drawRect(rect)
1801
1802       
1803class HeatmapSelectionManager(QObject):
1804    """ Selection manager for heatmap rows
1805    """
1806    def __init__(self, parent=None):
1807        QObject.__init__(self, parent)
1808        self.selections = []
1809        self.selection_ranges = []
1810        self.heatmap_widgets = []
1811        self.selection_rects = []
1812        self.heatmaps = []
1813        self._heatmap_ranges = {}
1814        self._start_row = 0
1815       
1816    def set_heatmap_widgets(self, widgets):
1817        self.remove_rows(self.selections)
1818        self.heatmaps = widgets
1819       
1820        # Compute row ranges for all heatmaps
1821        self._heatmap_ranges = {}
1822        for group in widgets:
1823            start = end = 0
1824            for heatmap in group:
1825                end += heatmap.heatmap.height
1826                self._heatmap_ranges[heatmap] = (start, end)
1827                start = end
1828       
1829    def select_rows(self, rows, heatmap=None, clear=True):
1830        """ Add `rows` to selection. If `heatmap` is provided the rows
1831        are mapped from the local indices to global heatmap indics. If `clear`
1832        then remove previous rows.
1833        """
1834        if heatmap is not None:
1835            start, end = self._heatmap_ranges[heatmap]
1836            rows = [start + r for r in rows]
1837           
1838        old_selection = list(self.selections)
1839        if clear:
1840            self.selections = rows
1841        else:
1842            self.selections = sorted(set(self.selections + rows))
1843        if self.selections != old_selection:
1844            self.update_selection_rects()
1845            self.emit(SIGNAL("selection_changed()"))
1846            self.emit(SIGNAL("selection_rects_changed"), self.selection_rects)
1847           
1848    def remove_rows(self, rows):
1849        """ Remove `rows` from the selection.
1850        """
1851        old_selection = list(self.selections)
1852        self.selections = sorted(set(self.selections) - set(rows))
1853        if old_selection != self.selections:
1854            self.update_selection_rects()
1855            self.emit(SIGNAL("selection_changed()"))
1856            self.emit(SIGNAL("selection_rects_changed"), self.selection_rects)
1857           
1858    def combined_ranges(self, ranges):
1859        combined_ranges = set()
1860        for start, end in ranges:
1861            if start <= end:
1862                rng = range(start, end + 1)
1863            else:
1864                rng = range(start, end - 1, -1)
1865            combined_ranges.update(rng)
1866        return sorted(combined_ranges)
1867       
1868    def selection_start(self, heatmap_widget, event):
1869        """ Selection  started by `heatmap_widget` due to `event`.
1870        """
1871        pos = heatmap_widget.mapFromScene(event.scenePos())
1872        row, column = heatmap_widget.cell_at(pos)
1873        start, _ = self._heatmap_ranges[heatmap_widget]
1874        row = start + row
1875        self._start_row = row
1876        range = (row, row)
1877        if event.modifiers() & Qt.ControlModifier:
1878            self.selection_ranges.append(range)
1879        else:
1880            self.selection_ranges = [range]
1881        self.select_rows(self.combined_ranges(self.selection_ranges))
1882       
1883    def selection_update(self, heatmap_widget, event):
1884        """ Selection updated by `heatmap_widget due to `event` (mouse drag).
1885        """
1886        pos = heatmap_widget.mapFromScene(event.scenePos())
1887        row, column = heatmap_widget.cell_at(pos)
1888        start, _ = self._heatmap_ranges[heatmap_widget]
1889        row = start + row
1890        if self.selection_ranges:
1891            self.selection_ranges[-1] = (self._start_row, row)
1892        else:
1893            self.selection_ranges = [(row, row)]
1894           
1895        self.select_rows(self.combined_ranges(self.selection_ranges))
1896       
1897    def selection_finish(self, heatmap_widget, event):
1898        """ Selection finished by `heatmap_widget due to `event`.
1899        """
1900        pos = heatmap_widget.mapFromScene(event.scenePos())
1901        row, column = heatmap_widget.cell_at(pos)
1902        start, _ = self._heatmap_ranges[heatmap_widget]
1903        row = start + row
1904        range = (self._start_row, row)
1905        self.selection_ranges[-1] = range
1906        self.select_rows(self.combined_ranges(self.selection_ranges),
1907                         clear=not event.modifiers() & Qt.ControlModifier)
1908        self.emit(SIGNAL("selection_finished()"))
1909       
1910    def selection_add(self, start, end, heatmap=None, clear=True):
1911        """ Add a selection range from `start` to `end`.
1912        """ 
1913        if heatmap is not None:
1914            _start, _ = self._heatmap_ranges[heatmap]
1915            start = _start + start
1916            end = _start + end
1917       
1918        if clear:
1919            self.selection_ranges = []
1920        self.selection_ranges.append((start, end))
1921        self.select_rows(self.combined_ranges(self.selection_ranges))
1922        self.emit(SIGNAL("selection_finished()"))
1923       
1924    def update_selection_rects(self):
1925        """ Update the selection rects.
1926        """
1927        def continuous_ranges(selections):
1928            """ Group continuous ranges
1929            """
1930            selections = iter(selections)
1931            start = end = selections.next()
1932            try:
1933                while True:
1934                    new_end = selections.next()
1935                    if new_end > end + 1:
1936                        yield start, end
1937                        start = end = new_end
1938                    else:
1939                        end = new_end
1940            except StopIteration:
1941                yield start, end
1942               
1943        def group_selections(selections):
1944            """ Group selections along with heatmaps.
1945            """
1946            rows2hm = self.rows_to_heatmaps()
1947            selections = iter(selections)
1948            start = end = selections.next()
1949            end_heatmaps = rows2hm[end]
1950            try:
1951                while True:
1952                    new_end = selections.next()
1953                    new_end_heatmaps = rows2hm[new_end]
1954                    if new_end > end + 1 or new_end_heatmaps != end_heatmaps:
1955                        yield start, end, end_heatmaps
1956                        start = end = new_end
1957                        end_heatmaps = new_end_heatmaps
1958                    else:
1959                        end = new_end
1960                       
1961            except StopIteration:
1962                yield start, end, end_heatmaps
1963               
1964        def selection_rect(start, end, heatmaps):
1965            rect = QRectF()
1966            for heatmap in heatmaps:
1967                h_start, _ = self._heatmap_ranges[heatmap] 
1968                rect |= heatmap.mapToScene(heatmap.row_rect(start - h_start)).boundingRect()
1969                rect |= heatmap.mapToScene(heatmap.row_rect(end - h_start)).boundingRect()
1970            return rect
1971             
1972        self.selection_rects = []
1973        for start, end, heatmaps in group_selections(self.selections):
1974            rect = selection_rect(start, end, heatmaps)
1975            self.selection_rects.append(rect)
1976           
1977    def rows_to_heatmaps(self):
1978        heatmap_groups = zip(*self.heatmaps)
1979        rows2hm = {}
1980        for heatmaps in heatmap_groups:
1981            hm = heatmaps[0]
1982            start, end = self._heatmap_ranges[hm]
1983            rows2hm.update(dict.fromkeys(range(start, end), heatmaps))
1984        return rows2hm
1985   
1986         
1987##################################################################################################
1988# test script
1989
1990if __name__=="__main__":
1991    a=QApplication(sys.argv)
1992    ow = OWHeatMap()
1993   
1994    ow.set_dataset(orange.ExampleTable("brown-selected"), 0)
1995    ow.handleNewSignals()
1996    ow.show()
1997   
1998    a.exec_()
1999    ow.saveSettings()
2000
Note: See TracBrowser for help on using the repository browser.