_images/toyplot.png

Graph Visualization

Overview

Toyplot now includes support for visualizing graphs - in the mathematical sense of vertices connected by edges - using the toyplot.coordinates.Cartesian.graph() and toyplot.graph() functions. As we will see, graph visualizations combine many of the aspects and properties of line plots (for drawing the edges), scatterplots (for drawing the vertices), and text (for drawing labels).

At a minimum, a graph can be specified as a collection of edges. For example, consider a trivial social network:

sources = ["Tim", "Tim", "Fred", "Janet"]
targets = ["Fred", "Janet", "Janet", "Pam"]

... here, we have specified a sequence of source (start) vertices and target (end) vertices for each edge in the graph, which we can pass directly to Toyplot for rendering:

import toyplot
toyplot.graph(sources, targets, width=300);
FredJanetPamTim

Simple as it is, Toyplot had to perform many steps to arrive at this figure:

  • We specified a set of edges as input, and Toyplot induced a set of unique vertices from them.
  • Used a layout algorithm to calculate coordinates for each vertex.
  • Rendered the vertices.
  • Rendered a set of vertex labels.
  • Rendered an edge (line) between each pair of connected vertices.

We will examine each of these concepts in depth over the course of this guide.

Inputs

At a minimum, you must specify the edges in a graph to create a visualization. In the above example, we specified a sequence of edge sources and a sequence of edge targets. We could also specify the edges as a numpy matrix (2D array) containing a column of sources and a column of targets:

import numpy
edges = numpy.array([["Tim", "Fred"], ["Tim", "Janet"], ["Fred", "Janet"], ["Janet", "Pam"]])
toyplot.graph(edges, width=300);
FredJanetPamTim

In either case, Toyplot creates (induces) vertices using the edge source / target values. Specifically, the source / target values are used as vertex identifiers, with a vertex created for each unique identifier. Note that vertex identifiers don’t have to be strings, as in the following example:

edges = numpy.array([[0, 1], [0, 2], [1, 2], [2, 3]])
toyplot.graph(edges, width=300);
0123

Inducing vertices from edge data is sufficient for many problems, but there may be occaisions when your graph contains disconnected vertices without any edge connections. For this case, you may specify an optional collection of extra vertex identifiers to add to your graph:

extra_vertices=[10]
toyplot.graph(edges, extra_vertices, width=300);
012310

Layout Algorithms

The next step in rendering a graph is using a layout algorithm to determine the locations of the vertices and routing of edges. Graph layout is an active area of research and there are many competing ideas about what constitutes a good layout, so Toyplot provides a variety of layouts to meet individual needs. By default, graphs are layed-out using the classic force-directed layout of Fruchterman and Reingold:

import toyplot.generate
edges = toyplot.generate.barabasi_albert_graph()
toyplot.graph(edges, width=500);
01234567891011121314151617181920212223242526272829

To explicitly specify the layout, use the toyplot.layout module:

import toyplot.layout
layout = toyplot.layout.FruchtermanReingold()
toyplot.graph(edges, layout=layout, width=500);
01234567891011121314151617181920212223242526272829

Note that by default most layouts produce straight-line edges, but this can be overridden by supplying an alternate edge-layout algorithm:

layout = toyplot.layout.FruchtermanReingold(edges=toyplot.layout.CurvedEdges())
toyplot.graph(edges, layout=layout, width=500);
01234567891011121314151617181920212223242526272829

If your graph is a tree, there are also tree-specific layouts to choose from:

numpy.random.seed(1234)
edges = toyplot.generate.prufer_tree(numpy.random.choice(4, 12))
layout = toyplot.layout.Buchheim()
toyplot.graph(edges, layout=layout, width=500, height=200);
012345678910111213

When computing a layout, Toyplot doesn’t have to compute the coordinates for every vertex ... you can explicitly specify some or all of the coordinates yourself. To do so, you can pass a matrix containing X and Y coordinates for the vertices you want to control, that is masked everywhere. Suppose we rendered our tree with the default force directed layout:

toyplot.graph(edges, width=500);
012345678910111213

... but we want to force vertices 0, 1, and 3 to lie on the X axis:

vcoordinates = numpy.ma.masked_all((14, 2)) # We know in advance there are 14 vertices
vcoordinates[0] = (-1, 0)
vcoordinates[1] = (0, 0)
vcoordinates[3] = (1, 0)

toyplot.graph(edges, vcoordinates=vcoordinates, width=500);
012345678910111213

Note that we’ve “pinned” our three vertices of interest, and the layout algorithm has placed the other vertices around them as normal. This is particularly useful when there are vertices of special significance that we wish to place explicitly, either to steer the layout, or to work with a narrative flow.

Keep in mind that we aren’t limited to explicitly constraining both coordinates for a vertex. For example, if we had some other per-vertex variable that we wanted to use for the visualization, we might map it to the X axis:

numpy.random.seed(1234)
data = numpy.random.uniform(0, 1, size=14)

vcoordinates = numpy.ma.masked_all((14, 2))
vcoordinates[:,0] = data

canvas, axes, mark = toyplot.graph(edges, vcoordinates=vcoordinates, width=500)
axes.show = True
axes.aspect = None
axes.y.show = False
0123456789101112130.20.50.81.0

Now, the X coordinate of every vertex is constrained, while the force-directed layout places just the Y coordinates.

Vertex Rendering

As you might expect, you can treat graph vertices as a single series of markers for rendering purposes. For example, you could specify a custom vertex color, marker, size, and label style:

edges = toyplot.generate.barabasi_albert_graph()
layout = toyplot.layout.FruchtermanReingold(edges=toyplot.layout.CurvedEdges())
vlstyle = {"fill":"white"}

toyplot.graph(edges, layout=layout, vcolor="steelblue", vmarker="d", vsize=18, vlstyle=vlstyle, width=500);
01234567891011121314151617181920212223242526272829

Of course, you can assign a \([0, N)\) colormap to the vertices based on their index, or some other variable:

colormap = toyplot.color.LinearMap(toyplot.color.Palette(["white", "yellow", "red"]))
vstyle = {"stroke":toyplot.color.near_black}

toyplot.graph(edges, layout=layout, vcolor=colormap, vsize=20, vstyle=vstyle, width=500);
01234567891011121314151617181920212223242526272829

Edge Rendering

Much like vertices, there are color, width, and style controls for edges:

estyle = {"stroke-dasharray":"3,3"}
toyplot.graph(
    edges,
    layout=layout,
    ecolor="black",
    ewidth=1.2,
    eopacity=0.4,
    estyle=estyle,
    vcolor=colormap,
    vsize=20,
    vstyle=vstyle,
    width=500,
);
01234567891011121314151617181920212223242526272829