source: orange/Orange/OrangeWidgets/Unsupervised/OWHierarchicalClustering.py @ 11354:daead04df646

Revision 11354:daead04df646, 38.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Ported to new Orange namespace hierarchy (orange2to25).

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