source: orange/Orange/OrangeWidgets/Unsupervised/OWHierarchicalClustering.py @ 11217:adbdaf6efe02

Revision 11217:adbdaf6efe02, 37.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Addeed new widget icons by Peter Cuhalev and replaced existing ones with expanded paths and removed groups.

Line 
1"""
2<name>Hierarchical Clustering</name>
3<description>Hierarchical clustering based on distance matrix, and a dendrogram viewer.</description>
4<icon>icons/HierarchicalClustering.svg</icon>
5<contact>Ales Erjavec (ales.erjavec(@at@)fri.uni-lj.si)</contact> 
6<priority>2100</priority>
7"""
8from __future__ import with_statement
9
10from OWWidget import *
11from OWQCanvasFuncts import *
12import OWClustering
13import OWGUI
14import OWColorPalette
15import math
16import numpy
17import os
18
19import orange
20from Orange.clustering import hierarchical
21
22from OWDlgs import OWChooseImageSizeDlg
23
24from PyQt4.QtCore import *
25from PyQt4.QtGui import *
26
27class OWHierarchicalClustering(OWWidget):
28    settingsList = ["Linkage", "Annotation", "PrintDepthCheck",
29                    "PrintDepth", "HDSize", "VDSize", "ManualHorSize","AutoResize",
30                    "TextSize", "LineSpacing", "SelectionMode",
31                    "AppendClusters", "CommitOnChange", "ClassifyName", "addIdAs"]
32   
33    contextHandlers={"":DomainContextHandler("", [ContextField("Annotation", DomainContextHandler.Required)])}
34   
35    def __init__(self, parent=None, signalManager=None):
36        OWWidget.__init__(self, parent, signalManager, 'Hierarchical Clustering', wantGraph=True)
37       
38        self.inputs = [("Distances", orange.SymMatrix, self.set_matrix)]
39        self.outputs = [("Selected Data", ExampleTable), ("Other Data", ExampleTable), ("Centroids", ExampleTable)]
40        self.linkage = [("Single linkage", orange.HierarchicalClustering.Single),
41                        ("Average linkage", orange.HierarchicalClustering.Average),
42                        ("Ward's linkage", orange.HierarchicalClustering.Ward),
43                        ("Complete linkage", orange.HierarchicalClustering.Complete),
44                       ]
45        self.Linkage = 3
46        self.Annotation = 0
47        self.PrintDepthCheck = 0
48        self.PrintDepth = 10
49        self.HDSize = 500         #initial horizontal and vertical dendrogram size
50        self.VDSize = 800
51        self.ManualHorSize = 0
52        self.AutoResize = 0
53        self.TextSize = 8
54        self.LineSpacing = 4
55        self.SelectionMode = 0
56        self.AppendClusters = 0
57        self.CommitOnChange = 0
58        self.ClassifyName = "HC_class"
59        self.addIdAs = 0
60       
61        self.loadSettings()
62       
63        self.inputMatrix = None
64        self.matrixSource = "Unknown"
65        self.root_cluster = None
66        self.selectedExamples = None
67       
68        self.selectionChanged = False
69
70        self.linkageMethods = [a[0] for a in self.linkage]
71
72        #################################
73        ##GUI
74        #################################
75       
76        #HC Settings
77        OWGUI.comboBox(self.controlArea, self, "Linkage", box="Linkage",
78                items=self.linkageMethods, tooltip="Choose linkage method",
79                callback=self.run_clustering, addSpace = True)
80        #Label
81        box = OWGUI.widgetBox(self.controlArea, "Annotation", addSpace = True)
82        self.labelCombo = OWGUI.comboBox(box, self, "Annotation",
83                items=["None"],tooltip="Choose label attribute",
84                callback=self.update_labels)
85
86        OWGUI.spin(box, self, "TextSize", label="Text size",
87                        min=5, max=15, step=1, 
88                        callback=self.update_font,
89                        controlWidth=40,
90                        keyboardTracking=False)
91#        OWGUI.spin(box,self, "LineSpacing", label="Line spacing",
92#                        min=2,max=8,step=1,
93#                        callback=self.update_spacing,
94#                        controlWidth=40,
95#                        keyboardTracking=False)
96
97        # Dendrogram graphics settings
98        dendrogramBox = OWGUI.widgetBox(self.controlArea, "Limits", 
99                                        addSpace=True)
100       
101        form = QFormLayout()
102        form.setLabelAlignment(Qt.AlignLeft)
103       
104        # Depth settings
105        sw = OWGUI.widgetBox(dendrogramBox, orientation="horizontal",
106                             addToLayout=False)
107        cw = OWGUI.widgetBox(dendrogramBox, orientation="horizontal", 
108                             addToLayout=False)
109       
110        sllp = OWGUI.hSlider(sw, self, "PrintDepth", minValue=1, maxValue=50, 
111                             callback=self.on_depth_change)
112        cblp = OWGUI.checkBox(cw, self, "PrintDepthCheck", "Show to depth", 
113                              callback = self.on_depth_change, 
114                              disables=[sw])
115        form.addRow(cw, sw)
116       
117        checkWidth = OWGUI.checkButtonOffsetHint(cblp)
118       
119        # Width settings
120        sw = OWGUI.widgetBox(dendrogramBox, orientation="horizontal", 
121                             addToLayout=False)
122        cw = OWGUI.widgetBox(dendrogramBox, orientation="horizontal", 
123                             addToLayout=False)
124       
125        hsb = OWGUI.spin(sw, self, "HDSize", min=200, max=10000, step=10, 
126                         callback=self.on_width_changed, 
127                         callbackOnReturn = False,
128                         keyboardTracking=False)
129        cbhs = OWGUI.checkBox(cw, self, "ManualHorSize", "Horizontal size", 
130                              callback=self.on_width_changed, 
131                              disables=[sw])
132       
133        self.hSizeBox = hsb
134        form.addRow(cw, sw)
135        dendrogramBox.layout().addLayout(form)
136
137        # Selection settings
138        box = OWGUI.widgetBox(self.controlArea, "Selection")
139        OWGUI.checkBox(box, self, "SelectionMode", "Show cutoff line",
140                       callback=self.update_cutoff_line)
141        cb = OWGUI.checkBox(box, self, "AppendClusters", "Append cluster IDs",
142                            callback=self.commit_data_if)
143       
144        self.classificationBox = ib = OWGUI.widgetBox(box, margin=0)
145       
146        form = QWidget()
147        le = OWGUI.lineEdit(form, self, "ClassifyName", None, callback=None,
148                            orientation="horizontal")
149        self.connect(le, SIGNAL("editingFinished()"), self.commit_data_if)
150
151        aa = OWGUI.comboBox(form, self, "addIdAs", label=None,
152                            orientation="horizontal",
153                            items=["Class attribute",
154                                   "Attribute",
155                                   "Meta attribute"],
156                            callback=self.commit_data_if)
157       
158        layout = QFormLayout()
159        layout.setSpacing(8)
160        layout.setContentsMargins(0, 5, 0, 5)
161        layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
162        layout.setLabelAlignment(Qt.AlignLeft)
163        layout.addRow("Name  ", le)
164        layout.addRow("Place  ", aa)
165       
166        form.setLayout(layout)
167       
168        ib.layout().addWidget(form)
169        ib.layout().setContentsMargins(checkWidth, 5, 5, 5)
170       
171        cb.disables.append(ib)
172        cb.makeConsistent()
173       
174        OWGUI.separator(box)
175        cbAuto = OWGUI.checkBox(box, self, "CommitOnChange", "Commit on change")
176        btCommit = OWGUI.button(box, self, "&Commit", self.commit_data, default=True)
177        OWGUI.setStopper(self, btCommit, cbAuto, "selectionChanged", self.commit_data)
178       
179       
180        OWGUI.rubber(self.controlArea)
181        self.connect(self.graphButton, SIGNAL("clicked()"), self.saveGraph)
182
183        self.scale_scene = scale = ScaleScene(self, self)
184        self.headerView = ScaleView(scale, self)
185        self.footerView = ScaleView(scale, self)
186       
187        self.dendrogram = DendrogramScene(self)
188        self.dendrogramView = DendrogramView(self.dendrogram, self.mainArea)
189       
190        self.connect(self.dendrogram, SIGNAL("clusterSelectionChanged()"), self.on_selection_change)
191        self.connect(self.dendrogram, SIGNAL("sceneRectChanged(QRectF)"), scale.scene_rect_update)
192        self.connect(self.dendrogram, SIGNAL("dendrogramGeometryChanged(QRectF)"), self.on_dendrogram_geometry_change)
193        self.connect(self.dendrogram, SIGNAL("cutoffValueChanged(float)"), self.on_cuttof_value_changed)
194       
195        self.connect(self.dendrogramView, SIGNAL("viewportResized(QSize)"), self.on_width_changed)
196        self.connect(self.dendrogramView, SIGNAL("transformChanged(QTransform)"), self.headerView.setTransform)
197        self.connect(self.dendrogramView, SIGNAL("transformChanged(QTransform)"), self.footerView.setTransform)
198       
199        self.mainArea.layout().addWidget(self.headerView)
200        self.mainArea.layout().addWidget(self.dendrogramView)
201        self.mainArea.layout().addWidget(self.footerView)
202       
203        self.dendrogram.header = self.headerView
204        self.dendrogram.footer = self.footerView
205
206        self.connect(self.dendrogramView.horizontalScrollBar(),SIGNAL("valueChanged(int)"),
207                self.footerView.horizontalScrollBar().setValue)
208        self.connect(self.dendrogramView.horizontalScrollBar(),SIGNAL("valueChanged(int)"),
209                self.headerView.horizontalScrollBar().setValue)
210        self.dendrogram.setSceneRect(0, 0, self.HDSize,self.VDSize)
211        self.dendrogram.update()
212        self.resize(800, 500)
213       
214        self.natural_dendrogram_width = 800 
215        self.dendrogramView.set_fit_to_width(not self.ManualHorSize)
216       
217        self.matrix = None
218        self.selectionList = []
219        self.selected_clusters = [] 
220
221    def sendReport(self):
222        self.reportSettings("Settings",
223                            [("Linkage", self.linkageMethods[self.Linkage]),
224                             ("Annotation", self.labelCombo.currentText()),
225                             self.PrintDepthCheck and ("Shown depth limited to", self.PrintDepth),
226                             self.SelectionMode and hasattr(self, "cutoff_height") and ("Cutoff line at", self.cutoff_height)])
227       
228        self.reportSection("Dendrogram")
229        canvases = header, graph, footer = self.headerView.scene(), self.dendrogramView.scene(), self.footerView.scene()
230        buffer = QPixmap(max(c.width() for c in canvases), sum(c.height() for c in canvases))
231        painter = QPainter(buffer)
232        painter.fillRect(buffer.rect(), QBrush(QColor(255, 255, 255)))
233        header.render(painter, QRectF(0, 0, header.width(), header.height()), QRectF(0, 0, header.width(), header.height()))
234        graph.render(painter, QRectF(0, header.height(), graph.width(), graph.height()), QRectF(0, 0, graph.width(), graph.height()))
235        footer.render(painter, QRectF(0, header.height()+graph.height(), footer.width(), footer.height()), QRectF(0, 0, footer.width(), footer.height()))
236        painter.end()
237        self.reportImage(lambda filename: buffer.save(filename, os.path.splitext(filename)[1][1:]))
238       
239    def clear(self):
240        self.matrix = None
241        self.root_cluster = None
242        self.selected_clusters = []
243        self.dendrogram.clear()
244        self.labelCombo.clear()
245       
246    def set_matrix(self, data):
247        self.clear()
248        self.matrix = data
249        self.closeContext()
250        if not self.matrix:
251            self.root_cluster = None
252            self.selectedExamples = None
253            self.dendrogram.clear()
254            self.labelCombo.clear()
255            self.send("Selected Data", None)
256            self.send("Other Data", None)
257            self.classificationBox.setDisabled(True)
258            return
259
260        self.matrixSource = "Unknown"
261        items = getattr(self.matrix, "items")
262        if isinstance(items, orange.ExampleTable): #Example Table from Example Distance
263
264            self.labels = ["None", "Default"]+ \
265                          [a.name for a in items.domain.attributes]
266            if items.domain.classVar:
267                self.labels.append(items.domain.classVar.name)
268
269            self.labelInd = range(len(self.labels) - 2)
270            self.labels.extend([m.name for m in items.domain.getmetas().values()])
271            self.labelInd.extend(items.domain.getmetas().keys())
272            self.numMeta = len(items.domain.getmetas())
273            self.metaLabels = items.domain.getmetas().values()
274            self.matrixSource = "Example Distance"
275        elif isinstance(items, list):   # a list of item (most probably strings)
276            self.labels = ["None", "Default", "Name", "Strain"]
277            self.Annotation = 0
278            self.matrixSource = "Data Distance"
279        else:   #From Attribute Distance
280            self.labels = ["None", "Attribute Name"]
281            self.Annotation = 1
282            self.matrixSource="Attribute Distance"
283           
284        self.labelCombo.clear()
285        self.labelCombo.addItems(self.labels)
286        if len(self.labels) < self.Annotation - 1:
287                self.Annotation = 0
288        self.labelCombo.setCurrentIndex(self.Annotation)
289        if self.matrixSource == "Example Distance":
290            self.classificationBox.setDisabled(False)
291        else:
292            self.classificationBox.setDisabled(True)
293        if self.matrixSource == "Example Distance":
294            self.openContext("", items)
295        self.error(0)
296        try:
297            self.run_clustering()
298        except orange.KernelException, ex:
299            self.error(0, "Could not cluster data! %s" % ex.message)
300            self.setMatrix(None)
301       
302    def update_labels(self):
303        """ Change the labels in the scene.
304        """
305        if self.matrix is None:
306            return
307       
308        items = getattr(self.matrix, "items", range(self.matrix.dim))
309       
310        if self.Annotation == 0:
311            labels = [""] * len(items)
312        elif self.Annotation == 1:
313            try:
314                labels = [item.name for item in items]
315                if not any(labels): raise AttributeError("No labels.")
316            except AttributeError:
317                labels = [str(item) for item in items]
318        elif self.Annotation > 1 and isinstance(items, ExampleTable):
319            attr = self.labelInd[min(self.Annotation - 2, len(self.labelInd) - 1)]
320            labels = [str(ex[attr]) for ex in items]
321        else:
322            labels = [str(item) for item in items]
323           
324        self.dendrogram.set_labels(labels)
325        self.dendrogram.set_tool_tips(labels)
326
327    def run_clustering(self):
328        if self.matrix:
329            self.progressBarInit()
330            self.root_cluster = orange.HierarchicalClustering(self.matrix,
331                linkage=self.linkage[self.Linkage][1],
332                progressCallback=lambda value, a: self.progressBarSet(value*100))
333            self.progressBarFinished()
334            self.display_tree()
335
336    def display_tree(self):
337        root = self.root_cluster
338        if self.PrintDepthCheck:
339            root = hierarchical.pruned(root, level=self.PrintDepth)
340        self.display_tree1(root)
341       
342    def display_tree1(self, tree):
343        self.dendrogram.clear()
344        self.update_font()
345        self.cutoff_height = tree.height * 0.95
346        self.dendrogram.set_cluster(tree)
347        self.update_labels()
348        self.update_cutoff_line()
349       
350    def update_font(self):
351        font = self.font()
352        font.setPointSize(self.TextSize)
353        self.dendrogram.setFont(font)
354        if self.dendrogram.widget:
355            self.update_labels()
356           
357    def update_spacing(self):
358        if self.dendrogram.labels_widget:
359            layout = self.dendrogram.labels_widget.layout()
360            layout.setSpacing(self.LineSpacing)
361       
362    def on_width_changed(self, size=None):
363        if size is not None:
364            auto_width = size.width() - 20
365        else:
366            auto_width = self.natural_dendrogram_width
367           
368        self.dendrogramView.set_fit_to_width(not self.ManualHorSize)
369        if self.ManualHorSize:
370            self.dendrogram.set_scene_width_hint(self.HDSize)
371            self.update_labels()
372        else:
373            self.dendrogram.set_scene_width_hint(auto_width)
374            self.update_labels()
375           
376    def on_dendrogram_geometry_change(self, geometry):
377        if self.root_cluster and self.dendrogram.widget:
378            widget = self.dendrogram.widget
379            left, top, right, bottom = widget.layout().getContentsMargins()
380            geometry = geometry.adjusted(left, top, -right, -bottom)
381            self.scale_scene.set_scale_bounds(geometry.left(), geometry.right())
382            self.scale_scene.set_scale(self.root_cluster.height, 0.0)
383            pos = widget.pos_at_height(self.cutoff_height)
384            self.scale_scene.set_marker(pos)
385           
386    def on_depth_change(self):
387        if self.root_cluster and self.dendrogram.widget:
388            selected = self.dendrogram.widget.selected_clusters()
389            selected = set([(c.first, c.last) for c in selected])
390            root = self.root_cluster
391            if self.PrintDepthCheck:
392                root = hierarchical.pruned(root, level=self.PrintDepth)
393            self.display_tree1(root)
394           
395            selected = [c for c in hierarchical.preorder(root) if (c.first, c.last) in selected]
396            self.dendrogram.widget.set_selected_clusters(selected) 
397
398    def set_cuttof_position_from_scale(self, pos):
399        """ Cuttof position changed due to a mouse event in the scale scene.
400        """
401        if self.root_cluster and self.dendrogram.cutoff_line:
402            height = self.dendrogram.widget.height_at(pos)
403            self.cutoff_height = height
404            line = self.dendrogram.cutoff_line.set_cutoff_at_height(height)
405           
406    def on_cuttof_value_changed(self, height):
407        """ Cuttof value changed due to line drag in the dendrogram.
408        """
409        if self.root_cluster and self.dendrogram.widget:
410            self.cutoff_height = height
411            widget = self.dendrogram.widget
412            pos = widget.pos_at_height(height)
413            self.scale_scene.set_marker(pos)
414           
415    def update_cutoff_line(self):
416        if self.matrix:
417            if self.SelectionMode:
418                self.dendrogram.cutoff_line.show()
419                self.scale_scene.marker.show()
420            else:
421                self.dendrogram.cutoff_line.hide()
422                self.scale_scene.marker.hide()
423           
424    def on_selection_change(self):
425        if self.matrix:
426            try:
427                items = self.dendrogram.widget.selected_items
428                self.selected_clusters = [item.cluster for item in items]
429                self.commit_data_if()
430            except RuntimeError: # underlying C/C++ object has been deleted
431                pass
432       
433    def commit_data_if(self):
434        if self.CommitOnChange:
435            self.commit_data()
436        else:
437            self.selectionChanged = True
438
439    def commit_data(self):
440        items = getattr(self.matrix, "items", None)
441        if not items:
442            return # nothing to commit
443       
444        self.selectionChanged = False
445        self.selectedExamples = None
446        selection = self.selected_clusters
447        selection = sorted(selection, key=lambda c: c.first)
448        maps = [list(self.root_cluster.mapping[c.first: c.last]) for c in selection]
449       
450        from operator import add
451        selected_indices = reduce(add, maps, [])
452        unselected_indices = sorted(set(self.root_cluster.mapping) - set(selected_indices))
453       
454        self.selection = selected = [items[k] for k in selected_indices]
455        unselected = [items[k] for k in unselected_indices]
456       
457        if not selected:
458            self.send("Selected Data", None)
459            self.send("Other Data", None)
460            self.send("Centroids", None)
461            return
462       
463        if isinstance(items, ExampleTable): 
464            c = [i for i in range(len(maps)) for j in maps[i]]
465            aid = clustVar = None
466            if self.AppendClusters:
467                clustVar = orange.EnumVariable(str(self.ClassifyName) ,
468                            values=["Cluster " + str(i) for i in range(len(maps))] + ["Other"])
469                origDomain = items.domain
470                if self.addIdAs == 0:
471                    domain = orange.Domain(origDomain.attributes, clustVar)
472                    if origDomain.classVar:
473                        domain.addmeta(orange.newmetaid(), origDomain.classVar)
474                    aid = -1
475                elif self.addIdAs == 1:
476                    domain = orange.Domain(origDomain.attributes + [clustVar], origDomain.classVar)
477                    aid = len(origDomain.attributes)
478                else:
479                    domain = orange.Domain(origDomain.attributes, origDomain.classVar)
480                    aid = orange.newmetaid()
481                    domain.addmeta(aid, clustVar)
482
483                domain.addmetas(origDomain.getmetas())
484                table1 = table2 = None
485                if selected:
486                    table1 = orange.ExampleTable(domain, selected)
487                    for i in range(len(selected)):
488                        table1[i][clustVar] = clustVar("Cluster " + str(c[i]))
489               
490                if unselected:
491                    table2 = orange.ExampleTable(domain, unselected)
492                    for ex in table2:
493                        ex[clustVar] = clustVar("Other")
494
495                self.selectedExamples = table1
496                self.unselectedExamples = table2
497            else:
498                self.selectedExamples = orange.ExampleTable(selected) if selected else None
499                self.unselectedExamples = orange.ExampleTable(unselected) if unselected else None
500               
501            self.send("Selected Data", self.selectedExamples)
502            self.send("Other Data", self.unselectedExamples)
503
504            self.centroids = None
505            if self.selectedExamples:
506                self.centroids = orange.ExampleTable(self.selectedExamples.domain)
507                for i in range(len(maps)):
508                    clusterEx = [ex for cluster, ex in zip(c, self.selectedExamples) if cluster == i]
509                    clusterEx = orange.ExampleTable(clusterEx)
510                    contstat = orange.DomainBasicAttrStat(clusterEx)
511                    discstat = orange.DomainDistributions(clusterEx, 0, 0, 1)
512                    ex = [cs.avg if cs else (ds.modus() if ds else "?") for cs, ds in zip(contstat, discstat)]
513                    example = orange.Example(self.centroids.domain, ex)
514                    if clustVar is not None:
515                        example[clustVar] = clustVar(i)
516                    self.centroids.append(ex)
517            self.send("Centroids", self.centroids)
518           
519        elif self.matrixSource=="Data Distance":
520            names=list(set([d.strain for d in self.selection]))
521            data=[(name, [d for d in filter(lambda a:a.strain==name, self.selection)]) for name in names]
522            self.send("Structured Data Files",data)
523           
524    def saveGraph(self):
525       sizeDlg = OWChooseImageSizeDlg(self.dendrogram, parent=self)
526       sizeDlg.exec_()
527       
528
529class DendrogramView(QGraphicsView):
530    def __init__(self, *args):
531        QGraphicsView.__init__(self, *args)
532        self.viewport().setMouseTracking(True)
533        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
534        self.setAlignment(Qt.AlignLeft | Qt.AlignCenter)
535        self.setFocusPolicy(Qt.WheelFocus)
536#        self.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
537        self.setRenderHints(QPainter.TextAntialiasing)
538        self.fit_contents = True
539        self.connect(self, SIGNAL("viewportResized(QSize)"), self.updateTransform)
540        self.connect(self.scene(), SIGNAL("sceneRectChanged(QRectF)"), lambda: self.updateTransform(self.viewport().size()))
541
542    def resizeEvent(self, e):
543        QGraphicsView.resizeEvent(self, e)
544        self.emit(SIGNAL("viewportResized(QSize)"), e.size())
545       
546    def updateTransform(self, size):
547        return
548#        if self.fit_contents:
549#            scene_rect = self.scene().sceneRect()
550#            trans = QTransform()
551#            scale = size.width() / scene_rect.width()
552#            trans.scale(scale, scale)
553#            self.setTransform(trans)
554#        else:
555#            self.setTransform(QTransform())
556#        self.emit(SIGNAL("transformChanged(QTransform)"), self.transform())
557       
558    def set_fit_to_width(self, state):
559        self.fit_contents = state
560        self.updateTransform(self.viewport().size())
561
562from OWGraphics import GraphicsSimpleTextList
563
564class DendrogramScene(QGraphicsScene):
565    def __init__(self, *args):
566        QGraphicsScene.__init__(self, *args)
567        self.root_cluster = None
568        self.header = None
569        self.footer = None
570       
571        self.grid_widget = None
572        self.widget = None
573        self.labels_widget = None
574        self.scene_width_hint = 800
575        self.leaf_labels = {}
576
577    def set_cluster(self, root):
578        """ Set the cluster to display
579        """
580        self.clear()
581        self.root_cluster = root
582        self.cutoff_line = None
583       
584        if not self.root_cluster:
585            return
586       
587        # the main widget containing the dendrogram and labels
588        self.grid_widget = QGraphicsWidget()
589        self.addItem(self.grid_widget)
590        layout = QGraphicsGridLayout()
591        self.grid_widget.setLayout(layout)
592       
593        # dendrogram widget
594        self.widget = widget = OWClustering.DendrogramWidget(root=root, orientation=Qt.Vertical, parent=self.grid_widget)
595        self.connect(widget, SIGNAL("dendrogramGeometryChanged(QRectF)"), 
596                     lambda rect: self.emit(SIGNAL("dendrogramGeometryChanged(QRectF)"),
597                                            rect))
598        self.connect(widget, SIGNAL("selectionChanged()"),
599                     lambda: self.emit(SIGNAL("clusterSelectionChanged()"))
600                     )
601       
602        widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
603       
604        layout.addItem(self.widget, 0, 0)
605        self.grid_widget.setMinimumWidth(self.scene_width_hint)
606        self.grid_widget.setMaximumWidth(self.scene_width_hint)
607       
608        spacing = QFontMetrics(self.font()).lineSpacing()
609        left, top, right, bottom = widget.layout().getContentsMargins()
610        widget.layout().setContentsMargins(0.0, spacing / 2.0, 0.0, spacing / 2.0)
611       
612        labels = [self.cluster_text(leaf.cluster) for leaf in widget.leaf_items()]
613       
614        # Labels widget
615        labels = GraphicsSimpleTextList(labels, orientation=Qt.Vertical, parent=self.grid_widget)
616        labels.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
617        labels.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
618        labels.setFont(self.font())
619        layout.addItem(labels, 0, 1)
620       
621        # Cutoff line   
622        self.cutoff_line = OWClustering.CutoffLine(widget)
623        self.connect(self.cutoff_line.emiter, SIGNAL("cutoffValueChanged(float)"),
624                     lambda val: self.emit(SIGNAL("cutoffValueChanged(float)"), val))
625        self.cutoff_line.set_cutoff_at_height(self.root_cluster.height * 0.95)
626           
627        self.labels_widget = labels
628       
629        layout.activate() 
630        self._update_scene_rect()
631       
632    def cluster_text(self, cluster):
633        """ Return the text to display next to the cluster.
634        """
635        if cluster in self.leaf_labels:
636            return self.leaf_labels[cluster]
637        elif cluster.first == cluster.last - 1:
638            value = str(cluster.mapping[cluster.first])
639            return value
640        else:
641            values = [str(cluster.mapping[i]) \
642                      for i in range(cluster.first, cluster.last)]
643            return str(values[0]) + "..."
644
645    def set_labels(self, labels):
646        """ Set the item labels.
647        """
648        assert(len(labels) == len(self.root_cluster.mapping))
649        if self.labels_widget:
650            label_items = []
651            for leaf in self.widget.leaf_items():
652                cluster = leaf.cluster
653                indices = cluster.mapping[cluster.first: cluster.last]
654                text = [labels[i] for i in indices]
655                if len(text) > 1:
656                    text = text[0] + "..."
657                else:
658                    text = text[0]
659                label_items.append(text)
660               
661            self.labels_widget.set_labels(label_items)
662        self._update_scene_rect()
663
664    def set_tool_tips(self, tool_tips):
665        """ Set the item tool tips.
666        """
667        assert(len(tool_tips) == len(self.root_cluster.mapping))
668        if self.labels_widget:
669            for leaf, label in zip(self.widget.leaf_items(), self.labels_widget):
670                cluster = leaf.cluster
671                indices = cluster.mapping[cluster.first: cluster.last]
672                text = [tool_tips[i] for i in indices]
673                text = "<br>".join(text)
674                label.setToolTip(text)
675               
676    def set_scene_width_hint(self, width):
677        self.scene_width_hint = width
678        if self.grid_widget:
679            self.grid_widget.setMinimumWidth(self.scene_width_hint)
680            self.grid_widget.setMaximumWidth(self.scene_width_hint)
681           
682    def clear(self):
683        self.widget = None
684        self.grid_widget = None
685        self.labels_widget = None
686        self.root_cluster = None
687        self.cutoff_line = None
688        QGraphicsScene.clear(self)
689       
690    def setFont(self, font):
691        QGraphicsScene.setFont(self, font)
692        if self.labels_widget:
693            self.labels_widget.setFont(self.font())
694        if self.widget:
695            # Fix widget top and bottom margins.
696            spacing = QFontMetrics(self.font()).lineSpacing()
697            left, top, right, bottom = self.widget.layout().getContentsMargins()
698            self.widget.layout().setContentsMargins(left, spacing / 2.0, right, spacing / 2.0)
699            self.grid_widget.resize(self.grid_widget.sizeHint(Qt.PreferredSize))
700           
701    def _update_scene_rect(self):
702        self.setSceneRect(reduce(QRectF.united, [item.sceneBoundingRect() for item in self.items()], QRectF()).adjusted(-10, -10, 10, 10))
703       
704   
705class AxisScale(QGraphicsWidget):
706    """ A graphics widget for an axis scale
707    """
708    # Defaults
709    orientation = Qt.Horizontal
710    tick_count=5
711    tick_align = Qt.AlignTop
712    text_align = Qt.AlignHCenter | Qt.AlignBottom
713    axis_scale = (0.0, 1.0)
714   
715    def __init__(self, parent=None, orientation=Qt.Horizontal, tick_count=5,
716                 tick_align = Qt.AlignBottom,
717                 text_align = Qt.AlignHCenter | Qt.AlignBottom,
718                 axis_scale = (0.0, 1.0)):
719        QGraphicsWidget.__init__(self, parent) 
720        self.orientation = orientation
721        self.tick_count = tick_count
722        self.tick_align = tick_align
723        self.text_align = text_align
724        self.axis_scale = axis_scale
725       
726    def set_orientation(self, orientation):
727        self.orientation = orientation
728        if self.orientation == Qt.Horizontal:
729            self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
730        else:
731            self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
732        self.updateGeometry()
733       
734    def ticks(self):
735        minval, maxval = self.axis_scale
736        ticks = ["%.2f" % val for val in numpy.linspace(minval, maxval, self.tick_count)]
737        return ticks
738       
739    def paint(self, painter, option, widget=0):
740        painter.setFont(self.font())
741        size = self.geometry().size()
742        metrics = QFontMetrics(painter.font())
743        minval, maxval = self.axis_scale
744        tick_count = self.tick_count
745       
746        if self.orientation == Qt.Horizontal:
747            spanx, spany = size.width(), 0.0
748            xadv, yadv =  spanx / tick_count, 0.0
749            tick_w, tick_h = 0.0, -5.0
750            tick_offset = QPointF(0.0, tick_h)
751        else:
752            spanx, spany = 0.0, size.height()
753            xadv, yadv = 0.0, spany / tick_count
754            tick_w, tick_h = 5.0, 0.0
755            tick_func = lambda : (y / spany)
756            tick_offset = QPointF(tick_w + 1.0, metrics.ascent()/2)
757           
758        ticks = self.ticks()
759           
760        xstart, ystart = 0.0, 0.0
761       
762        if self.orientation == Qt.Horizontal:
763            painter.translate(0.0, size.height())
764           
765        painter.drawLine(xstart, ystart, xstart + tick_count*xadv, ystart + tick_count*yadv)
766       
767        linspacex = numpy.linspace(0.0, spanx, tick_count)
768        linspacey = numpy.linspace(0.0, spany, tick_count)
769       
770        metrics = painter.fontMetrics()
771        for x, y, tick in zip(linspacex, linspacey, ticks):
772            painter.drawLine(x, y, x + tick_w, y + tick_h)
773            if self.orientation == Qt.Horizontal:
774                rect = QRectF(metrics.boundingRect(tick))
775                rect.moveCenter(QPointF(x, y) + tick_offset + QPointF(0.0, -rect.height()/2.0))
776                painter.drawText(rect, tick)
777            else:
778                painter.drawText(QPointF(x, y) + tick_offset, tick)
779               
780#        rect =  self.geometry()
781#        rect.translate(-self.pos())
782#        painter.drawRect(rect)
783       
784    def setGeometry(self, rect):
785        self.prepareGeometryChange()
786        return QGraphicsWidget.setGeometry(self, rect)
787       
788    def sizeHint(self, which, *args):
789        if which == Qt.PreferredSize:
790            minval, maxval = self.axis_scale
791            ticks = self.ticks()
792            metrics = QFontMetrics(self.font())
793            if self.orientation == Qt.Horizontal:
794                h = metrics.height() + 5
795                w = 100 
796            else:
797                h = 100
798                w = max([metrics.width(t) for t in ticks]) + 5
799            return QSizeF(w, h)
800        else:
801            return QSizeF()
802
803    def boundingRect(self):
804        metrics = QFontMetrics(self.font())
805        geometry = self.geometry()
806        ticks = self.ticks()
807        if self.orientation == Qt.Horizontal:
808            h = 5 + metrics.height()
809            left = - metrics.boundingRect(ticks[0]).width()/2.0 #+ geometry.left()
810            right = geometry.width() + metrics.boundingRect(ticks[-1]).width()/2.0
811            rect = QRectF(left, 0.0, right - left, h)
812        else:
813            h = geometry.height()
814            w = max([metrics.width(t) for t in ticks]) + 5
815            rect = QRectF(0.0, 0.0, w, h) 
816        return rect
817   
818    def set_axis_scale(self, min, max):
819        self.axis_scale = (min, max)
820        self.updateGeometry()
821       
822    def set_axis_ticks(self, ticks):
823        if isinstance(ticks, dict):
824            self.ticks = ticks
825        self.updateGeometry()
826           
827    def tick_layout(self):
828        """ Return the tick layout
829        """
830        minval, maxval = self.axis_scale
831        ticks = numpy.linspace(minval, maxval, self.tick_count)
832        return zip(ticks, self.ticks())
833   
834#        min, max = self.axis_scale
835#        ticks = self.ticks()
836#        span = max - min
837#        span_log = math.log10(span)
838#        log_sign = -1 if log_sign < 0.0 else 1
839#        span_log = math.floor(span_log)
840#        major_ticks = [(x, 5.0, tick(i, span_log)) for i in range(self.tick_count)]
841#        minor_ticks = [(x, 3.0, tick(i, span_log + log_sign))  for i in range(self.tick_count * 2)]
842#        return [(i, major, label) for i, tick, label in major_ticks]
843   
844   
845class ScaleScene(QGraphicsScene):
846    def __init__(self, widget, parent=None):
847        QGraphicsScene.__init__(self, parent)
848        self.widget = widget
849        self.scale_widget = AxisScale(orientation=Qt.Horizontal)
850        font = self.font()
851        font.setPointSize(10)
852        self.scale_widget.setFont(font)
853        self.marker = QGraphicsLineItem()
854        pen = QPen(Qt.black, 2)
855        pen.setCosmetic(True)
856        self.marker.setPen(pen)
857        self.marker.setLine(0.0, 0.0, 0.0, 25.0)
858        self.marker.setCursor(Qt.SizeHorCursor)
859        self.addItem(self.scale_widget)
860        self.addItem(self.marker)
861        self.setSceneRect(QRectF(0, 0, self.scale_widget.size().height(), 25.0))
862       
863    def set_scale(self, min, max):
864        self.scale_widget.set_axis_scale(min, max)
865       
866    def set_scale_bounds(self, start, end):
867        self.scale_widget.setPos(start, 0)
868        size_hint = self.scale_widget.sizeHint(Qt.PreferredSize)
869        self.scale_widget.resize(end - start, self.scale_widget.size().height())
870       
871    def scene_rect_update(self, rect):
872        scale_rect = self.scale_widget.sceneBoundingRect()
873        rect = QRectF(rect.x(), scale_rect.y(), rect.width(), scale_rect.height())
874        rect = QRectF(rect.x(), 0.0, rect.width(), scale_rect.height())
875        self.marker.setLine(0, 0, 0, scale_rect.height())
876        self.setSceneRect(rect)
877       
878    def set_marker(self, pos):
879        pos = self.scale_widget.mapToScene(pos)
880        self.marker.setPos(pos.x(), 0.0)
881       
882    def mousePressEvent(self, event):
883        if event.button() == Qt.LeftButton:
884            pos = self.scale_widget.mapFromScene(event.scenePos())
885            self.widget.set_cuttof_position_from_scale(pos)
886           
887    def mouseMoveEvent(self, event):
888        if event.buttons() & Qt.LeftButton:
889            pos = self.scale_widget.mapFromScene(event.scenePos())
890            self.widget.set_cuttof_position_from_scale(pos)
891       
892    def mouseReleaseEvent(self, event):
893        if event.button() == Qt.LeftButton:
894            pos = self.scale_widget.mapFromScene(event.scenePos())
895            self.widget.set_cuttof_position_from_scale(pos)
896           
897               
898class ScaleView(QGraphicsView):
899    def __init__(self, scene=None, parent=None):
900        QGraphicsView.__init__(self, scene, parent)
901        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
902        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
903        self.setAlignment(Qt.AlignLeft | Qt.AlignCenter)
904        self.setFixedHeight(25.0)
905
906
907def test():
908    app=QApplication(sys.argv)
909    w=OWHierarchicalClustering()
910    w.show()
911    data=orange.ExampleTable("../../doc/datasets/iris.tab")
912    id=orange.newmetaid()
913    data.domain.addmeta(id, orange.FloatVariable("a"))
914    data.addMetaAttribute(id)
915    matrix = orange.SymMatrix(len(data))
916    dist = orange.ExamplesDistanceConstructor_Euclidean(data)
917    matrix = orange.SymMatrix(len(data))
918    matrix.setattr('items', data)
919    for i in range(len(data)):
920        for j in range(i+1):
921            matrix[i, j] = dist(data[i], data[j])
922
923    w.set_matrix(matrix)
924    app.exec_()
925   
926if __name__=="__main__":
927    test()
Note: See TracBrowser for help on using the repository browser.