moshi4/phyTreeViz

rotate the tree 180 degree

Closed this issue · 7 comments

Dear developer,

Thank you for your previous suggestion. I discovered that phyTreeViz is a useful tool that can integrate Matplotlib components. Currently, I'm developing a function to create a tangled tree. To accomplish this, I first need to mirror the tree so that it faces left (i.e., rotate the tree 180 degrees). The second step is to obtain the x and y coordinates of the leaf labels in the mirrored tree. Do you have any suggested code for achieving this?

The resulting tree should resemble the following:

image

Sincerely,
Dong

There is no functionality in phyTreeViz to do what you want to do.
I have a feeling it would not be difficult to implement, so I will consider implementing it when I feel like it.

I implemented the orientation functionality you suggested in newly released v0.2.0. It was easier than I thought it would be.

Code Example

from phytreeviz import TreeViz, load_example_tree_file

# Plot tree in `left` orientation
tree_file = load_example_tree_file("medium_example.nwk")
tv = TreeViz(tree_file, orientation="left")
tv.savefig("example.png")

# x and y coordinates of the leaf labels?
# No further coordinate information exists in phyTreeViz
for leaf_label in tv.leaf_labels:
    leaf_line_tip_xy = tv.name2xy[leaf_label]
    rect_for_highlight = tv.name2rect[leaf_label]
    print(leaf_label, leaf_line_tip_xy, rect_for_highlight)

example.png
example

Dear @moshi4 ,

Thank you very much for your update! The code to mirror a tree is quite elegant. However, I encountered an issue while attempting to create a tanglegram using the following code:

from phytreeviz import TreeViz, load_example_tree_file
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec

tree_file = load_example_tree_file("small_example.nwk")
tree_file2 = load_example_tree_file("small_example.nwk")

tv1 = TreeViz(tree_file, orientation="right")
tv2 = TreeViz(tree_file2, orientation="left")

fig = plt.figure(figsize=(9, 7))
gs = GridSpec(1, 2, width_ratios=[1,1])
ax1 = fig.add_subplot(gs[0,0])
ax2 = fig.add_subplot(gs[0,1])
tv1.plotfig(ax=ax1)
tv2.plotfig(ax=ax2)

fig.tight_layout()

plt.subplots_adjust(wspace=0, hspace=0)

Unfortunately, it seems that the labels of the two trees are overlapping:
image

Upon investigation, I discovered that this is due to the x-limit (xlim) of the axis extending only from the root to the furthest node, instead of encompassing the entire width of the tree including the labels. To address this, I attempted to extend the xlim to the farthest label using the code below:

from phytreeviz import TreeViz


class PS_Treeviz(TreeViz):

    def __init__(
            self,
            tree_data: str | Path | Tree,  # type: ignore
            # invert_: bool = False,
            orientation: str = "right",
            *,
            format: str = "newick",
            height: float = 0.5,
            width: float = 8,
            align_leaf_label: bool = False,
            ignore_branch_length: bool = False,
            leaf_label_size: float = 12,
            innode_label_size: float = 0,
            show_auto_innode_label: bool = True,
            leaf_label_xmargin_ratio: float = 0.01,
            innode_label_xmargin_ratio: float = 0.01,
            reverse: bool = False,
    ):
        super(PS_Treeviz, self).__init__(
            tree_data,
            orientation=orientation,
            format=format,
            height=height,
            width=width,
            align_leaf_label=align_leaf_label,
            ignore_branch_length=ignore_branch_length,
            leaf_label_size=leaf_label_size,
            innode_label_size=innode_label_size,
            show_auto_innode_label=show_auto_innode_label,
            leaf_label_xmargin_ratio=leaf_label_xmargin_ratio,
            innode_label_xmargin_ratio=innode_label_xmargin_ratio,
            reverse=reverse,
        )
        # for setting xlim
        self.tree_lengths: list = [self.max_tree_depth]

    @property
    def xlim(self) -> tuple[float, float]:
        """Axes xlim"""
        if self._orientation == "left":
            return (max(self.tree_lengths), 0)
        else:
            return (0, max(self.tree_lengths))

    def _plot_node_label(self, ax: Axes) -> None:
        """Plot tree node label

        Parameters
        ----------
        ax : Axes
            Matplotlib axes for plotting
        """
        node: Clade
        self.tree_lengths: list = [self.max_tree_depth]
        for node in self.tree.find_clades():
            # Get label x, y position
            x, y = self.name2xy[str(node.name)]
            # Get label size & xmargin
            if node.is_terminal():
                label_size = self._leaf_label_size
                label_xmargin_ratio = self._leaf_label_xmargin_ratio
            else:
                label_size = self._innode_label_size
                label_xmargin_ratio = self._innode_label_xmargin_ratio
            label_xmargin = self.max_tree_depth * label_xmargin_ratio
            # Set label x position with margin
            if node.is_terminal() and self._align_leaf_label:
                x = self.max_tree_depth + label_xmargin
            else:
                x += label_xmargin
            # Skip if 'label is auto set name' or 'no label size'
            if label_size <= 0:
                continue
            is_auto_innode_label = node.name in self._auto_innode_labels
            if not self._show_auto_innode_label and is_auto_innode_label:
                continue
            # Plot label
            text_kws = dict(size=label_size, ha="left", va="center_baseline")
            text_kws.update(self._node2label_props[str(node.name)])
            if self._orientation == "left":
                text_kws.update(ha="right")

            ax_text = ax.text(x, y, s=node.name, **text_kws)
            trans_bbox = self.ax.transData.inverted().transform_bbox(ax_text.get_window_extent())
            text_length = trans_bbox.width # trans_bbox.xmax - trans_bbox.xmin

            # get toot to text width
            if node.is_terminal():
                # text_length = self._get_texts_rect(node.name).get_width()
                length = x + text_length
                self.tree_lengths.append(length)
        self._init_axes(self.ax)    

