source: orange/orange/OrangeWidgets/Unsupervised/OWHierarchicalClustering.py @ 9132:8442ceb5e817

Revision 9132:8442ceb5e817, 37.9 KB checked in by ales_erjavec <ales.erjavec@…>, 3 years ago (diff)

Fixed cutoff line Z ordering.
Fixes and error with selection of the last added cluster.

Line 
1"""
2<name>Hierarchical Clustering</name>
3<description>Hierarchical clustering based on distance matrix, and a dendrogram viewer.</description>
4<icon>icons/HierarchicalClustering.png</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 = [("Distance matrix", orange.SymMatrix, self.set_matrix)]
39        self.outputs = [("Selected Examples", ExampleTable), ("Unselected Examples", 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 = None
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 Examples", None)
256            self.send("Unselected Examples", 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            if isinstance(items, ExampleTable):
314                labels = [str(i) for i in range(len(items))]
315            else:
316                try:
317                    labels = [item.name for item in items]
318                except AttributeError:
319                    labels = [str(item) for item in items]
320        elif self.Annotation > 1 and isinstance(items, ExampleTable):
321            attr = self.labelInd[min(self.Annotation - 2, len(self.labelInd) - 1)]
322            labels = [str(ex[attr]) for ex in items]
323        else:
324            labels = [str(item) for item in items]
325           
326        self.dendrogram.set_labels(labels)
327        self.dendrogram.set_tool_tips(labels)
328
329    def run_clustering(self):
330        if self.matrix:
331            self.progressBarInit()
332            self.root_cluster = orange.HierarchicalClustering(self.matrix,
333                linkage=self.linkage[self.Linkage][1],
334                progressCallback=lambda value, a: self.progressBarSet(value*100))
335            self.progressBarFinished()
336            self.display_tree()
337
338    def display_tree(self):
339        root = self.root_cluster
340        if self.PrintDepthCheck:
341            root = hierarchical.pruned(root, level=self.PrintDepth)
342        self.display_tree1(root)
343       
344    def display_tree1(self, tree):
345        self.dendrogram.clear()
346        self.update_font()
347        self.cutoff_height = tree.height * 0.95
348        self.dendrogram.set_cluster(tree)
349        self.update_labels()
350        self.update_cutoff_line()
351       
352    def update_font(self):
353        font = self.font()
354        font.setPointSize(self.TextSize)
355        self.dendrogram.setFont(font)
356        if self.dendrogram.widget:
357            self.update_labels()
358           
359    def update_spacing(self):
360        if self.dendrogram.labels_widget:
361            layout = self.dendrogram.labels_widget.layout()
362            layout.setSpacing(self.LineSpacing)
363       
364    def on_width_changed(self, size=None):
365        if size is not None:
366            auto_width = size.width() - 20
367        else:
368            auto_width = self.natural_dendrogram_width
369           
370        self.dendrogramView.set_fit_to_width(not self.ManualHorSize)
371        if self.ManualHorSize:
372            self.dendrogram.set_scene_width_hint(self.HDSize)
373            self.update_labels()
374        else:
375            self.dendrogram.set_scene_width_hint(auto_width)
376            self.update_labels()
377           
378    def on_dendrogram_geometry_change(self, geometry):
379        if self.root_cluster and self.dendrogram.widget:
380            widget = self.dendrogram.widget
381            left, top, right, bottom = widget.layout().getContentsMargins()
382            geometry = geometry.adjusted(left, top, -right, -bottom)
383            self.scale_scene.set_scale_bounds(geometry.left(), geometry.right())
384            self.scale_scene.set_scale(self.root_cluster.height, 0.0)
385            pos = widget.pos_at_height(self.cutoff_height)
386            self.scale_scene.set_marker(pos)
387           
388    def on_depth_change(self):
389        if self.root_cluster and self.dendrogram.widget:
390            selected = self.dendrogram.widget.selected_clusters()
391            selected = set([(c.first, c.last) for c in selected])
392            root = self.root_cluster
393            if self.PrintDepthCheck:
394                root = hierarchical.pruned(root, level=self.PrintDepth)
395            self.display_tree1(root)
396           
397            selected = [c for c in hierarchical.preorder(root) if (c.first, c.last) in selected]
398            self.dendrogram.widget.set_selected_clusters(selected) 
399
400    def set_cuttof_position_from_scale(self, pos):
401        """ Cuttof position changed due to a mouse event in the scale scene.
402        """
403        if self.root_cluster and self.dendrogram.cutoff_line:
404            height = self.dendrogram.widget.height_at(pos)
405            self.cutoff_height = height
406            line = self.dendrogram.cutoff_line.set_cutoff_at_height(height)
407           
408    def on_cuttof_value_changed(self, height):
409        """ Cuttof value changed due to line drag in the dendrogram.
410        """
411        if self.root_cluster and self.dendrogram.widget:
412            self.cutoff_height = height
413            widget = self.dendrogram.widget
414            pos = widget.pos_at_height(height)
415            self.scale_scene.set_marker(pos)
416           
417    def update_cutoff_line(self):
418        if self.matrix:
419            if self.SelectionMode:
420                self.dendrogram.cutoff_line.show()
421                self.scale_scene.marker.show()
422            else:
423                self.dendrogram.cutoff_line.hide()
424                self.scale_scene.marker.hide()
425           
426    def on_selection_change(self):
427        if self.matrix:
428            try:
429                items = self.dendrogram.widget.selected_items
430                self.selected_clusters = [item.cluster for item in items]
431                self.commit_data_if()
432            except RuntimeError: # underlying C/C++ object has been deleted
433                pass
434       
435    def commit_data_if(self):
436        if self.CommitOnChange:
437            self.commit_data()
438        else:
439            self.selectionChanged = True
440
441    def commit_data(self):
442        items = getattr(self.matrix, "items", None)
443        if not items:
444            return # nothing to commit
445       
446        self.selectionChanged = False
447        self.selectedExamples = None
448        selection = self.selected_clusters
449        selection = sorted(selection, key=lambda c: c.first)
450        maps = [list(self.root_cluster.mapping[c.first: c.last]) for c in selection]
451       
452        from operator import add
453        selected_indices = reduce(add, maps, [])
454        unselected_indices = sorted(set(self.root_cluster.mapping) - set(selected_indices))
455       
456        self.selection = selected = [items[k] for k in selected_indices]
457        unselected = [items[k] for k in unselected_indices]
458       
459        if not selected:
460            self.send("Selected Examples", None)
461            self.send("Unselected Examples", None)
462            self.send("Centroids", None)
463            return
464       
465        if isinstance(items, ExampleTable): 
466            c = [i for i in range(len(maps)) for j in maps[i]]
467            aid = clustVar = None
468            if self.AppendClusters:
469                clustVar = orange.EnumVariable(str(self.ClassifyName) ,
470                            values=["Cluster " + str(i) for i in range(len(maps))] + ["Other"])
471                origDomain = items.domain
472                if self.addIdAs == 0:
473                    domain = orange.Domain(origDomain.attributes, clustVar)
474                    if origDomain.classVar:
475                        domain.addmeta(orange.newmetaid(), origDomain.classVar)
476                    aid = -1
477                elif self.addIdAs == 1:
478                    domain = orange.Domain(origDomain.attributes + [clustVar], origDomain.classVar)
479                    aid = len(origDomain.attributes)
480                else:
481                    domain = orange.Domain(origDomain.attributes, origDomain.classVar)
482                    aid = orange.newmetaid()
483                    domain.addmeta(aid, clustVar)
484
485                domain.addmetas(origDomain.getmetas())
486                table1 = table2 = None
487                if selected:
488                    table1 = orange.ExampleTable(domain, selected)
489                    for i in range(len(selected)):
490                        table1[i][clustVar] = clustVar("Cluster " + str(c[i]))
491               
492                if unselected:
493                    table2 = orange.ExampleTable(domain, unselected)
494                    for ex in table2:
495                        ex[clustVar] = clustVar("Other")
496
497                self.selectedExamples = table1
498                self.unselectedExamples = table2
499            else:
500                self.selectedExamples = orange.ExampleTable(selected) if selected else None
501                self.unselectedExamples = orange.ExampleTable(unselected) if unselected else None
502               
503            self.send("Selected Examples", self.selectedExamples)
504            self.send("Unselected Examples", self.unselectedExamples)
505
506            self.centroids = None
507            if self.selectedExamples:
508                self.centroids = orange.ExampleTable(self.selectedExamples.domain)
509                for i in range(len(maps)):
510                    clusterEx = [ex for cluster, ex in zip(c, self.selectedExamples) if cluster == i]
511                    clusterEx = orange.ExampleTable(clusterEx)
512                    contstat = orange.DomainBasicAttrStat(clusterEx)
513                    discstat = orange.DomainDistributions(clusterEx, 0, 0, 1)
514                    ex = [cs.avg if cs else (ds.modus() if ds else "?") for cs, ds in zip(contstat, discstat)]
515                    example = orange.Example(self.centroids.domain, ex)
516                    if clustVar is not None:
517                        example[clustVar] = clustVar(i)
518                    self.centroids.append(ex)
519            self.send("Centroids", self.centroids)
520           
521        elif self.matrixSource=="Data Distance":
522            names=list(set([d.strain for d in self.selection]))
523            data=[(name, [d for d in filter(lambda a:a.strain==name, self.selection)]) for name in names]
524            self.send("Structured Data Files",data)
525           
526    def saveGraph(self):
527       sizeDlg = OWChooseImageSizeDlg(self.dendrogram, parent=self)
528       sizeDlg.exec_()
529       
530
531class DendrogramView(QGraphicsView):
532    def __init__(self, *args):
533        QGraphicsView.__init__(self, *args)
534        self.viewport().setMouseTracking(True)
535        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
536        self.setAlignment(Qt.AlignLeft | Qt.AlignCenter)
537        self.setFocusPolicy(Qt.WheelFocus)
538#        self.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
539        self.setRenderHints(QPainter.TextAntialiasing)
540        self.fit_contents = True
541        self.connect(self, SIGNAL("viewportResized(QSize)"), self.updateTransform)
542        self.connect(self.scene(), SIGNAL("sceneRectChanged(QRectF)"), lambda: self.updateTransform(self.viewport().size()))
543
544    def resizeEvent(self, e):
545        QGraphicsView.resizeEvent(self, e)
546        self.emit(SIGNAL("viewportResized(QSize)"), e.size())
547       
548    def updateTransform(self, size):
549        return
550#        if self.fit_contents:
551#            scene_rect = self.scene().sceneRect()
552#            trans = QTransform()
553#            scale = size.width() / scene_rect.width()
554#            trans.scale(scale, scale)
555#            self.setTransform(trans)
556#        else:
557#            self.setTransform(QTransform())
558#        self.emit(SIGNAL("transformChanged(QTransform)"), self.transform())
559       
560    def set_fit_to_width(self, state):
561        self.fit_contents = state
562        self.updateTransform(self.viewport().size())
563
564from OWGraphics import GraphicsSimpleTextList
565
566class DendrogramScene(QGraphicsScene):
567    def __init__(self, *args):
568        QGraphicsScene.__init__(self, *args)
569        self.root_cluster = None
570        self.header = None
571        self.footer = None
572       
573        self.grid_widget = None
574        self.widget = None
575        self.labels_widget = None
576        self.scene_width_hint = 800
577        self.leaf_labels = {}
578
579    def set_cluster(self, root):
580        """ Set the cluster to display
581        """
582        self.clear()
583        self.root_cluster = root
584        self.cutoff_line = None
585       
586        if not self.root_cluster:
587            return
588       
589        # the main widget containing the dendrogram and labels
590        self.grid_widget = QGraphicsWidget()
591        self.addItem(self.grid_widget)
592        layout = QGraphicsGridLayout()
593        self.grid_widget.setLayout(layout)
594       
595        # dendrogram widget
596        self.widget = widget = OWClustering.DendrogramWidget(root=root, orientation=Qt.Vertical, parent=self.grid_widget)
597        self.connect(widget, SIGNAL("dendrogramGeometryChanged(QRectF)"), 
598                     lambda rect: self.emit(SIGNAL("dendrogramGeometryChanged(QRectF)"),
599                                            rect))
600        self.connect(widget, SIGNAL("selectionChanged()"),
601                     lambda: self.emit(SIGNAL("clusterSelectionChanged()"))
602                     )
603       
604        widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
605       
606        layout.addItem(self.widget, 0, 0)
607        self.grid_widget.setMinimumWidth(self.scene_width_hint)
608        self.grid_widget.setMaximumWidth(self.scene_width_hint)
609       
610        spacing = QFontMetrics(self.font()).lineSpacing()
611        left, top, right, bottom = widget.layout().getContentsMargins()
612        widget.layout().setContentsMargins(0.0, spacing / 2.0, 0.0, spacing / 2.0)
613       
614        labels = [self.cluster_text(leaf.cluster) for leaf in widget.leaf_items()]
615       
616        # Labels widget
617        labels = GraphicsSimpleTextList(labels, orientation=Qt.Vertical, parent=self.grid_widget)
618        labels.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
619        labels.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
620        labels.setFont(self.font())
621        layout.addItem(labels, 0, 1)
622       
623        # Cutoff line   
624        self.cutoff_line = OWClustering.CutoffLine(widget)
625        self.connect(self.cutoff_line.emiter, SIGNAL("cutoffValueChanged(float)"),
626                     lambda val: self.emit(SIGNAL("cutoffValueChanged(float)"), val))
627        self.cutoff_line.set_cutoff_at_height(self.root_cluster.height * 0.95)
628           
629        self.labels_widget = labels
630       
631        layout.activate() 
632        self._update_scene_rect()
633       
634    def cluster_text(self, cluster):
635        """ Return the text to display next to the cluster.
636        """
637        if cluster in self.leaf_labels:
638            return self.leaf_labels[cluster]
639        elif cluster.first == cluster.last - 1:
640            value = str(cluster.mapping[cluster.first])
641            return value
642        else:
643            values = [str(cluster.mapping[i]) \
644                      for i in range(cluster.first, cluster.last)]
645            return str(values[0]) + "..."
646
647    def set_labels(self, labels):
648        """ Set the item labels.
649        """
650        assert(len(labels) == len(self.root_cluster.mapping))
651        if self.labels_widget:
652            label_items = []
653            for leaf in self.widget.leaf_items():
654                cluster = leaf.cluster
655                indices = cluster.mapping[cluster.first: cluster.last]
656                text = [labels[i] for i in indices]
657                if len(text) > 1:
658                    text = text[0] + "..."
659                else:
660                    text = text[0]
661                label_items.append(text)
662               
663            self.labels_widget.set_labels(label_items)
664        self._update_scene_rect()
665
666    def set_tool_tips(self, tool_tips):
667        """ Set the item tool tips.
668        """
669        assert(len(tool_tips) == len(self.root_cluster.mapping))
670        if self.labels_widget:
671            for leaf, label in zip(self.widget.leaf_items(), self.labels_widget):
672                cluster = leaf.cluster
673                indices = cluster.mapping[cluster.first: cluster.last]
674                text = [tool_tips[i] for i in indices]
675                text = "<br>".join(text)
676                label.setToolTip(text)
677               
678    def set_scene_width_hint(self, width):
679        self.scene_width_hint = width
680        if self.grid_widget:
681            self.grid_widget.setMinimumWidth(self.scene_width_hint)
682            self.grid_widget.setMaximumWidth(self.scene_width_hint)
683           
684    def clear(self):
685        self.widget = None
686        self.grid_widget = None
687        self.labels_widget = None
688        self.root_cluster = None
689        self.cutoff_line = None
690        QGraphicsScene.clear(self)
691       
692    def setFont(self, font):
693        QGraphicsScene.setFont(self, font)
694        if self.labels_widget:
695            self.labels_widget.setFont(self.font())
696        if self.widget:
697            # Fix widget top and bottom margins.
698            spacing = QFontMetrics(self.font()).lineSpacing()
699            left, top, right, bottom = self.widget.layout().getContentsMargins()
700            self.widget.layout().setContentsMargins(left, spacing / 2.0, right, spacing / 2.0)
701            self.grid_widget.resize(self.grid_widget.sizeHint(Qt.PreferredSize))
702           
703    def _update_scene_rect(self):
704        self.setSceneRect(reduce(QRectF.united, [item.sceneBoundingRect() for item in self.items()], QRectF()).adjusted(-10, -10, 10, 10))
705       
706   
707class AxisScale(QGraphicsWidget):
708    """ A graphics widget for an axis scale
709    """
710    # Defaults
711    orientation = Qt.Horizontal
712    tick_count=5
713    tick_align = Qt.AlignTop
714    text_align = Qt.AlignHCenter | Qt.AlignBottom
715    axis_scale = (0.0, 1.0)
716   
717    def __init__(self, parent=None, orientation=Qt.Horizontal, tick_count=5,
718                 tick_align = Qt.AlignBottom,
719                 text_align = Qt.AlignHCenter | Qt.AlignBottom,
720                 axis_scale = (0.0, 1.0)):
721        QGraphicsWidget.__init__(self, parent) 
722        self.orientation = orientation
723        self.tick_count = tick_count
724        self.tick_align = tick_align
725        self.text_align = text_align
726        self.axis_scale = axis_scale
727       
728    def set_orientation(self, orientation):
729        self.orientation = orientation
730        if self.orientation == Qt.Horizontal:
731            self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
732        else:
733            self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
734        self.updateGeometry()
735       
736    def ticks(self):
737        minval, maxval = self.axis_scale
738        ticks = ["%.2f" % val for val in numpy.linspace(minval, maxval, self.tick_count)]
739        return ticks
740       
741    def paint(self, painter, option, widget=0):
742        painter.setFont(self.font())
743        size = self.geometry().size()
744        metrics = QFontMetrics(painter.font())
745        minval, maxval = self.axis_scale
746        tick_count = self.tick_count
747       
748        if self.orientation == Qt.Horizontal:
749            spanx, spany = size.width(), 0.0
750            xadv, yadv =  spanx / tick_count, 0.0
751            tick_w, tick_h = 0.0, -5.0
752            tick_offset = QPointF(0.0, tick_h)
753        else:
754            spanx, spany = 0.0, size.height()
755            xadv, yadv = 0.0, spany / tick_count
756            tick_w, tick_h = 5.0, 0.0
757            tick_func = lambda : (y / spany)
758            tick_offset = QPointF(tick_w + 1.0, metrics.ascent()/2)
759           
760        ticks = self.ticks()
761           
762        xstart, ystart = 0.0, 0.0
763       
764        if self.orientation == Qt.Horizontal:
765            painter.translate(0.0, size.height())
766           
767        painter.drawLine(xstart, ystart, xstart + tick_count*xadv, ystart + tick_count*yadv)
768       
769        linspacex = numpy.linspace(0.0, spanx, tick_count)
770        linspacey = numpy.linspace(0.0, spany, tick_count)
771       
772        metrics = painter.fontMetrics()
773        for x, y, tick in zip(linspacex, linspacey, ticks):
774            painter.drawLine(x, y, x + tick_w, y + tick_h)
775            if self.orientation == Qt.Horizontal:
776                rect = QRectF(metrics.boundingRect(tick))
777                rect.moveCenter(QPointF(x, y) + tick_offset + QPointF(0.0, -rect.height()/2.0))
778                painter.drawText(rect, tick)
779            else:
780                painter.drawText(QPointF(x, y) + tick_offset, tick)
781               
782#        rect =  self.geometry()
783#        rect.translate(-self.pos())
784#        painter.drawRect(rect)
785       
786    def setGeometry(self, rect):
787        self.prepareGeometryChange()
788        return QGraphicsWidget.setGeometry(self, rect)
789       
790    def sizeHint(self, which, *args):
791        if which == Qt.PreferredSize:
792            minval, maxval = self.axis_scale
793            ticks = self.ticks()
794            metrics = QFontMetrics(self.font())
795            if self.orientation == Qt.Horizontal:
796                h = metrics.height() + 5
797                w = 100 
798            else:
799                h = 100
800                w = max([metrics.width(t) for t in ticks]) + 5
801            return QSizeF(w, h)
802        else:
803            return QSizeF()
804
805    def boundingRect(self):
806        metrics = QFontMetrics(self.font())
807        geometry = self.geometry()
808        ticks = self.ticks()
809        if self.orientation == Qt.Horizontal:
810            h = 5 + metrics.height()
811            left = - metrics.boundingRect(ticks[0]).width()/2.0 #+ geometry.left()
812            right = geometry.width() + metrics.boundingRect(ticks[-1]).width()/2.0
813            rect = QRectF(left, 0.0, right - left, h)
814        else:
815            h = geometry.height()
816            w = max([metrics.width(t) for t in ticks]) + 5
817            rect = QRectF(0.0, 0.0, w, h) 
818        return rect
819   
820    def set_axis_scale(self, min, max):
821        self.axis_scale = (min, max)
822        self.updateGeometry()
823       
824    def set_axis_ticks(self, ticks):
825        if isinstance(ticks, dict):
826            self.ticks = ticks
827        self.updateGeometry()
828           
829    def tick_layout(self):
830        """ Return the tick layout
831        """
832        minval, maxval = self.axis_scale
833        ticks = numpy.linspace(minval, maxval, self.tick_count)
834        return zip(ticks, self.ticks())
835   
836#        min, max = self.axis_scale
837#        ticks = self.ticks()
838#        span = max - min
839#        span_log = math.log10(span)
840#        log_sign = -1 if log_sign < 0.0 else 1
841#        span_log = math.floor(span_log)
842#        major_ticks = [(x, 5.0, tick(i, span_log)) for i in range(self.tick_count)]
843#        minor_ticks = [(x, 3.0, tick(i, span_log + log_sign))  for i in range(self.tick_count * 2)]
844#        return [(i, major, label) for i, tick, label in major_ticks]
845   
846   
847class ScaleScene(QGraphicsScene):
848    def __init__(self, widget, parent=None):
849        QGraphicsScene.__init__(self, parent)
850        self.widget = widget
851        self.scale_widget = AxisScale(orientation=Qt.Horizontal)
852        font = self.font()
853        font.setPointSize(10)
854        self.scale_widget.setFont(font)
855        self.marker = QGraphicsLineItem()
856        pen = QPen(Qt.black, 2)
857        pen.setCosmetic(True)
858        self.marker.setPen(pen)
859        self.marker.setLine(0.0, 0.0, 0.0, 25.0)
860        self.marker.setCursor(Qt.SizeHorCursor)
861        self.addItem(self.scale_widget)
862        self.addItem(self.marker)
863        self.setSceneRect(QRectF(0, 0, self.scale_widget.size().height(), 25.0))
864       
865    def set_scale(self, min, max):
866        self.scale_widget.set_axis_scale(min, max)
867       
868    def set_scale_bounds(self, start, end):
869        self.scale_widget.setPos(start, 0)
870        size_hint = self.scale_widget.sizeHint(Qt.PreferredSize)
871        self.scale_widget.resize(end - start, self.scale_widget.size().height())
872       
873    def scene_rect_update(self, rect):
874        scale_rect = self.scale_widget.sceneBoundingRect()
875        rect = QRectF(rect.x(), scale_rect.y(), rect.width(), scale_rect.height())
876        rect = QRectF(rect.x(), 0.0, rect.width(), scale_rect.height())
877        self.marker.setLine(0, 0, 0, scale_rect.height())
878        self.setSceneRect(rect)
879       
880    def set_marker(self, pos):
881        pos = self.scale_widget.mapToScene(pos)
882        self.marker.setPos(pos.x(), 0.0)
883       
884    def mousePressEvent(self, event):
885        if event.button() == Qt.LeftButton:
886            pos = self.scale_widget.mapFromScene(event.scenePos())
887            self.widget.set_cuttof_position_from_scale(pos)
888           
889    def mouseMoveEvent(self, event):
890        if event.buttons() & Qt.LeftButton:
891            pos = self.scale_widget.mapFromScene(event.scenePos())
892            self.widget.set_cuttof_position_from_scale(pos)
893       
894    def mouseReleaseEvent(self, event):
895        if event.button() == Qt.LeftButton:
896            pos = self.scale_widget.mapFromScene(event.scenePos())
897            self.widget.set_cuttof_position_from_scale(pos)
898           
899               
900class ScaleView(QGraphicsView):
901    def __init__(self, scene=None, parent=None):
902        QGraphicsView.__init__(self, scene, parent)
903        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
904        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
905        self.setAlignment(Qt.AlignLeft | Qt.AlignCenter)
906        self.setFixedHeight(25.0)
907
908
909def test():
910    app=QApplication(sys.argv)
911    w=OWHierarchicalClustering()
912    w.show()
913    data=orange.ExampleTable("../../doc/datasets/iris.tab")
914    id=orange.newmetaid()
915    data.domain.addmeta(id, orange.FloatVariable("a"))
916    data.addMetaAttribute(id)
917    matrix = orange.SymMatrix(len(data))
918    dist = orange.ExamplesDistanceConstructor_Euclidean(data)
919    matrix = orange.SymMatrix(len(data))
920    matrix.setattr('items', data)
921    for i in range(len(data)):
922        for j in range(i+1):
923            matrix[i, j] = dist(data[i], data[j])
924
925    w.set_matrix(matrix)
926    app.exec_()
927   
928if __name__=="__main__":
929    test()
Note: See TracBrowser for help on using the repository browser.