source: orange/Orange/OrangeWidgets/Unsupervised/OWHierarchicalClustering.py @ 11357:4abd5db855ac

Revision 11357:4abd5db855ac, 39.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Removed code for obsolete 'Structured data' matrix items.

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