tree_file = load_example_tree_file("small_example.nwk")
tree_file2 = load_example_tree_file("small_example.nwk")

tv1 = PS_Treeviz(tree_file, orientation="right")
tv2 = PS_Treeviz(tree_file2, orientation="left")

fig = plt.figure(figsize=(9, 7))
gs = GridSpec(1, 2, width_ratios=[1,1])
ax1 = fig.add_subplot(gs[0,0]) 
ax2 = fig.add_subplot(gs[0,1]) 

tv1.plotfig(ax=ax1)
tv2.plotfig(ax=ax2)

fig.tight_layout()

plt.subplots_adjust(wspace=0, hspace=0)

However, some overlaps persist:
image

It's worth noting that extending the xlim in this manner could also enhance our ability to draw alignment figures alongside the tree (related to #1 ssue). Interestingly, my modified code works seamlessly when there's only one tree:

tree_file = load_example_tree_file("small_example.nwk")

tv = PS_Treeviz(tree_file, orientation='right')
tv.show_branch_length(color="red")
tv.show_confidence(color="blue")
tv.show_scale_bar()

fig = plt.figure(figsize=(9, 7))

gs = GridSpec(1, 3, width_ratios=[4, 1, 1])
ax1 = fig.add_subplot(gs[0,0]) 
ax2 = fig.add_subplot(gs[0,1]) 
ax3 = fig.add_subplot(gs[0,2]) 
ax1.sharey(ax2)
ax2.sharey(ax3)
ax2.axis('off') 
ax2.grid(False)  
ax3.axis('off') 
ax3.grid(False)

def draw_piechart(ax, labels, sizes, center, radius, start_angle):
    total = sum(sizes)
    theta1 = start_angle
    for size in sizes:
        theta2 = theta1 + (size / total) * 360.0
        wedge = Wedge(center, radius, theta1, theta2, edgecolor=ColorCycler(), facecolor=ColorCycler())  
        ax.add_patch(wedge)
        theta1 = theta2

tv.plotfig(ax=ax1)
ax1.grid(True)  

categories = [1, 2, 3, 4, 5, 6, 7]
values = [25, 40, 30, 20, 40, 10, 30]
ax3.barh(categories, values, color='skyblue')  

labels = ['Category A', 'Category B', 'Category C', 'Category D']
sizes = [25, 30, 20, 25]
radius = 0.3
ax2.set_xlim(0, radius*2) 
start_angle = 0
ax2.set_aspect('equal', adjustable='box')
for leaf in tv.leaf_labels:
    x, y = tv.name2xy_center[leaf]
    center = (0.3, y)
    draw_piechart(ax2, labels, sizes, center, radius, start_angle)

x_total = tv.xlim[1] - tv.xlim[0]
y_total = tv.ylim[1] - tv.ylim[0]
in_axs_wh = 0.1 
radius = 0.5 
for node in tv.innode_labels:
    x, y = tv.name2xy_center[node]
    center = (0.5,0.5)
    x_ = (x-in_axs_wh/2)/x_total 
    y_ = y/y_total
    in_axs = ax1.inset_axes([x_,y_,in_axs_wh,in_axs_wh]) 
    in_axs.axis('off') 
    in_axs.grid(False)
    in_axs.set_aspect('equal', adjustable='box')
    draw_piechart(in_axs, labels, sizes, center, radius, start_angle)

fig.tight_layout() 

plt.subplots_adjust(wspace=0, hspace=0)

image

Do you have any insights into why the code works for one tree but fails for two trees? Additionally, do you have any suggestions for achieving the desired outcome more effectively?

On a side note, it appears that the scale bar is not displaying correctly in my figure. I'm uncertain about the cause of this issue.

Thank you for your patience and assistance!

Sincerely,

Dong

I think what you want to do is a challenging task in using matplotlib. With patchworklib, you may possibly be able to solve your problem.

Code Example1

