Skip to content

Commit

Permalink
Fix issues with circular layout #568.
Browse files Browse the repository at this point in the history
This fix addresses #568
by using a slightly modified version of the PR that @luederm
created for ete3 in #569 .

Thanks @luederm !
  • Loading branch information
jordibc committed Oct 3, 2023
1 parent ef7e005 commit 9f7c136
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 6 deletions.
3 changes: 3 additions & 0 deletions ete4/treeview/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ class TreeStyle:
Draws an extra line (dotted by default) to complete branch
lengths when the space to cover is larger than the branch
itself.
:param False pack_leaves: If True, in circular layouts pull leaf
nodes closer to center while avoiding collisions.
:param 2 extra_branch_line_type: 0=solid, 1=dashed, 2=dotted
:param "gray" extra_branch_line_color: RGB code or name in
:data:`SVG_COLORS`
Expand Down Expand Up @@ -376,6 +378,7 @@ def __init__(self):
# branch length, branch line can be completed. Also, when
# circular trees are drawn,
self.complete_branch_lines_when_necessary = True
self.pack_leaves = False
self.extra_branch_line_type = 2 # 0 solid, 1 dashed, 2 dotted
self.extra_branch_line_color = "gray"

Expand Down
93 changes: 90 additions & 3 deletions ete4/treeview/qt_circular_render.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import math
import colorsys
import itertools

from .qt import *
from .main import _leaf, tracktime
Expand Down Expand Up @@ -110,6 +111,77 @@ def clockwise(a):
def paint(self, painter, option, index):
return QGraphicsPathItem.paint(self, painter, option, index)


def pack_nodes(n2i):
"""Shorten extra branch length to bring nodes closer to center."""
max_r = max_x = min_x = max_y = min_y = 0.0
for node in sorted(n2i, key=lambda x: n2i[x].radius): # Move closer items first
item = n2i[node]
# QGraphicsRectItem(item.nodeRegion, item.content)
if node.is_leaf and item.extra_branch_line and item.extra_branch_line.line().dx() > 0:
itemBoundingPoly = item.content.mapToScene(item.nodeRegion)

intersecting_polys = []
for other_item in n2i.values():
if item != other_item:
otherItemBoundingPoly = other_item.content.mapToScene(other_item.nodeRegion)
if itemBoundingPoly.intersects(otherItemBoundingPoly):
for part in itertools.chain(other_item.static_items, other_item.movable_items):
intersecting_poly = part.mapToScene(part.boundingRect())
intersecting_polys.append(intersecting_poly)

def has_intersecting_poly():
for movable in item.movable_items:
mpoly = movable.mapToScene(movable.boundingRect())
for intersector in intersecting_polys:
if mpoly.intersects(intersector):
return True
return False

def move_node_towards_center(amount: int):
# Update extra branch length
old_line = item.extra_branch_line.line()
item.extra_branch_line.setLine(old_line.x1(), old_line.y1(), math.floor(old_line.x2() - amount), old_line.y1())

# Move items closer
for movable in item.movable_items:
movable.moveBy(-amount, 0)
item.nodeRegion.setWidth(item.nodeRegion.width() - amount)
item.radius -= amount

def binary_search_for_position(low: int, high: int):
if high <= low:
return

mid = (high + low) // 2
move_by = math.ceil(item.extra_branch_line.line().dx()) - mid
move_node_towards_center(move_by)

if has_intersecting_poly():
move_node_towards_center(-move_by)
binary_search_for_position(mid+1, high)
else:
binary_search_for_position(low, mid)

binary_search_for_position(0, math.floor(item.extra_branch_line.line().dx()))

# Update bounding values
if max_r < item.radius:
max_r = item.radius
for movable in item.movable_items:
b_rect = movable.sceneBoundingRect()
if b_rect.top() < min_y:
min_y = b_rect.top()
if b_rect.bottom() > max_y:
max_y = b_rect.bottom()
if b_rect.left() < min_x:
min_x = b_rect.left()
if b_rect.right() > max_x:
max_x = b_rect.right()

return max_r, min_y, max_y, min_x, max_x


def rotate_and_displace(item, rotation, height, offset):
""" Rotates an item of a given height over its own left most edis and moves
the item offset units in the rotated x axis """
Expand Down Expand Up @@ -151,8 +223,8 @@ def get_min_radius(w, h, angle, xoffset):

return r, off

def render_circular(root_node, n2i, rot_step):
max_r = 0.0
def render_circular(root_node, n2i, rot_step, pack_leaves):
max_r = max_x = min_x = max_y = min_y = 0.0
for node in root_node.traverse('preorder', is_leaf_fn=_leaf):
item = n2i[node]
w = sum(item.widths[1:5])
Expand Down Expand Up @@ -213,9 +285,24 @@ def render_circular(root_node, n2i, rot_step):
for i in item.movable_items:
i.moveBy(xoffset, 0)

if not pack_leaves:
for qt_item in itertools.chain(item.static_items, item.movable_items):
b_rect = qt_item.sceneBoundingRect()
if b_rect.top() < min_y:
min_y = b_rect.top()
if b_rect.bottom() > max_y:
max_y = b_rect.bottom()
if b_rect.left() < min_x:
min_x = b_rect.left()
if b_rect.right() > max_x:
max_x = b_rect.right()

if pack_leaves:
max_r, min_y, max_y, min_x, max_x = pack_nodes(n2i)

n2i[root_node].max_r = max_r
return max_r

return max_r, min_y, max_y, min_x, max_x

def init_circular_leaf_item(node, n2i, n2f, last_rotation, rot_step):
item = n2i[node]
Expand Down
15 changes: 12 additions & 3 deletions ete4/treeview/qt_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,17 @@ def render(root_node, img, hide_root=False):
# Adjust content to rect or circular layout
mainRect = parent.rect()

if mode == "c":
tree_radius = crender.render_circular(root_node, n2i, rot_step)
mainRect.adjust(-tree_radius, -tree_radius, tree_radius, tree_radius)
if mode == 'c':
tree_radius, min_y, max_y, min_x, max_x = crender.render_circular(root_node, n2i, rot_step, img.pack_leaves)

# If semicircle and cropping removes at least 25% of mainRect's area, crop
full_circle_area = (tree_radius * 2) ** 2
cropped_area = (max_x - min_x) * (max_y - min_y)

if arc_span < 359 and (cropped_area / full_circle_area) < 0.75:
mainRect.adjust(min_x, min_y, max_x, max_y)
else:
mainRect.adjust(-tree_radius, -tree_radius, tree_radius, tree_radius)
else:
iwidth = n2i[root_node].fullRegion.width()
iheight = n2i[root_node].fullRegion.height()
Expand Down Expand Up @@ -644,6 +652,7 @@ def render_node_content(node, n2i, n2f, img):
extra_line.setPen(pen)
else:
extra_line = None
item.extra_branch_line = extra_line

# Attach branch-right faces to child
fblock_r = n2f[node]["branch-right"]
Expand Down

0 comments on commit 9f7c136

Please sign in to comment.