Skip to content

Binder

Implementing a new plot

We'll show you how to implement a new plot using the nxviz's layered API.

As an example, we'll show you how the design process works for the matrix plot.

%config InlineBackend.figure_format = 'retina'
%load_ext autoreload
%autoreload 2

Example graph

As always, we'll need an example graph to anchor our notebook.

from random import choice

import networkx as nx
import numpy as np

G = nx.erdos_renyi_graph(n=20, p=0.1)
for n, d in G.nodes(data=True):
    G.nodes[n]["group"] = choice(["a", "b", "c"])
    G.nodes[n]["value"] = np.random.exponential()

np.random.seed(44)
for u, v, d in G.edges(data=True):
    G.edges[u, v]["edge_value"] = np.random.exponential()

Implement node layout

We first have to worry about how the nodes are placed. Therefore, we need a node layout function.

All node layout functions accept the following arguments:

  • a node table nt,
  • the key to group_by
  • the key to sort_by (optionally)
  • any other relevant keyword arguments

With the matrix plot layout, from thinking about how the nodes should be laid out, we will probably arrive at the conclusion that grouping and sorting are technically optional and not intrinsic to the layout. If that's not obvious at first glance, please think about it, you'll probably arrive at the same conclusion!

They then return the x, y coordinates to place nodes on.

The exact glyphs and their styles are out-of-bounds! Therefore, don't worry about them just yet.

from typing import Hashable

import pandas as pd

from nxviz.utils import group_and_sort


# Just the skeleton first!
def matrix_layout(
    nt: pd.DataFrame, group_by: Hashable = None, sort_by: Hashable = None
):
    nt = group_and_sort(nt=nt, group_by=group_by, sort_by=sort_by)

With a matrix plot, our goal is to place nodes along the x- and y-axis. It's a bit like the hive plot with cloned axes.

See the code annotations for the logic.

# Filling in the x,y positions dictionary.
def matrix_layout(
    nt: pd.DataFrame,
    group_by: Hashable = None,
    sort_by: Hashable = None,
    axis="x",
):
    # Nodes should be grouped and sorted before we begin assigning coordinates.
    nt = group_and_sort(node_table=nt, group_by=group_by, sort_by=sort_by)

    # We are eventually going to return this pos dictionary.
    pos = dict()

    # Loop over each of the rows, and assign x, y coordinates in order of them being grouped and sorted.
    for i, (node, data) in enumerate(nt.iterrows()):
        x = (i + 1) * 2
        y = 0

        if axis == "y":
            x, y = y, x
        pos[node] = np.array([x, y])
    return pos

Now that we have the positions implemented, let's see what they look like.

from nxviz import layouts
from nxviz.utils import node_table

nt = node_table(G)
pos_x = matrix_layout(nt, group_by="group", sort_by="value")
pos_y = matrix_layout(nt, group_by="group", sort_by="value", axis="y")
pd.DataFrame(pos_x).T
pd.DataFrame(pos_y).T

Now, we can worry about the glyphs being drawn to screen. We will follow the logic for the mid-level API. There is a draw function that we can take advantage of to make it happen.

from functools import partial

from nxviz import nodes
matrix = partial(
    nodes.draw, layout_func=matrix_layout, group_by=None, sort_by=None
)
pos_x = matrix(G)
pos_y = matrix(G, layout_kwargs=dict(axis="y"))

Not bad! We're off to a good start. This looks ugly, but upon inspection, its' because the aspect ratio isn't that good. We can fix this.

from nxviz.plots import aspect_equal, despine

matrix = partial(
    nodes.draw, layout_func=matrix_layout, group_by=None, sort_by=None
)
pos_x = matrix(G)
pos_y = matrix(G, layout_kwargs=dict(axis="y"))

aspect_equal()
despine()

Now that's looking good! We have a square matrix, just as we expected.

Drawing edges

For edges, we could take advantage of hive plot's lines. That would make the chart look interesting... like one of those arts and crafts tapestries we might have made when we were younger.

from nxviz import edges

matrix = partial(
    nodes.draw, layout_func=matrix_layout, group_by=None, sort_by=None
)
pos_x = matrix(G)
pos_y = matrix(G, layout_kwargs=dict(axis="y"))
edges.hive(G, pos_x, pos_cloned=pos_y, curves=False)

However, the spirit of a matrix plot is to fill in an n-by-n matrix. Thus, we should actually be using a custom implementation of edges that draws in a circle glyph where needed.

The matrix "lines" function will follow the API of the functions in the nxviz.lines file. Lines are in quotes because we're not technically writing out lines. :)

from typing import Dict, Iterable

from matplotlib.patches import Circle


def matrix_lines(
    et,
    pos,
    pos_cloned,
    edge_color: Iterable,
    alpha: Iterable,
    lw: Iterable,
    aes_kw: Dict,
):
    patches = []
    for r, d in et.iterrows():
        start = d["source"]
        end = d["target"]
        x_start, y_start = pos_y[start]
        x_end, y_end = pos[end]

        x, y = (max(x_start, y_start), max(x_end, y_end))
        kw = {
            "fc": edge_color[r],
            "alpha": alpha[r],
            "radius": lw[r],
            "zorder": 10,
        }
        kw.update(aes_kw)
        patch = Circle(xy=(x, y), **kw)
        patches.append(patch)
    return patches


