source: orange/Orange/OrangeWidgets/Unsupervised/OWHierarchicalClustering.py @ 11355:92126d5aff73

Revision 11355:92126d5aff73, 40.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Include the axis scale in the saved dendrogram image.

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