diff --git a/Orange/widgets/unsupervised/owhierarchicalclustering.py b/Orange/widgets/unsupervised/owhierarchicalclustering.py index ef690cf80b0..cdb601fb772 100644 --- a/Orange/widgets/unsupervised/owhierarchicalclustering.py +++ b/Orange/widgets/unsupervised/owhierarchicalclustering.py @@ -1,3 +1,5 @@ +import fractions + from collections import namedtuple, OrderedDict from itertools import chain from contextlib import contextmanager @@ -102,12 +104,12 @@ def path_toQtPath(geom): Left, Top, Right, Bottom = 1, 2, 3, 4 -def dendrogram_path(tree, orientation=Left): +def dendrogram_path(tree, orientation=Left, scaleh=1): layout = dendrogram_layout(tree) T = {} paths = {} rootdata = tree.value - base = rootdata.height + base = scaleh * rootdata.height if orientation == Bottom: transform = lambda x, y: (x, y) @@ -126,10 +128,10 @@ def dendrogram_path(tree, orientation=Left): else: left, right = paths[node.left], paths[node.right] lines = (left.anchor, - Point(*transform(start, node.value.height)), - Point(*transform(end, node.value.height)), + Point(*transform(start, scaleh * node.value.height)), + Point(*transform(end, scaleh * node.value.height)), right.anchor) - anchor = Point(*transform(center, node.value.height)) + anchor = Point(*transform(center, scaleh * node.value.height)) paths[node] = Element(anchor, lines) T[node] = Tree((node, paths[node]), @@ -372,11 +374,18 @@ def height_at(self, point): height = tpoint.x() else: height = tpoint.y() - + # Undo geometry prescaling + base = self._root.value.height + scale = self._height_scale_factor() + # Use better better precision then double provides. + Fr = fractions.Fraction + if scale > 0: + height = Fr(height) / Fr(scale) + else: + height = 0 if self.orientation in [self.Left, self.Bottom]: - base = self._root.value.height - height = base - height - return height + height = Fr(base) - Fr(height) + return float(height) def pos_at_height(self, height): """Return a point in local coordinates for `height` (in cluster @@ -384,10 +393,11 @@ def pos_at_height(self, height): """ if not self._root: return QPointF() - + scale = self._height_scale_factor() + base = self._root.value.height + height = scale * height if self.orientation in [self.Left, self.Bottom]: - base = self._root.value.height - height = base - height + height = scale * base - height if self.orientation in [self.Left, self.Right]: p = QPointF(height, 0) @@ -635,11 +645,26 @@ def _update_selection_items(self): ppath = self._create_path(item, path) selection.set_path(ppath) + def _height_scale_factor(self): + # Internal dendrogram height scale factor. The dendrogram geometry is + # scaled by this factor to better condition the geometry + if self._root is None: + return 1 + base = self._root.value.height + # implicitly scale the geometry to 0..1 scale or flush to 0 for fuzz + if base >= np.finfo(base).eps: + return 1 / base + else: + return 0 + def _relayout(self): - if not self._root: + if self._root is None: return - self._layout = dendrogram_path(self._root, self.orientation) + scale = self._height_scale_factor() + base = scale * self._root.value.height + self._layout = dendrogram_path(self._root, self.orientation, + scaleh=scale) for node_geom in postorder(self._layout): node, geom = node_geom.value item = self._items[node] @@ -647,7 +672,6 @@ def _relayout(self): # the untransformed source path item.sourcePath = path_toQtPath(geom) r = item.sourcePath.boundingRect() - base = self._root.value.height if self.orientation == Left: r.setRight(base) @@ -668,12 +692,14 @@ def _rescale(self): if self._root is None: return + scale = self._height_scale_factor() + base = scale * self._root.value.height crect = self.contentsRect() leaf_count = len(list(leaves(self._root))) if self.orientation in [Left, Right]: - drect = QSizeF(self._root.value.height, leaf_count) + drect = QSizeF(base, leaf_count) else: - drect = QSizeF(leaf_count, self._root.value.height) + drect = QSizeF(leaf_count, base) eps = np.finfo(np.float64).eps diff --git a/Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py b/Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py index 61f1d36196f..5056b3c8f95 100644 --- a/Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py +++ b/Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py @@ -5,14 +5,17 @@ import numpy as np from AnyQt.QtCore import QPoint, Qt +from AnyQt.QtWidgets import QGraphicsScene, QGraphicsView from AnyQt.QtTest import QTest +from orangewidget.tests.base import GuiTest import Orange.misc +from Orange.clustering import hierarchical from Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable from Orange.distance import Euclidean from Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin from Orange.widgets.unsupervised.owhierarchicalclustering import \ - OWHierarchicalClustering + OWHierarchicalClustering, DendrogramWidget class TestOWHierarchicalClustering(WidgetTest, WidgetOutputsTestMixin): @@ -170,3 +173,54 @@ def test_restore_state(self): self.send_signal(w.Inputs.distances, self.distances, widget=w) ids_2 = self.get_output(w.Outputs.selected_data, widget=w).ids self.assertSequenceEqual(list(ids_1), list(ids_2)) + + +class TestDendrogramWidget(GuiTest): + def setUp(self) -> None: + super().setUp() + self.scene = QGraphicsScene() + self.view = QGraphicsView(self.scene) + self.widget = DendrogramWidget() + self.scene.addItem(self.widget) + + def tearDown(self) -> None: + self.scene.clear() + del self.widget + del self.view + super().tearDown() + + def test_widget(self): + w = self.widget + + T = hierarchical.Tree + C = hierarchical.ClusterData + S = hierarchical.SingletonData + + def t(h: float, left: T, right: T): + return T(C((left.value.first, right.value.last), h), (left, right)) + + def leaf(r, index): + return T(S((r, r + 1), 0.0, index)) + + T = hierarchical.Tree + + w.set_root(t(0.0, leaf(0, 0), leaf(1, 1))) + w.resize(w.effectiveSizeHint(Qt.PreferredSize)) + h = w.height_at(QPoint()) + self.assertEqual(h, 0) + h = w.height_at(QPoint(10, 0)) + self.assertEqual(h, 0) + + self.assertEqual(w.pos_at_height(0).x(), w.rect().x()) + self.assertEqual(w.pos_at_height(1).x(), w.rect().x()) + + height = np.finfo(float).eps + w.set_root(t(height, leaf(0, 0), leaf(1, 1))) + + h = w.height_at(QPoint()) + self.assertEqual(h, height) + h = w.height_at(QPoint(w.size().width(), 0)) + self.assertEqual(h, 0) + + self.assertEqual(w.pos_at_height(0).x(), w.rect().right()) + self.assertEqual(w.pos_at_height(height).x(), w.rect().left())