Annotating tree drawings¶
The .annotate
subpackage offers a simple way to add additional drawing Marks to an existing toytree drawing. This provides an alternative syntax to add node or tip labels to a drawing, outside of the ToyTree draw
function, but also includes many additional methods for adding complex Marks to highlight parts of a tree drawing, or represent additional data on trees, such as text, markers, pie charts, histograms, and barplots.
Recall that you can use the general purpose plotting library toyplot
to combine almost any type of plot with a toytree drawing. But that requires a learning curve. The toytree.annotate
module instead provides many simple functions for achieving simple but flexible drawing operations. If you are interested in a particular type of annotation that we do not yet support, please reach out and raise a ticket on Github at https://github.com/eaton-lab/toytree/discussions.
import toytree
# a tree to use for examples
tree = toytree.rtree.unittree(8, seed=123)
Context¶
When adding annotations (additional markers) to a tree drawing, it is necessary to provide the context of how the tree is positioned and oriented in the drawing space. This information is provided by the Cartesian
axes object that is returned when you create a toytree drawing, as one of the three returned objects (Canvas, Cartesian, Mark). You can store this object to a variable (I usually name it "axes") and then pass it to the annotation method. This allows the annotation drawing functions to know where the edges and nodes of the tree drawing are positioned, and thus additional marks can be drawn on the tree without having to specify their positions manually. Below is an example, where we add tip markers to the tree drawing as an annotation.
# store the objects returned from a drawing
canvas, axes, mark1 = tree.draw()
# call annotate method w/ 'axes' as an arg
mark2 = tree.annotate.add_tip_markers(axes=axes, size=8, color="red", marker="o");
Module and Tree-level APIs¶
You can access methods from the annotate
module from either the toytree.annotate
module, or from ToyTree
objects directly. These are simply two different syntax style options. In the former case you must provide the ToyTree as an argument, whereas in the latter the tree itself is known from the context.
# draw a tree
canvas, axes, m0 = tree.draw()
# add tip markers using module-level annotate API
toytree.annotate.add_tip_markers(tree, axes, color="blue")
# add node markers using tree-level annotate API
tree.annotate.add_node_markers(axes, color="orange");
Data annotations¶
Annotations are commonly used to display data on trees, such as support values on edges of a tree and trait values on the leaf or internal nodes of a tree. As discussed in our documentation section about tree Data, it is important to be careful when entering data manually to ensure the correct data is assigned to the correct Nodes in a tree. This is often best accomplished by assigning data to the Nodes of a tree directly, and then extracting the data in the correct plotting order using get_node_data()
, or selecting the feature by name within a drawing function. Below is an example annotation function where we select the "height" and "dist" features of Nodes directly from the tree, and use toytree's "tuple syntax" to indicate that these values should be color mapped and range mapped, respectively, to translate them to colors and appropriate ranges of values for plotting.
# draw the tree
canvas, axes, m0 = tree.draw(tip_labels=False)
# add markers to all nodes
m2 = tree.annotate.add_node_markers(axes, mask=False, color=("height", "Spectral"), size=("dist", 5, 15))
Text/Label annotations¶
Text can be added to plots using add_node_labels
, add_edge_labels
, or add_tip_labels
. These are particularly useful for adding names or metrics mapped to particular parts of a tree, from which the labels can then be further styled and shifted in position. These three functions target different sets of nodes in a tree. In reality, you can accomplish all three methods using just the add_node_labels
method while providing an optional mask
argument to subselect which nodes to target. However, the add_tip_labels
method provides a slightly faster and simpler way to target only the tip nodes, and add_edge_labels
to target the internal nodes - 1 (i.e., the edges). Edge labels are placed at the midpoint of an edge by default, but this can be modified with optional args.
Note that feature to be mapped is selected by providing the feature name to the labels
arg here (e.g., "height", "idx"). This is a simple way to extract the feature data directly from the ToyTree object. Alternatively, the labels arg can be passed in as a collection of values of the appropriate length (e.g., ntips, nnodes, or nedges).
# draw the tree without tip labels initially
canvas, axes, m0 = tree.draw(tip_labels=False)
# add name label to each leaf
m1 = tree.annotate.add_tip_labels(axes, labels="name", color="red", xshift=10)
# add height label to each node
m2 = tree.annotate.add_node_labels(axes, labels="height", color="purple", font_size=10, xshift=18)
# add idx label above each edge
m3 = tree.annotate.add_edge_labels(axes, labels="idx", color="blue", yshift=10, mask=False)
Marker annotations¶
Markers (shapes) can be added to nodes or edges of a tree, or positioned relative to these, as in the case of showing markers near the tips of a tree to display trait values. Below we demonstrate add_tip_markers
to add markers either at the tips or extending to the right past the name labels to show trait values. We also show add_node_markers
and add_edge_markers
which work similar to the similar methods above for adding labels to these positions of a tree.
# draw the tree
canvas, axes, m0 = tree.draw()
# add markers to one or more leaf nodes
m1 = tree.annotate.add_tip_markers(axes, marker="o", size=10, color="red")
# add markers to the right of each leaf node colored by 'dist' feature
m2 = tree.annotate.add_tip_markers(axes, marker="s", size=14, color=("dist",), xshift=45)
# add markers to one or more nodes
m3 = tree.annotate.add_node_markers(axes, marker="^", size=14, color="blue")
# add markers one or more edges
m4 = tree.annotate.add_edge_markers(axes, marker="s", size=10, color="purple", mask=False)
Combining annotations¶
You can layer annotations on top of each other. For example, it may often be useful to draw markers on nodes or edges to serve as a container or background for labels. Here edge markers and edge labels are combined to label each of the edges in the tree.
# draw tree with unrooted layout
canvas, axes, m0 = tree.draw()
# add rectangle markers to edges
m1 = tree.annotate.add_edge_markers(axes, marker="r2x1", size=15, color="lightgrey", mask=False)
# add labels on top of edge markers
m2 = tree.annotate.add_edge_labels(axes, labels="idx", font_size=10, mask=False)
Using a mask¶
The mask
argument can be used to select a subset of nodes or edges to add annotations to. This argument accepts True, False, or a boolean array of size nnodes (or optionally of size nedges or ntips for those associated functions). There is a convenience function of ToyTree objects named get_node_mask
that provides an easy way to generate a boolean array in node idx order containing True or False for each Node. This can be used to select to include all tip nodes, all internal nodes, and/or the root node, and also to select nodes individually by name, regular expression, or idx label. Below we show several examples.
# draw tree with unrooted layout
canvas, axes, m0 = tree.draw()
# default: adds markers to internal nodes only
m1 = tree.annotate.add_node_markers(axes, color="red")
# mask=False: add markers to all nodes
m2 = tree.annotate.add_node_markers(axes, color="blue", mask=False, xshift=8)
# mask=boolean array: add markers to subset of nodes
mask = tree.get_node_mask(8, 9, 10)
m3 = tree.annotate.add_node_markers(axes, color="yellow", mask=mask, xshift=-8)
# mask=boolean array: add a single marker and shift position by n pixels
mask = tree.get_node_mask(5)
m4 = tree.annotate.add_node_markers(axes, color="orange", mask=mask, xshift=-35, size=15)
Axes annotations¶
The annotation
module also includes several methods for annotating or styling the axes of a drawing. [Still in development] These are convenience functions. Remember that you also have the full suite of options from the toyplot library at your disposal to further modify and style the axes of a drawing.
# ...
add_node_bars¶
The add_node_bars
method can be used to add barplots spanning from a min to max value from a node. This is typically used to show uncertainty in node ages, and can be extracted from the feature data stored in the extended newick file output by a phylogenetic inference program for inferring a time tree. As you can see in the example below, if the tree is rotated the bars will also be rotated appropriately.
# draw tree with unrooted layout
canvas, axes, m0 = tree.draw(layout='d')
# create new features to serve as min, max of trait
tree.set_node_data("min_b", [i.height * 0.5 for i in tree], inplace=True)
tree.set_node_data("max_b", [i.height * 1.5 for i in tree], inplace=True)
# add bars at nodes
tree.annotate.add_node_bars(
axes=axes,
bar_min="min_b",
bar_max="max_b",
size=0.33,
color='purple',
opacity=0.4,
);
Pie charts¶
Pie charts are useful for showing categorical data on a tree. You can easily create pie charts using the add_node_pie_charts
or add_edge_pie_charts
methods. Here each value mapped to a node should a sequence of float values that sum to 1.
# draw tree with unrooted layout
canvas, axes, m0 = tree.draw()
# generate random pie-like (proportion) data array
import numpy as np
ncategories = 3
arr = np.random.random(size=(tree.nnodes, ncategories))
arr = (arr.T / arr.sum(axis=1)).T
print(arr)
# add pie charts to all internal Nodes
tree.annotate.add_node_pie_charts(
axes=axes,
data=arr,
size=18,
istroke_width=0.75,
istroke="black",
);
[[0.36180951 0.45225489 0.18593559] [0.44447308 0.01321342 0.5423135 ] [0.19423139 0.44688936 0.35887925] [0.39241973 0.1865384 0.42104187] [0.38266124 0.41678049 0.20055826] [0.74203503 0.1523099 0.10565506] [0.3349279 0.50101126 0.16406084] [0.26827169 0.30950003 0.42222828] [0.42639113 0.15933704 0.41427182] [0.49071719 0.18627327 0.32300954] [0.03971906 0.75128547 0.20899547]]