matrix_edges = partial(edges.draw, lines_func=matrix_lines)

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(4, 4))
pos_x = matrix(G, group_by="group", color_by="value", sort_by="value")
pos_y = matrix(
    G,
    group_by="group",
    color_by="value",
    sort_by="value",
    layout_kwargs=dict(axis="y"),
)
edges.matrix(G, pos_x, pos_cloned=pos_y, alpha_by="edge_value")

despine()
aspect_equal()

Annotations

We may wish to annotate the plot with additional information. For example, we might want to annotate the node values. This is doable using the same annotation tools available to us in nxviz.

Node color by group

from nxviz import annotate

fig, ax = plt.subplots(figsize=(4, 4))
pos_x = matrix(G, group_by="group", color_by="group", sort_by="value")
pos_y = matrix(
    G,
    group_by="group",
    color_by="group",
    sort_by="value",
    layout_kwargs=dict(axis="y"),
)
matrix_edges(G, pos_x, pos_cloned=pos_y, alpha_by="edge_value")

# Gives us a colorbar next to the chart.
annotate.node_colormapping(G, color_by="group")

despine()
aspect_equal()

Node color by value

from nxviz import annotate

fig, ax = plt.subplots(figsize=(4, 4))
pos_x = matrix(G, group_by="group", color_by="value", sort_by="value")
pos_y = matrix(
    G,
    group_by="group",
    color_by="value",
    sort_by="value",
    layout_kwargs=dict(axis="y"),
)
matrix_edges(G, pos_x, pos_cloned=pos_y, encodings_kwargs={"alpha_scale": 5})

# Gives us a colorbar next to the chart.
annotate.node_colormapping(G, color_by="value")

despine()
aspect_equal()

Annotating group identity

The group identities can also be annotated on the chart itself. Here's how the matrix_group annotation function is implemented.

from nxviz.plots import respine


def matrix_group(G, group_by, ax=None, offset=-3.0, xrotation=0, yrotation=0):
    if ax is None:
        ax = plt.gca()
    nt = node_table(G)
    group_sizes = nt.groupby(group_by).apply(lambda df: len(df))
    proportions = group_sizes / group_sizes.sum()
    midpoint = proportions / 2
    starting_positions = proportions.cumsum() - proportions
    label_positions = (starting_positions + midpoint) * len(G) * 2
    label_positions += 1

    for label, position in label_positions.to_dict().items():
        # Plot the x-axis labels
        y = offset
        x = position
        ax.annotate(label, xy=(x, y), ha="center", va="center", rotation=0)

        # Plot the y-axis labels
        x = offset
        y = position
        ax.annotate(label, xy=(x, y), ha="center", va="center", rotation=90)


fig, ax = plt.subplots(figsize=(4, 4))
pos_x = matrix(G, group_by="group", color_by="group", sort_by="value")
pos_y = matrix(
    G,
    group_by="group",
    color_by="group",
    sort_by="value",
    layout_kwargs=dict(axis="y"),
)
matrix_edges(G, pos_x, pos_cloned=pos_y, alpha_by="edge_value")

# Gives us a colorbar next to the chart.
matrix_group(G, group_by="group")

despine()
aspect_equal()

Annotate matrix blocks

We could also annotate the matrix blocks using the exact same logic.

Matrix blocks are defined as the blocks of nodes in the same group, so this only applies to graphs for which the nodes can be grouped together.

fig, ax = plt.subplots(figsize=(4, 4))
pos_x = matrix(G, group_by="group", color_by="group", sort_by="value")
pos_y = matrix(
    G,
    group_by="group",
    color_by="group",
    sort_by="value",
    layout_kwargs=dict(axis="y"),
)
matrix_edges(G, pos_x, pos_cloned=pos_y, alpha_by="edge_value")

matrix_group(G, group_by="group")

respine()
aspect_equal()

##### FUNCTION STARTS

from matplotlib.patches import Rectangle

from nxviz import encodings as aes
from nxviz import utils

nt = node_table(G)
group_by = "group"
color_by = "group"


def matrix_block(G, group_by, color_by=None, ax=None):

    group_sizes = nt.groupby(group_by).apply(lambda df: len(df)) * 2
    starting_positions = group_sizes.cumsum() + 1 - group_sizes
    starting_positions

    colors = pd.Series(["black"] * len(group_sizes), index=group_sizes.index)
    if color_by:
        color_data = pd.Series(group_sizes.index, index=group_sizes.index)
        colors = aes.data_color(color_data, color_data)
    # Generate patches first
    patches = []
    for label, position in starting_positions.to_dict().items():
        xy = (position, position)
        width = height = group_sizes[label]

        patch = Rectangle(
            xy, width, height, zorder=0, alpha=0.1, facecolor=colors[label]
        )
        patches.append(patch)

    if ax is None:
        ax = plt.gca()
    # Then add patches in.
    for patch in patches:
        ax.add_patch(patch)


matrix_block(G, group_by=group_by, color_by=color_by)
##### FUNCTION ENDS
despine()

High level API

Of course, in showing you how to implement a matrix plot from scratch, we took the code and shoved it into our high-level API. Here's a few examples of how it's used.

import nxviz as nv

ax = nv.matrix(G)
ax = nv.matrix(
    G, group_by="group", node_color_by="group", edge_alpha_by="edge_value"
)
annotate.matrix_block(G, group_by="group", color_by="group")