source: orange/Orange/OrangeWidgets/Unsupervised/OWHierarchicalClustering.py @ 11356:8c65f3946c66

Revision 11356:8c65f3946c66, 40.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Ensure manual width spin box is disabled when not in use.

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