399
447
def _draw_node_label(self, renderer, label_style):
400
text = label_style.getNodeLabel(self.edge)
401
(x, ha, y, va) = self.getLabelCoordinates(text, renderer)
402
return [renderer.string(x, y, text, ha=ha, va=va)]
405
class _RootedDendrogramForm(object):
406
"""A rooted dendrogram form defines how lengths get mapped to X and Y coodinates.
408
_RootedDendrogramStyle subclasses provide yCoords and xCoords, which examine
448
text = label_style.getNodeLabel(self)
449
color = self.NameColor
450
(x, ha, y, va) = self.getLabelCoordinates(text, renderer)
451
return [renderer.string(x, y, text, ha=ha, va=va, color=color)]
453
def _draw_collapsed_clade(self, renderer, label_style):
454
text = label_style.getNodeLabel(self)
455
color = _first_non_none([self.CladeColor, self.Color, 'black'])
456
icolor = 'white' if sum(to_rgb(color))/3 < 0.5 else 'black'
458
if not self.Children:
460
(l,r,t,b), vertices = self.wedgeVertices()
461
g.append(renderer.polygon(vertices, color))
462
if not b <= self.y2 <= t:
463
# ShelvedDendrogram needs this extra line segment
464
g.append(renderer.line(self.x2, self.y2, self.x2, b, self))
465
(x, ha, y, va) = self.getLabelCoordinates(text, renderer)
466
g.append(renderer.string(
467
(self.x2+r)/2, (t+b)/2, str(self.leafcount), ha=ha, va=va,
469
g.append(renderer.string(
470
x-self.x2+r, y, text, ha=ha, va=va, color=self.NameColor))
473
def setCollapsed(self, collapsed=True, label=None, color=None):
474
if color is not None:
475
self.CladeColor = color
476
if label is not None:
478
self.Collapsed = collapsed
481
class Dimensions(object):
482
def __init__(self, xscale, yscale, total_tree_height):
485
self.height = total_tree_height
488
class _RootedDendrogram(_Dendrogram):
489
"""_RootedDendrogram subclasses provide yCoords and xCoords, which examine
409
490
attributes of a node (its length, coodinates of its children) and return
410
491
a tuple for start/end of the line representing the edge."""
411
aspect_distorts_lengths = True
413
def __init__(self, tree, width, height):
415
self.yscale = height / self.widthRequiredFor(tree)
416
self.xscale = width / tree.height
417
self.total_tree_height = tree.height
419
def widthRequiredFor(self, node):
420
return node.leafcount * self.yscale
422
def xCoords(self, node, x1):
423
raise NotImplementedError
425
def yCoords(self, node, x1):
426
raise NotImplementedError
492
def labelMargins(self, label_width):
493
return (0, label_width)
495
def widthRequired(self):
496
return self.leafcount
498
def xCoords(self, scale, x1):
499
raise NotImplementedError
501
def yCoords(self, scale, y1):
502
raise NotImplementedError
504
def updateCoordinates(self, width, height):
505
xscale = width / self.height
506
yscale = height / self.widthRequired()
507
scale = Dimensions(xscale, yscale, self.height)
509
# y coords done postorder, x preorder, y first.
510
# so it has to be done in 2 passes.
511
self.update_y_coordinates(scale)
512
self.update_x_coordinates(scale)
515
def update_y_coordinates(self, scale, y1=None):
516
"""The second pass through the tree. Y coordinates only
517
depend on the shape of the tree and yscale"""
519
y1 = self.widthRequired() * scale.y
521
for child in self.Children:
522
child.update_y_coordinates(scale, child_y)
523
child_y -= child.widthRequired() * scale.y
524
(self.y1, self.y2) = self.yCoords(scale, y1)
526
def update_x_coordinates(self, scale, x1=0):
527
"""For non 'square' styles the x coordinates will depend
528
(a bit) on the y coodinates, so they should be done first"""
529
(self.x1, self.x2) = self.xCoords(scale, x1)
530
for child in self.Children:
531
child.update_x_coordinates(scale, self.x2)
533
def getLabelCoordinates(self, text, renderer):
534
return (self.x2+renderer.labelPadDistance, 'left', self.y2, 'center')
429
class SquareDendrogramForm(_RootedDendrogramForm):
536
class SquareDendrogram(_RootedDendrogram):
430
537
aspect_distorts_lengths = False
432
def yCoords(self, node, y1):
433
cys = [c.y1 for c in node.children]
539
def yCoords(self, scale, y1):
540
cys = [c.y1 for c in self.Children]
435
542
y2 = (cys[0]+cys[-1]) / 2.0
437
y2 = y1 - self.yscale / 2.0
544
y2 = y1 - 0.5 * scale.y
440
def xCoords(self, node, x1):
441
dx = self.xscale * node.length
547
def xCoords(self, scale, x1):
548
dx = scale.x * self.length
446
class ContemporaneousDendrogramForm(SquareDendrogramForm):
447
def xCoords(self, node, x1):
448
return (x1, (self.total_tree_height-(node.height-node.length))*self.xscale)
451
class ShelvedDendrogramForm(ContemporaneousDendrogramForm):
452
def widthRequiredFor(self, node):
453
return node.edgecount * self.yscale
455
def yCoords(self, node, y1):
456
cys = [c.y1 for c in node.children]
458
y2 = cys[-1] - 1.0 * self.yscale
460
y2 = y1 - 0.5 * self.yscale
464
class AlignedShelvedDendrogramForm(ShelvedDendrogramForm):
465
def __init__(self, tree, width, height):
467
self.xscale = width / tree.height
468
self.total_tree_height = tree.height
470
def yCoords(self, node, y1):
471
if hasattr(node, 'track_y'):
472
return (node.track_y, node.track_y)
474
raise RuntimeError, node.edge.Name
475
return ShelvedDendrogramForm.yCoords(self, node, y1)
477
def widthRequiredFor(self, node):
478
raise RuntimeError, node.edge.Name
481
class StraightDendrogramForm(_RootedDendrogramForm):
482
def yCoords(self, node, y1):
552
def wedgeVertices(self):
553
tip_ys = [(c.y2 + self.y2)/2 for c in self.iterTips()]
554
t,b = max(tip_ys), min(tip_ys)
555
cxs = [c.x2 for c in self.iterTips()]
556
l,r = min(cxs), max(cxs)
557
return (l,r,t,b), [(self.x2, b), (self.x2, t), (l, t), (r, b)]
560
class StraightDendrogram(_RootedDendrogram):
561
def yCoords(self, scale, y1):
483
562
# has a side effect of adjusting the child y1's to meet nodes' y2's
484
cys = [c.y1 for c in node.children]
563
cys = [c.y1 for c in self.Children]
486
565
y2 = (cys[0]+cys[-1]) / 2.0
487
distances = [child.length for child in node.children]
488
closest_child = node.children[distances.index(min(distances))]
566
distances = [child.length for child in self.Children]
567
closest_child = self.Children[distances.index(min(distances))]
489
568
dy = closest_child.y1 - y2
490
max_dy = 0.8*max(5, closest_child.length*self.xscale)
569
max_dy = 0.8*max(5, closest_child.length*scale.x)
491
570
if abs(dy) > max_dy:
492
# 'moved', node.edge.Name, y2, 'to within', max_dy,
493
# 'of', closest_child.edge.Name, closest_child.y1
571
# 'moved', node.Name, y2, 'to within', max_dy,
572
# 'of', closest_child.Name, closest_child.y1
494
573
y2 = closest_child.y1 - _sign(dy) * max_dy
496
y2 = y1 - self.yscale / 2.0
575
y2 = y1 - scale.y / 2.0
498
for child in node.children:
577
for child in self.Children:
502
def xCoords(self, node, x1):
503
dx = node.length * self.xscale
504
dy = node.y2 - node.y1
581
def xCoords(self, scale, x1):
582
dx = self.length * scale.x
583
dy = self.y2 - self.y1
505
584
dx = numpy.sqrt(max(dx**2 - dy**2, 1))
506
585
return (x1, x1 + dx)
509
class ContemporaneousStraightDendrogramForm(StraightDendrogramForm):
510
def xCoords(self, node, x1):
511
return (x1, (self.total_tree_height-(node.height-node.length))*self.xscale)
514
class _RootedDendrogram(_Dendrogram):
515
def updateCoordinates(self, width, height):
516
if width < self.labelwidth:
517
raise ValueError('%spt not wide enough for %spt wide labels' %
518
(width, self.labelwidth))
519
width -= self.labelwidth
521
form = self.FormClass(self, width, height)
523
# y coords done postorder, x preorder, y first.
524
# so it has to be done in 2 passes.
525
self.update_y_coordinates(form)
526
self.update_x_coordinates(form)
529
def update_y_coordinates(self, style, y1=None):
530
"""The second pass through the tree. Y coordinates only
531
depend on the shape of the tree and yscale"""
533
y1 = style.widthRequiredFor(self)
535
for child in self.children:
536
child.update_y_coordinates(style, child_y)
537
child_y -= style.widthRequiredFor(child)
538
(self.y1, self.y2) = style.yCoords(self, y1)
540
def update_x_coordinates(self, style, x1=0):
541
"""For non 'square' styles the x coordinates will depend
542
(a bit) on the y coodinates, so they should be done first"""
543
(self.x1, self.x2) = style.xCoords(self, x1)
544
for child in self.children:
545
child.update_x_coordinates(style, self.x2)
547
def getLabelCoordinates(self, text, renderer):
548
return (self.x2+renderer.labelPadDistance, 'left', self.y2, 'center')
550
class SquareDendrogram(_RootedDendrogram):
551
FormClass = SquareDendrogramForm
552
aspect_distorts_lengths = FormClass.aspect_distorts_lengths
553
use_lengths_default = True
555
class StraightDendrogram(_RootedDendrogram):
556
FormClass = StraightDendrogramForm
557
aspect_distorts_lengths = FormClass.aspect_distorts_lengths
558
use_lengths_default = True
560
class ContemporaneousDendrogram(_RootedDendrogram):
561
FormClass = ContemporaneousDendrogramForm
562
aspect_distorts_lengths = FormClass.aspect_distorts_lengths
563
use_lengths_default = False
565
class ShelvedDendrogram(_RootedDendrogram):
566
FormClass = ShelvedDendrogramForm
567
aspect_distorts_lengths = FormClass.aspect_distorts_lengths
568
use_lengths_default = False
570
class AlignedShelvedDendrogram(_RootedDendrogram):
571
FormClass = AlignedShelvedDendrogramForm
572
aspect_distorts_lengths = FormClass.aspect_distorts_lengths
573
use_lengths_default = False
575
def update_y_coordinates(self, style, y1=None):
576
"""The second pass through the tree. Y coordinates only
577
depend on the shape of the tree and yscale"""
578
for child in self.children:
579
child.update_y_coordinates(style, None)
580
(self.y1, self.y2) = style.yCoords(self, None)
582
class ContemporaneousStraightDendrogram(_RootedDendrogram):
583
FormClass = ContemporaneousStraightDendrogramForm
584
aspect_distorts_lengths = FormClass.aspect_distorts_lengths
585
use_lengths_default = False
587
def wedgeVertices(self):
588
tip_ys = [(c.y2 + self.y2)/2 for c in self.iterTips()]
589
t,b = max(tip_ys), min(tip_ys)
590
cxs = [c.x2 for c in self.iterTips()]
591
l,r = min(cxs), max(cxs)
592
vertices = [(self.x2, self.y2), (l, t), (r, b)]
593
return (l,r,t,b), vertices
595
class _ContemporaneousMixin(object):
596
"""A dendrogram with all of the tips lined up.
597
Tidy but not suitable for displaying evolutionary distances accurately"""
599
# Overrides init to change default for use_lengths
600
def __init__(self, edge, use_lengths=False):
601
super(_ContemporaneousMixin, self).__init__(edge, use_lengths)
603
def xCoords(self, scale, x1):
604
return (x1, (scale.height-(self.height-self.length))*scale.x)
606
class ContemporaneousDendrogram(_ContemporaneousMixin, SquareDendrogram):
609
class ContemporaneousStraightDendrogram(_ContemporaneousMixin, StraightDendrogram):
613
class ShelvedDendrogram(ContemporaneousDendrogram):
614
"""A dendrogram in which internal nodes also get a row to themselves"""
615
def widthRequired(self):
616
return self.edgecount # as opposed to tipcount
618
def yCoords(self, scale, y1):
619
cys = [c.y1 for c in self.Children]
621
y2 = cys[-1] - 1.0 * scale.y
623
y2 = y1 - 0.5 * scale.y
626
class AlignedShelvedDendrogram(ShelvedDendrogram):
628
def update_y_coordinates(self, scale, y1=None):
629
"""The second pass through the tree. Y coordinates only
630
depend on the shape of the tree and yscale"""
631
for child in self.Children:
632
child.update_y_coordinates(scale, None)
633
(self.y1, self.y2) = self.yCoords(scale, None)
635
def yCoords(self, scale, y1):
636
if hasattr(self, 'track_y'):
637
return (self.track_y, self.track_y)
639
raise RuntimeError, self.Name
587
642
class UnrootedDendrogram(_Dendrogram):
588
use_lengths_default = True
589
643
aspect_distorts_lengths = True
645
def labelMargins(self, label_width):
646
return (label_width, label_width)
648
def wedgeVertices(self):
649
tip_dists = [(c.depth-self.depth)*self.scale for c in self.iterTips()]
650
(near, far) = (min(tip_dists), max(tip_dists))
651
a = self.angle - 0.25 * self.wedge
652
(x1, y1) = (self.x2+near*numpy.sin(a), self.y2+near*numpy.cos(a))
653
a = self.angle + 0.25 * self.wedge
654
(x2, y2) = (self.x2+far*numpy.sin(a), self.y2+far*numpy.cos(a))
655
vertices = [(self.x2, self.y2), (x1, y1), (x2, y2)]
656
return (self.x2, (x1+x2)/2, self.y2, (y1+y2)/2), vertices
591
658
def updateCoordinates(self, width, height):
592
if width < 2*self.labelwidth:
593
raise ValueError('%spt not wide enough for %spt wide labels' %
594
(width, self.labelwidth))
595
width -= 2*self.labelwidth
597
659
angle = 2*numpy.pi / self.leafcount
598
660
# this loop is a horrible brute force hack
599
661
# there are better (but complex) ways to find