source: orange/Orange/OrangeWidgets/Unsupervised/OWHierarchicalClustering.py @ 11353:b86fabda2b08

Revision 11353:b86fabda2b08, 38.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Some PEP8 code style fixes.

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