Repository: biolab/orange3

Issue:
Hierarchical Clustering widget: show group ID on cluster selection
Dendrogram in Hierarchical Clustering widget should display cluster IDs when clusters are selected. Think about where to put the label: onto the cluster, to the left, or to the right of the cluster. TBD.



Discussion:
how about placing the cluster id vertically , starting from the point at which branches intersect 
or
making the cluster id follow the mouse pointer as long as a particular cluster remains in focus.

which one sounds better?

If I understood your first suggestion correctly - you meant this?
![labels](https://cloud.githubusercontent.com/assets/2387315/13702962/8387cd2a-e792-11e5-8067-b84f5d96ad56.png)
I like it.

Great, then I guess I will go with this one only.

Any other suggestion?

While you're at it, could you also add cluster id's as tooltips?

I am a little new to orange, what exactly is a tooltips?

Also should I put cluster id's permanently at each intersection or should they be visible as long as its respective cluster is in focus. 

Permanently.

Tooltip is a text that appears when the mouse is hovering over an object.

Correct me if I am wrong, their is no predefined cluster id declared for each cluster. So I have to put C1,C2,C3... in level order tree traversal.

I think it should follow the coloring.

@kumargaurav2596 Is this already included in any PR?

@ajdapretnar  I have implemented the "c1, c2, c3 ..." leveling but it gets really messy specially a the lower levels due to lack of space. Can we rethink of the naming style.

Can you show an example image?

![clustering](https://cloud.githubusercontent.com/assets/10504619/14710107/b3281ff6-07f1-11e6-9c5a-f943eb67646e.png)

Their isn't enough space to squeeze the levels in the lower branches of dendrogram. 

It wasn't meant like this. 
- If the user selects some clusters, mark them.
- If the user sets a threshold (e.g. by dragging the cut-off line), mark the resulting clusters.

We can assume that there would not be more that 99 clusters. If there are more (which does not make sense, though), don't show labels.

I like bigger labels -- at least in the picture at the beginning of the conversation they looked nice. If needed, use a larger font when there are <= 9 clusters, and smaller when there are <= 99.

Don't place the labels at the "roots" of subclusters, but above and to the left, like in the picture.

You can also assume that the labels won't appear at the far right.

sorry, I guess I misunderstood last time, I am working on it...

@kumargaurav2596 Is the fix perhaps ready? If you need any help, feel free to ask.

@janezd Is this still something we want implemented or did we decide not to go through with it?
We want this, but it's not trivial and it requires some designing (possibly with trial and error). This may not be the optimal test task for GSoC. 

Resolving patch:
diff --git a/Orange/widgets/unsupervised/owhierarchicalclustering.py b/Orange/widgets/unsupervised/owhierarchicalclustering.py
--- a/Orange/widgets/unsupervised/owhierarchicalclustering.py
+++ b/Orange/widgets/unsupervised/owhierarchicalclustering.py
@@ -10,8 +10,8 @@
 from AnyQt.QtWidgets import (
     QGraphicsWidget, QGraphicsObject, QGraphicsLinearLayout, QGraphicsPathItem,
     QGraphicsScene, QGraphicsView, QGridLayout, QFormLayout, QSizePolicy,
-    QGraphicsSimpleTextItem, QGraphicsLayoutItem, QAction, QComboBox
-)
+    QGraphicsSimpleTextItem, QGraphicsLayoutItem, QAction, QComboBox,
+    QGraphicsItemGroup)
 from AnyQt.QtGui import (
     QTransform, QPainterPath, QPainterPathStroker, QColor, QBrush, QPen,
     QFont, QFontMetrics, QPolygonF, QKeySequence
@@ -229,6 +229,42 @@ def boundingRect(self):
                 self.__boundingRect = sh.boundingRect().adjusted(-pw, -pw, pw, pw)
             return self.__boundingRect
 
+    class SelectionItem(QGraphicsItemGroup):
+        def __init__(self, parent, path, unscaled_path, label=""):
+            super().__init__(parent)
+            self.path = QGraphicsPathItem(path, self)
+            self.path.setPen(make_pen(width=1, cosmetic=True))
+            self.addToGroup(self.path)
+
+            self.label = QGraphicsSimpleTextItem(label)
+            self._update_label_pos()
+            self.addToGroup(self.label)
+
+            self.unscaled_path = unscaled_path
+
+        def set_path(self, path):
+            self.path.setPath(path)
+            self._update_label_pos()
+
+        def set_label(self, label):
+            self.label.setText(label)
+            self.label.setBrush(Qt.blue)
+            self._update_label_pos()
+
+        def set_color(self, color):
+            self.path.setBrush(QColor(color))
+
+        def _update_label_pos(self):
+            path = self.path.path()
+            elements = (path.elementAt(i) for i in range(path.elementCount()))
+            points = ((p.x, p.y) for p in elements)
+            p1, p2, *rest = sorted(points)
+            x, y = p1[0], (p1[1] + p2[1]) / 2
+            brect = self.label.boundingRect()
+            # leaf nodes' paths are 4 pixels higher; leafs are `len(rest) == 3`
+            self.label.setPos(x - brect.width() - 4,
+                              y - brect.height() + 4 * (len(rest) == 3))
+
     #: Orientation
     Left, Top, Right, Bottom = 1, 2, 3, 4
 
@@ -462,29 +498,30 @@ def select_item(self, item, state):
             self._re_enumerate_selections()
             self.selectionChanged.emit()
 
+    @staticmethod
+    def _create_path(item, path):
+        ppath = QPainterPath()
+        if item.node.is_leaf:
+            ppath.addRect(path.boundingRect().adjusted(-8, -4, 0, 4))
+        else:
+            ppath.addPolygon(path)
+            ppath = path_outline(ppath, width=-8)
+        return ppath
+
+
+    @staticmethod
+    def _create_label(i):
+        return f"C{i + 1}"
+
     def _add_selection(self, item):
         """Add selection rooted at item
         """
         outline = self._selection_poly(item)
-        selection_item = QGraphicsPathItem(self)
+        path = self._transform.map(outline)
+        ppath = self._create_path(item, path)
+        label = self._create_label(len(self._selection))
+        selection_item = self.SelectionItem(self, ppath, outline, label)
         selection_item.setPos(self.contentsRect().topLeft())
-        selection_item.setPen(make_pen(width=1, cosmetic=True))
-
-        transform = self._transform
-        path = transform.map(outline)
-        margin = 4
-
-        if item.node.is_leaf:
-            ppath = QPainterPath()
-            ppath.addRect(path.boundingRect()
-                          .adjusted(-margin, -margin, margin, margin))
-        else:
-            ppath = QPainterPath()
-            ppath.addPolygon(path)
-            ppath = path_outline(ppath, width=margin * 2,)
-
-        selection_item.setPath(ppath)
-        selection_item.unscaled_path = outline
         self._selection[item] = selection_item
 
     def _remove_selection(self, item):
@@ -531,9 +568,10 @@ def _re_enumerate_selections(self):
             # delete and then reinsert to update the ordering
             del self._selection[item]
             self._selection[item] = selection_item
+            selection_item.set_label(self._create_label(i))
             color = palette[i]
             color.setAlpha(150)
-            selection_item.setBrush(QColor(color))
+            selection_item.set_color(color)
 
     def _selection_poly(self, item):
         # type: (Tree) -> QPolygonF
@@ -589,15 +627,8 @@ def _update_selection_items(self):
         transform = self._transform
         for item, selection in self._selection.items():
             path = transform.map(selection.unscaled_path)
-            ppath = QPainterPath()
-            margin = 4
-            if item.node.is_leaf:
-                ppath.addRect(path.boundingRect()
-                              .adjusted(-margin, -margin, margin, margin))
-            else:
-                ppath.addPolygon(path)
-                ppath = path_outline(ppath, width=margin * 2)
-            selection.setPath(ppath)
+            ppath = self._create_path(item, path)
+            selection.set_path(ppath)
 
     def _relayout(self):
         if not self._root:
@@ -1766,7 +1797,8 @@ def __init__(self, parent=None, orientation=Qt.Vertical, value=0.0,
         super().__init__(parent, **kwargs)
 
         self.setAcceptedMouseButtons(Qt.LeftButton)
-        self.setPen(make_pen(brush=QColor(50, 50, 50), width=1, cosmetic=False))
+        self.setPen(make_pen(brush=QColor(50, 50, 50), width=1, cosmetic=False,
+                             style=Qt.DashLine))
 
         if self._orientation == Qt.Vertical:
             self.setCursor(Qt.SizeVerCursor)


Test patch: