Plotting Networks#

This tutorial walks through plotting phylogenetic networks step by step, using the Xiphophorus swordfish reticulate phylogeny from [Holtgrefe et al., 2025] as a running example: loading the rooted network, previewing it in the terminal, plotting it with the PhyloZoo layouts, converting it to a semi-directed network and plotting that, and finally styling and saving a publication-ready figure.

Prerequisites: phylozoo[viz] installed (pip install phylozoo[viz]).

Loading the Network#

The network is given as an eNewick string; loading it as a DirectedPhyNetwork keeps the root of the string:

from phylozoo import DirectedPhyNetwork

NEWICK = (
    "((Ps.jonesii,((((X.evelynae,(X.xiphidium,(X.maculatus)#H1)),"
    "(((X.meyeri,X.couchianus),X.gordoni),X.milleri)),X.variatus),"
    "((#H1,((((((X.multilineatus,X.nigrensis),(X.pygmaeus,X.continens)),"
    "((X.malinche,X.birchmanni),(X.cortezi)#H2)),(X.montezumae,"
    "(#H2,X.nezahualcoyotl))),((X.signum,(X.hellerii,(X.mayae,"
    "X.alvarezi))))#H3),(#H3,(X.clemenciae,X.monticolus)))),X.andersi))));"
)

dnet = DirectedPhyNetwork.from_string(NEWICK, format="enewick")
print(dnet.number_of_nodes(), "nodes,", len(dnet.taxa), "taxa,", len(dnet.hybrid_nodes), "hybrid nodes")
# 56 nodes, 25 taxa, 3 hybrid nodes

Taxon names containing spaces must be replaced with underscores or dots (the parser does not accept quoted names). Back-references like #H1 link the two incoming edges of a hybrid node.

Previewing in the Terminal#

Before plotting anything, preview() draws the network as text, root on the left and leaves on the right. It needs no matplotlib:

dnet.preview()
      ┌─────────────────────────────────────────────────────────────────────────────● Ps.jonesii
      │
      │           ┌─────────────────────────────────────────────────────────────────● X.andersi
      │           │
      │           │                                                           ┌─────● X.multilineatus
      │           │                                                     ┌─────●
      │           │                                                     │     └─────● X.nigrensis
      │           │                                               ┌─────●
      │           │                                               │     │     ┌─────● X.continens
      │           │                                               │     └─────●
      │           │                                               │           └─────● X.pygmaeus
      │           │                                         ┌─────●
      │           │                                         │     │           ┌─────● X.birchmanni
      │           │                                         │     │     ┌─────●
      │     ┌─────●                                         │     │     │     └─────● X.malinche
○─────●     │     │                                   ┌─────●     └─────●
      │     │     │                                   │     │           └┄┄┄┄>◆─────● X.cortezi
      │     │     │                                   │     │                 ^
      │     │     │                                   │     │     ┌───────────●─────● X.nezahualcoyotl
      │     │     │                                   │     └─────●
      │     │     │                                   │           └─────────────────● X.montezumae
      │     │     │                             ┌─────●
      │     │     │                             │     ┊                       ┌─────● X.alvarezi
      │     │     │                             │     ┊                 ┌─────●
      │     │     │                             │     ┊           ┌─────●     └─────● X.mayae
      │     │     │                             │     ┊           │     │
      │     │     └───────────────────────●─────●     └┄┄┄┄>◆─────●     └───────────● X.hellerii
      │     │                             ┊     │           ^     │
      │     │                             ┊     │           ┊     └─────────────────● X.signum
      └─────●                             ┊     │           ┊
            │                             ┊     │           ┊     ┌─────────────────● X.clemenciae
            │                             ┊     └───────────●─────●
            │                             ┊                       └─────────────────● X.monticolus
            │                             v
            │                       ┌┄┄┄┄>◆─────────────────────────────────────────● X.maculatus
            │                 ┌─────●
            │           ┌─────●     └───────────────────────────────────────────────● X.xiphidium
            │           │     │
            │           │     └─────────────────────────────────────────────────────● X.evelynae
            │           │
            │     ┌─────●                 ┌─────────────────────────────────────────● X.couchianus
            │     │     │           ┌─────●
            │     │     │     ┌─────●     └─────────────────────────────────────────● X.meyeri
            │     │     │     │     │
            └─────●     └─────●     └───────────────────────────────────────────────● X.gordoni
                  │           │
                  │           └─────────────────────────────────────────────────────● X.milleri
                  │
                  └─────────────────────────────────────────────────────────────────● X.variatus

Solid lines are tree edges, dotted lines with an arrowhead are hybrid edges, ◆ is a hybrid node and ○ the root.

Plotting a Directed Network#

All plotting goes through plot(), which picks a default layout per network class; pass layout= to choose another one and layout options as keyword arguments. In every drawing hybrid nodes are pink and hybrid edges red with an arrowhead at the hybrid node.

pz-cladogram#

