eaton-lab / toytree

A minimalist tree plotting library using toyplot graphs
http://eaton-lab.org/toytree
BSD 3-Clause "New" or "Revised" License
165 stars 28 forks source link

allow missing tips in draw_cloud_tree()? #80

Open ryneches opened 4 months ago

ryneches commented 4 months ago

I'm interested in visualizing gene tree distributions over taxa with a high rate of gene gain and loss. So, the set of tips in each of the gene trees is not the same. In principle, it should be possible to draw a cloud tree over the union tips in the mtree.

eaton-lab commented 4 months ago

Hi @ryneches ,

Yes it is possible, but not currently supported using the draw_cloud_tree function. Here is how to do it manually.

First, let's create six variable trees and draw them individually:

# the full tree
tree = toytree.rtree.unittree(6, seed=123)

# a tree with same tips but different topology and ladderization
subtree = tree.mod.prune("r2", "r3")
base = tree.mod.drop_tips("r2", "r3")
tree2 = toytree.mod.add_internal_node_and_subtree(base, "r1", subtree=subtree)
tree2.set_node_data("dist", {"r2": 0.1, "r3": 0.1}, inplace=True)
toytree.mod.edges_extend_tips_to_align(tree2, inplace=True)

# a set of trees with one missing tip
trees = [tree.mod.drop_tips(i) for i in range(4)]
mtree = toytree.mtree([tree, tree2] + trees)
mtree.draw((2, 3));

image

Then we'll skip over the cloud tree function...

## this func raises an error if trees have different tips
# mtree.draw_cloud_tree();

And instead, we will create a cloud tree drawing manually. To get tips to align we will use the tree drawing argument fixed_position. First, let's create a dict mapping tip names to vertical positions.

# assign a vertical position for each tip
tip_pos = dict(zip(tree.get_tip_labels(), range(6)))
# {'r0': 0, 'r1': 1, 'r2': 2, 'r3': 3, 'r4': 4, 'r5': 5}

Then, we create a canvas and plot multiple tree drawings on top of each other. I added some optional arguments and their explanations.

# instead, draw cloud tree manually
canvas = toytree.core.Canvas(width=400, height=400)
axes = canvas.cartesian(yshow=False)
for tidx, tre in enumerate(mtree):
    tre.draw(
        axes=axes, 
        edge_type='c', 
        edge_style={"stroke-opacity": 0.2, "stroke-width": 2.5},
        fixed_position=[tip_pos[i] for i in tre.get_tip_labels()],  # this assigns tips to vertical positions
        tip_labels=(True if not tidx else False), # uncomment to only print tip labels once.
        edge_colors=toytree.color.COLORS2[tidx], # uncomment to color each tree differently
    );

image

ryneches commented 3 months ago

Beautiful! I was working on exactly this, and you saved me a bunch of time.

Are there plans to implement support for mtrees with different tips? If that's not going to happen for a while, would you like me to flesh out this workaround for the documentation?

eaton-lab commented 2 months ago

Following up on the plan to implement this generically.

In the example above I selected one tree that has the full set of tips to use for setting the tip positions. However, the input tree set may not always include a tree that has the complete set of tips. So we need a more flexible automated method for ordering the full set of tips.

The current default is to create a Consensus tree from the input tree set, however, the consensus tree function does not currently support trees with different sets of tips. So, improving that function seems like the simplest and most appropriate solution, which will also improve consensus tree methods.