Tree Drawing basics¶
Tree visualization is a fundamental feature of toytree
. Following our minimalist ethos, it is possible to generate a beautiful tree drawing very easily. But in addition, it is also possible to create complex and data rich visualizations using a variety of styling options.
import toytree
import numpy as np
# an example tree
tree = toytree.rtree.bdtree(ntips=6, seed=123)
Drawing class objects¶
When you call .draw()
on a tree it returns three objects, a Canvas
, a Cartesian
axes, and a Mark
. This follows the design principle of the toyplot
plotting library on which toytree is based. The Canvas
describes the plot space, and the Cartesian
coordinates define how to project Marks
onto that space. Marks
are used to represent data using SVG markers and shapes. One canvas can have multiple cartesian coordinates, and each cartesian axes can contain multiple marks.
It is often useful to capture these drawing objects as variables so that they can be reused to further edit or annotate drawings, and to save them. However, if you are working in a jupyter notebook and only intend the drawings to be embedded in the output cells, then you do not need to save the plots externally. Canvas objects automatically render in output cells when they are created (this option can be toggled in Global config.)
# returns the three drawing objects
tree.draw()
(<toyplot.canvas.Canvas at 0x7f0edbe44be0>, <toyplot.coordinates.Cartesian at 0x7f0edbf9d660>, <toytree.drawing.src.mark_toytree.ToyTreeMark at 0x7f0edbf6eb90>)
Throughout this documentation you will see many toytree
drawing commands end with a semicolon (;), which is a simple method to hide the returned objects from being displayed in the output cell.
# semicolon hides the returned objects, but drawing still autorenders
tree.draw();
# store the drawing objects as variables
canvas, axes, mark = tree.draw()
Builtin Tree Styles¶
There are innumerous ways to style ToyTree drawings by combining different arguments to the .draw
function. As a convenience, we also provide a number of pre-built tree styles that represent collections of style arguments that can be set using a single command.
# drawing with pre-built tree_styles
tree.draw(tree_style='s'); # simple-style
tree.draw(tree_style='c'); # coalescent-style
tree.draw(tree_style='r'); # R-style
tree.draw(tree_style='p'); # population-style
tree.draw(tree_style='d'); # dark-style
tree.draw(tree_style='o'); # umlaut-style
You can think of setting a tree_style
as changing the baseline style on top of which additional drawing arguments can still be added. For example, here we select the "dark" tree style, which changes the default edge and tip colors, and on top of this we can still specify a change to the layout, node marker type, node size, and/or other drawing options.
# tree_style is a baseline on top of which additional styles can be added
tree.draw(tree_style='d', layout='d', node_markers="s", node_sizes=8, height=250);
Creating Tree Styles¶
There are a number of ways to apply individual styles to tree drawings. Which method you use may depend on whether you intend to reuse a particular tree style many times, or just once. The most common method is described below as the "one-time" setting, in which you enter style arguments to the .draw()
function. You will see this used most common throughout this tutorial. However, it is also possible for users to create reusable styles similar to the builtin tree style types shown above. A final option is to modify style settings saved to the tree objects themselves. Each is demonstrated below.
One-time style setting¶
Use tab-completion or other methods to examine the documentation string of the draw function to see the options available for styling tree drawings.
tree.draw(
node_sizes=8, node_markers=">", node_mask=False,
edge_colors="darkmagenta", edge_style={"opacity": 0.3}, edge_type="c",
scale_bar=True,
);
Reusable style dict¶
You can create a custom reusable tree-style as a dictionary of key-value pairs specifying options to the draw function. To apply this to many tree drawings you can simply use variadics to expand the dictionary as a single argument to the draw function, as demonstrated below.
# define a re-usable style dictionary
mystyle = {
"layout": 'd',
"edge_type": 'p',
"edge_style": {
"stroke": "black",
"stroke-width": 2.5,
},
"tip_labels_align": True,
"tip_labels_colors": "darkcyan",
"tip_labels_style": {
"font-size": "15px"
},
"node_labels": False,
"node_sizes": 8,
"node_colors": "goldenrod",
"node_mask": False,
}
# apply the custom treestyle dict to a drawing
tree.draw(**mystyle);
Persistent style¶
Finally, you can modify the .style
settings of a ToyTree
object to modify its saved default drawing options. This can useful if you plan to visualize the same tree many times, or wish to save different trees with different style settings for easier comparison. For example, it can be used within a function to return a tree object that has a style setting saved to it which will highlight a particular feature or clade when visualized. This framework is also useful for learning which tree style options are available, since you can use tab-completion in an interactive environment to explore the different style options available. In this example, I first create a copy of our example tree object and save the new copy as stree. Then, I modify the the style settings of the stree object. Finally, we can simply call .draw()
to draw the tree using its saved tree style. (Note that if you enter a new tree_style
argument to the draw() function of this tree it will override the settings in the tree's .style
settings.)
# set a style that will persist on an individual tree object
stree = tree.copy()
stree.style.edge_colors = "darkcyan"
stree.style.edge_widths = 2.5
stree.style.node_mask = False
stree.style.node_sizes = 16
stree.style.node_markers = "s"
stree.style.node_style.fill_opacity = 0.5
stree.style.node_labels = "idx"
stree.style.tip_labels_style.font_size = 16
stree.style.tip_labels_style.anchor_shift = 25
stree.draw();
Drawing nodes¶
Plotting node values on a tree is a useful way of representing additional information about trees. See the Data/Features section for details on extracting data from tree objects to plot on nodes. Here we will focus on options available for styling nodes. Nodes are often used to show labels, support values, or trait data. They can convey information through variation in their marker shapes (e.g., circles, rectangles, pie-charts), colors, and size. Node markers in toytree
are represented by SVG shape objects for which a fill (color), fill-opacity, stroke (outline color), stroke-opacity, and stroke-width can be set.
Below is a complex example showing how
# hover over nodes to see pop-up elements
tree.draw(
node_sizes=18,
node_style={"fill-opacity": 0.75, "stroke": "white", "stroke-width": 2.5},
node_mask=(0, 1, 0),
node_labels="idx",
node_labels_style={"font-size": 14, "fill": "white"},
node_colors=("idx", "BlueRed", 6, 9),
node_markers="s",
node_hover=True,
);
node_sizes¶
In the default tree style node sizes are set to zero, meaning that they will not be shown. Node sizes are described in pixel units as floats or ints, which scale node marker sizes. A single value can be entered to apply to all nodes, or a sequence of values of length nnodes can be entered to scale nodes to different sizes. In this case, the values should be enetered in node idxorder, which is best done by extracting data from the tree object itself, as described in Data/Features. See also Range-Mapping for examples of scaling node sizes to data features.
# hover over nodes to see pop-up elements
tree.draw(node_sizes=12);
tree.draw(node_sizes=("height", 5, 20));
tree.draw(node_sizes=("height",));
node_mask¶
You will notice that the node_sizes
argument shown above only adds node markers to the internal nodes. What if we want to choose which nodes to display markers on? In that case, you will want to use the node_mask
argument. This accepts a boolean or array of boolean values to describe which nodes to show versus hide. There is a convenience function of tree objects named get_node_mask
that can be used to generate a boolean mask in the correct order to designate a subset of nodes to show. Finally, there is a simplified tuple syntax that can be used to choose to display particular sets of nodes composing only the tips, only the internal nodes, or only the root.
# mask=False reveals all nodes
tree.draw(node_mask=False, node_sizes=12);
# mask=True masks all nodes
tree.draw(node_mask=True, node_sizes=12);
# mask=[True, False, True, ...] shows Nodes with True
mask = tree.get_node_mask(show_tips=True, show_root=True, show_internal=False)
print(mask)
tree.draw(node_mask=mask, node_sizes=12);
[ True True True True True True False False False False True]
# mask=[True, False, True, ...] shows Nodes with True
mask = tree.get_node_mask("~r[0-5]")
print(mask)
tree.draw(node_mask=mask, node_sizes=12);
[ True True True True True True False False False False False]
# mask=[True, False, True, ...] shows a subset of Nodes
mask = tree.get_node_mask(2, 3, 7, 8)
print(mask)
tree.draw(node_mask=mask, node_sizes=15, node_labels="idx");
node_markers¶
Node markers are the shapes of the mark objects plotted on nodes. The default shape is a circle, but a variety of marker shapes are available and can be selected by the shorthand str names used for toyplot markers. For example, 's' for a square, 'o' for a circle, 'r1x5' for a rectangle that is 5 times taller than wide. Each marker shape is still scaled to a particular pixel size using the node_sizes
argument, and optionally shown or hidden using node_mask
. You can enter a single node marker argument to apply to all nodes uniformly, or a series of node markers of length nnodes to apply different markers shapes to different nodes.
# apply square markers to all nodes
tree.draw(node_sizes=10, node_markers="s");
# apply rectangle markers to each node
tree.draw(node_sizes=15, node_markers="r2x1");
# apply a rectangle marker with width scaled to n digits in data
rects = [f"r{len(str(i))}x1" for i in tree.get_node_data("idx")]
tree.draw(node_sizes=15, node_markers=rects, node_labels="idx");
# show
tree = tree.set_node_data("support", default=100)
tree[-1].support = np.nan
tree.draw(node_labels='support', node_sizes=18, node_markers='r2x1');
# ...
mask = tree.get_node_mask(2, 9, 10)
canvas, axes, mark = tree.draw();
tree.annotate.add_node_markers(axes=axes, marker="s", size=10, mask=mask);
node_colors¶
The fill color of nodes can be set in a variety of ways. The node_colors option can be used to set a single color to all nodes, or different colors to nodes. The colors can be entered manually, or they can be automatically projected from color map to data values. There is another option for setting a single color to all nodes, using node_style.fill. The node_colors argument overrides node_style.fill.
# set a single color to all nodes
tree.draw(node_colors='red', node_sizes=10);
# list of colors of length nnodes
colors = ['darkcyan'] * 6 + ['goldenrod'] * 5
tree.draw(node_colors=colors, node_sizes=10, node_mask=False);
Automatically color nodes by projecting a data feature that is saved to tree object, such as the node heights, using color-mapping.
# colormapping the 'height' feature
tree.draw(node_colors='height', node_sizes=10, node_mask=False);
Global config¶
The default visualization settings in toytree are inherited from toyplot. This includes the default usage of html format for displaying plots in a notebook, and the default behavior of automatically rendering Canvas objects in a notebook cell when they are created. Both of these options can be changed in the config
settings of the toyplot library.
import toyplot
# set config options to new settings
toyplot.config.autoformat = "png"
toyplot.config.autorender = False
# embed a PNG drawing in the notebook
canvas, axes, mark = tree.draw();
# it will only display here b/c we return the Canvas
canvas
# set config options back to their defaults
toyplot.config.autoformat = "html"
toyplot.config.autorender = True