pip install patchworklib

from phytreeviz import TreeViz, load_example_tree_file
import patchworklib as pw

tree_file = load_example_tree_file("small_example.nwk")
tv1 = TreeViz(tree_file, orientation="right")
tv1.highlight(["Homo_sapiens", "Pan_paniscus"], color="salmon")
tv2 = TreeViz(tree_file, orientation="left")
tv2.highlight(["Hylobates_moloch", "Nomascus_leucogenys"], color="skyblue")

ax1 = pw.Brick(figsize=(3, 7))
ax2 = pw.Brick(figsize=(3, 7))

tv1.plotfig(ax=ax1)
tv2.plotfig(ax=ax2)

ax12 = ax1 | ax2

ax12.savefig("example1.png")

example1.png

example

Code Example2

from phytreeviz import TreeViz, load_example_tree_file
import patchworklib as pw
from matplotlib.patches import Wedge
import random
random.seed(0)

# Plot tree (ax1)
tree_file = load_example_tree_file("small_example.nwk")
tv = TreeViz(tree_file, orientation="right")
tv.highlight(["Homo_sapiens", "Pan_paniscus"], color="salmon")
tv.show_branch_length(color="red")
tv.show_confidence(color="blue")
tv.show_scale_bar()

ax1 = pw.Brick(figsize=(3, 7))
tv.plotfig(ax=ax1)

# Plot piechart (ax2)
def draw_piechart(ax, sizes, center, radius):
    total = sum(sizes)
    theta1 = 0
    colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
    for idx, size in enumerate(sizes):
        theta2 = theta1 + (size / total) * 360.0
        wedge = Wedge(center, radius, theta1, theta2, edgecolor=colors[idx], facecolor=colors[idx])
        ax.add_patch(wedge)
        theta1 = theta2

ax2 = pw.Brick(figsize=(1, 7))
ax2.set_xlim(0, 1)
ax2.set_ylim(*tv.ylim)
ax2.set_axis_off()
ax2.set_aspect(aspect=1)
for i in range(len(tv.leaf_labels)):
    draw_piechart(ax2, [10, 20, 30, 40], center=(0.5, i + 1), radius=0.35)

# Plot bar (ax3)
ax3 = pw.Brick(figsize=(3, 7))
ax3.set_ylim(*tv.ylim)
ax3.set_axis_off()
y = list(range(1, len(tv.leaf_labels) + 1))
x = [random.randint(1, 10) for _ in range(len(tv.leaf_labels))]
ax3.barh(y, x, color="skyblue")

# Concatenate axes
ax123 = ax1 | ax2 | ax3

ax123.savefig("example2.png")

example2.png

example2

I can't advise you on anything more than this, as it is a difficult problem for me as well.
If you want to do more than this, you should use other libraries like ete or ggtree.

Dear @moshi4,

Thank you for providing the suggested codes; they resolved my problem. I'm also delighted to discover patchworklib—it's a fantastic package and has been quite helpful.

If you want to do more than this, you should use other libraries like ete or ggtree.
As I'm in the process of integrating a Python package for phylogenetic tree annotation into PhyloSuite (https://github.com/dongzhang0725/PhyloSuite), a platform we've designed for molecular phylogenetic analysis, ggtree may not be suitable for our purposes. Regarding ete3, despite investing significant effort in exploring it and integrating it into PhyloSuite, I encountered its somewhat complex logical structure and unresolved bugs. Additionally, its circular tree function performed poorly, leading me to abandon it. I prefer phyTreeViz and pycirclize due to their relatively simple logical structures and their ability to integrate with Matplotlib's figure elements. Consequently, I've decided to integrate them into PhyloSuite for tree annotation. Once we've made substantial progress, I'd be delighted to share the code with you and extend an invitation for you to participate in this project, provided you're willing to do so.

A quick overview of the fundamental work for phyTreeViz we have completed:
20240130225530
We will cite phyTreeViz and pycirclize in the GUI in future updates.

One of my previous questions remains unresolved: On a side note, it appears that the scale bar is not displaying correctly in my figure. I'm uncertain about the cause of this issue..

I've noticed that in your code, there is a scale bar when using tv.show_scale_bar(). However, in my version, the same code fails to display the scale bar. I'm uncertain about the cause of this discrepancy.

One of my previous questions remains unresolved: On a side note, it appears that the scale bar is not displaying correctly in my figure. I'm uncertain about the cause of this issue..

I've noticed that in your code, there is a scale bar when using tv.show_scale_bar(). However, in my version, the same code fails to display the scale bar. I'm uncertain about the cause of this discrepancy.

Your use of phyTreeViz is out of the basic usage originally expected.
Therefore, it is difficult to advise you on the cause of the discrepancy you pointed out.

Thank you @moshi4 , I discovered that the issue was caused by the size_vertical parameter of AnchoredSizeBar. When I set it to 0.1, I was able to see the bar.

image