Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 43 additions & 17 deletions Orange/widgets/unsupervised/owhierarchicalclustering.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fractions

from collections import namedtuple, OrderedDict
from itertools import chain
from contextlib import contextmanager
Expand Down Expand Up @@ -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)
Expand All @@ -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]),
Expand Down Expand Up @@ -372,22 +374,30 @@ 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
height scale).
"""
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)
Expand Down Expand Up @@ -635,19 +645,33 @@ 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]
item.element = geom
# 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)
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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())