The default layout for directed networks is a rectangular layered drawing with the root on the left and the leaves aligned in a column on the right, with horizontal labels, which suits long taxon names. Every node hangs off one of its parents, sibling subtrees stay together, and where the network allows it the parent of a hybrid edge is lowered onto the hybrid’s layer so the edge is horizontal. Only hybrid edges carry an arrowhead, since the direction is implied by the drawing.

from phylozoo.viz import plot

ax = plot(dnet)
pz-cladogram layout

plot(dnet): the default pz-cladogram layout.#

direction="TD" puts the root at the top and the leaves aligned at the bottom (rectangular=False gives straight edges in either direction):

ax = plot(dnet, direction="TD")
pz-cladogram layout, top down

pz-cladogram with direction="TD".#

pz-layered#

A layered layout in the style of Graphviz dot: nodes of a layer are ordered freely (no tree backbone), which gives fewer crossings on heavily reticulate networks at the cost of sibling subtrees not staying together. Leaves keep their own depth by default; align_leaves=True puts them on the bottom row:

ax = plot(dnet, layout="pz-layered", align_leaves=True)
pz-layered layout

pz-layered with align_leaves=True.#

pz-radial#

A circular cladogram: root at the centre, leaves evenly spaced on the outer circle, hybrid edges as chords:

ax = plot(dnet, layout="pz-radial")
pz-radial layout of the directed network

pz-radial.#

The unrooted pz-unrooted layout of the next section can be used for directed networks as well; the root is then an ordinary node, marked in gold. All layouts are deterministic; their options and the generic NetworkX and Graphviz layouts are listed in the plotting manual.

Plotting a Semi-Directed Network#

Converting to a Semi-Directed Network#

A SemiDirectedPhyNetwork forgets the root: the hybrid edges keep their direction, every other edge becomes undirected. Convert the rooted network with to_sd_network() (an eNewick string can also be loaded as a semi-directed network directly with SemiDirectedPhyNetwork.from_string):

from phylozoo.core.network.dnetwork.derivations import to_sd_network

sd = to_sd_network(dnet)
print(sd.number_of_nodes(), "nodes,", len(sd.taxa), "taxa,", len(sd.hybrid_nodes), "hybrid nodes")
# 54 nodes, 25 taxa, 3 hybrid nodes

The two nodes fewer are the root and its now-suppressed neighbour.

pz-unrooted#

The default layout for semi-directed networks draws the tree of blobs first and each blob (the reticulate parts of the network) as a polygon in the space reserved for it, so cut edges never cross blob edges; a short relaxation then spreads the drawing. Leaf labels read outwards along their pendant edge.

ax = plot(sd)
Semi-directed Xiphophorus network, pz-unrooted layout

plot(sd): the default pz-unrooted layout.#

pz-radial is available for semi-directed networks too (the network is rooted first, root_location chooses where), and sd.preview() works like the directed one.

Customising the Style#

Pass a style object to control colours, node sizes and label placement: SDNetStyle for semi-directed networks, DNetStyle for directed ones (same attributes, plus arrows and the root options):

from phylozoo.viz.sdnetwork.style import SDNetStyle

style = SDNetStyle(
    node_size=60,               # internal node size
    leaf_size=140,              # leaf node size (overrides node_size for leaves)
    node_color="#dddddd",       # internal node fill
    leaf_color="#1f4e79",       # leaf fill
    hybrid_color="#f5b041",     # hybrid node fill
    edge_color="#555555",       # tree edges
    hybrid_edge_color="#d35400",  # hybrid edges
    edge_width=1.6,
    label_offset=0.012,         # distance from node centre to label
    label_font_size=11,
)
ax = plot(sd, style=style)
Semi-directed phylogenetic network of Xiphophorus swordfish species

Semi-directed phylogenetic network of 25 Xiphophorus species with three hybridisation events (H1–H3) [Holtgrefe et al., 2025], with the style above.#

Key style attributes:

  • node_size / leaf_size — size of node circles (arbitrary units; 100 ≈ small dot).

  • node_color / leaf_color / hybrid_color — any matplotlib colour string.

  • edge_color / hybrid_edge_color / edge_width — edge colours (defaults "gray" and "red") and width.

  • label_offset — label distance from node centre in layout coordinates.

  • label_font_size — matplotlib font size.

  • label_rotation — None (default) aligns each label with its edge; a fixed angle such as 0 keeps all labels horizontal, placed beside the node on its outward side.

  • arrows (directed only) — 'all', 'hybrid' or 'none'; None (default) draws arrowheads on hybrid edges only in the layered and radial layouts and on all edges otherwise.

Saving to File#

To save the figure above, pass an existing axes to embed the plot in a figure, then save:

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(10, 7))
plot(sd, ax=ax, style=style)
fig.savefig("xiphophorus.png", dpi=150, bbox_inches="tight", pad_inches=0.05)

Use bbox_inches="tight" to crop whitespace and pad_inches to control the remaining margin. For a publication-quality PDF, replace .png with .pdf.

See Also#