Visualizing any NetworkX graph¶
This notebook is for readers who already have a NetworkX graph. Every nv.plot function accepts a Graph, DiGraph, MultiGraph or MultiDiGraph directly, so the cleaning and graph-building steps of the pipeline can be skipped. The examples use graphs that ship with NetworkX and a few seeded random graphs, so everything runs offline.
The plot functions share one set of arguments: color_by, size_by, edge_width_by, edge_color_by, label_by, show_labels, top_n, focus, time, title and height. Each returns a Plotly figure and never shows it; the cells below call fig.show().
import warnings
import networkx as nx
import pandas as pd
import plotly.io as pio
import netviz_tools as nv
pio.renderers.default = "notebook_connected"
Karate club: colour, size and communities¶
Zachary's karate club has 34 members and 78 friendships. Each node carries a club attribute (the faction the member joined after the club split), and each edge a weight. color_by and size_by take a node attribute or a metric name, here the faction and betweenness centrality.
karate = nx.karate_club_graph()
print(karate.number_of_nodes(), "nodes,", karate.number_of_edges(), "edges")
print(nv.metrics.centrality(karate, ["betweenness", "degree"]).head(3).round(3))
34 nodes, 78 edges
betweenness degree
node
0 0.395 16.0
2 0.337 10.0
33 0.329 17.0
nv.plot.network(karate, color_by="club", size_by="betweenness").show()
Members 0, 2 and 33 have the highest betweenness (0.395, 0.337 and 0.329), so they are drawn largest. Member 0 is the instructor ("Mr. Hi") and member 33 leads the other faction ("Officer").
Without color_by, nodes are coloured by Louvain community (seeded, so the result is the same on every run) and sized by strength, the sum of edge weights. layout="community" also places each community in its own cluster:
nv.plot.network(
karate, layout="community", title="Karate club, coloured and placed by community"
).show()
The plots compute communities with nv.metrics.communities and their own seed argument (42 by default), so the same seed reproduces the partition in the chart and lets it be compared with the factions:
part = nv.metrics.communities(karate, seed=42)
pd.crosstab(
part.rename("community"), pd.Series(nx.get_node_attributes(karate, "club"), name="club")
)
| club | Mr. Hi | Officer |
|---|---|---|
| community | ||
| 0 | 1 | 13 |
| 1 | 10 | 0 |
| 2 | 6 | 0 |
| 3 | 0 | 4 |
Louvain finds four communities. Three of them contain members of only one faction; community 0 holds 13 "Officer" members and 1 "Mr. Hi" member.
Les Misérables: a weighted graph¶
In the Les Misérables co-appearance graph, an edge's weight counts the chapters in which two characters appear together. Edge widths follow weight by default, and node size follows strength.
lesmis = nx.les_miserables_graph()
lm_part = nv.metrics.communities(lesmis, seed=42)
print(f"{lesmis.number_of_nodes()} characters, {lesmis.number_of_edges()} edges")
print(f"{lm_part.nunique()} communities, modularity {nv.metrics.modularity(lesmis, lm_part):.3f}")
print(nv.metrics.centrality(lesmis, ["strength"]).head(3))
77 characters, 254 edges
6 communities, modularity 0.565
strength
node
Valjean 158.0
Marius 104.0
Enjolras 91.0
nv.plot.network(
lesmis, layout="community", show_labels=12, title="Les Misérables co-appearances"
).show()
Valjean has the largest strength (158), followed by Marius (104) and Enjolras (91). With 77 nodes and 254 edges the node-link diagram is crowded. An adjacency matrix ordered by community shows the same structure as blocks along the diagonal; each coloured cell is one pair of characters, and thin lines mark the community boundaries.
nv.plot.adjacency(lesmis, top_n=None, title="Les Misérables, adjacency ordered by community").show()
Florentine families: focus and ego views¶
The Florentine families graph records marriage ties between 15 families in 15th-century Florence. focus= outlines one node, labels it, darkens its edges and fades the rest.
florentine = nx.florentine_families_graph()
nv.metrics.centrality(florentine, ["betweenness", "degree"]).head(3).round(3)
| betweenness | degree | |
|---|---|---|
| node | ||
| Medici | 0.522 | 6.0 |
| Guadagni | 0.255 | 4.0 |
| Albizzi | 0.212 | 3.0 |
nv.plot.network(florentine, focus="Medici", title="Florentine families, Medici in focus").show()
The Medici have 6 ties and a betweenness of 0.522, the highest in the graph; the Guadagni come second with 0.255. nv.plot.ego keeps only the focus node and its neighbours, here up to two steps away:
nv.plot.ego(florentine, "Medici", radius=2).show()
A directed graph: arrows, edge colours and a colour bar¶
Directed edges get an arrowhead at the target. edge_color_by="source" colours each edge like the node it leaves, and a numeric color_by such as "pagerank" gives a colour bar instead of a legend.
digraph = nx.gnp_random_graph(30, 0.08, seed=1, directed=True)
print(digraph.number_of_nodes(), "nodes,", digraph.number_of_edges(), "edges")
nv.metrics.centrality(digraph, ["pagerank", "in_degree"]).head(3).round(3)
30 nodes, 75 edges
| pagerank | in_degree | |
|---|---|---|
| node | ||
| 24 | 0.090 | 3.0 |
| 0 | 0.077 | 4.0 |
| 22 | 0.075 | 5.0 |
nv.plot.network(
digraph, color_by="pagerank", edge_color_by="source", title="Random directed graph (seed 1)"
).show()
Node 24 has the highest PageRank (0.090) with only 3 incoming edges, while node 22 has 5 incoming edges and a PageRank of 0.075. PageRank also depends on how important the nodes sending those edges are.
Multigraphs are collapsed¶
A MultiDiGraph can hold several edges between the same pair, for example one edge per message. The plots sum parallel edges (their weight, or 1 per edge when there is none), so the three messages from Ana to Ben become one edge of weight 3. Hover over an edge midpoint to see it.
messages = nx.MultiDiGraph()
messages.add_edges_from(
[
("Ana", "Ben"),
("Ana", "Ben"),
("Ana", "Ben"),
("Ben", "Ana"),
("Ben", "Cho"),
("Cho", "Dev"),
("Cho", "Dev"),
("Dev", "Ana"),
]
)
print(messages.number_of_edges(), "edges in the multigraph")
nv.plot.network(messages, title="Messages, parallel edges summed").show()
8 edges in the multigraph
Your own positions¶
When the graph already says where its nodes are, pass pos=, a mapping from node to (x, y). The nodes of a grid graph are (row, column) tuples, so each node is its own coordinate:
grid = nx.grid_2d_graph(6, 6)
nv.plot.network(
grid, pos={node: node for node in grid}, show_labels=False, title="6 x 6 grid"
).show()
Bipartite graphs¶
layout="bipartite" draws two columns, using the bipartite node attribute (0 or 1) when it is present. The Southern Women graph links 18 women to the 14 social events they attended:
davis = nx.davis_southern_women_graph()
sides = pd.Series(nx.get_node_attributes(davis, "bipartite")).value_counts().sort_index()
print(f"{davis.number_of_edges()} attendances; nodes per side: {sides.to_dict()}")
nv.plot.network(
davis, layout="bipartite", show_labels=True, height=750, title="Southern Women"
).show()
89 attendances; nodes per side: {0: 18, 1: 14}
Labels: show_labels and label_by¶
show_labels decides which nodes get a text label: "auto" (every node up to 30 nodes, otherwise the 12 largest), a number of largest nodes, True, False, or a list of nodes. label_by names the node attribute that supplies the text. Here only the two faction leaders of the karate club are labelled, with a name attribute added for the purpose:
nx.set_node_attributes(karate, {0: "Mr. Hi", 33: "Officer"}, "name")
nv.plot.network(
karate, color_by="club", show_labels=[0, 33], label_by="name", title="Faction leaders"
).show()
A larger graph¶
Browsers slow down with thousands of nodes, so graphs over 2,000 nodes are cut to the 2,000 largest, with a warning. A 3,000-node Barabási-Albert graph triggers it (the figure is built but not shown here):
ba = nx.barabasi_albert_graph(3000, 2, seed=0)
print(ba.number_of_nodes(), "nodes,", ba.number_of_edges(), "edges")
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
big = nv.plot.network(ba)
print(caught[0].message)
3000 nodes, 5996 edges
the graph has 3000 nodes; drawing the 2000 largest. Pass top_n= to choose, or max_nodes=None to draw all (slow in the browser).
top_n= chooses how many of the largest nodes to keep. The 300 best-connected nodes, with the 10 largest labelled. Selected nodes with no edge to the rest of the selection are drawn apart from it. layout="kamada_kawai" spreads the densely connected core more evenly than the default spring layout:
nv.plot.network(
ba,
top_n=300,
size_by="degree",
layout="kamada_kawai",
show_labels=10,
title="Barabási-Albert graph, top 300 nodes",
).show()
Letting plot.auto choose¶
nv.plot.auto draws any chart by name (kind="ego", kind="adjacency", ...) or picks one from the data. The choice and the reason are stored in fig.layout.meta["netviz"] and written under the title. nv.plot.choose_kind returns the choice without drawing:
fig = nv.plot.auto(karate)
print(fig.layout.meta["netviz"])
{'kind': 'network', 'reason': 'general graph'}
senders_receivers = nx.DiGraph([("s1", "t1"), ("s1", "t2"), ("s2", "t2"), ("s3", "t1")])
for data, kwargs in [
(karate, {}),
(karate, {"focus": 0}),
(digraph, {}),
(senders_receivers, {}),
]:
print(nv.plot.choose_kind(data, **kwargs))
('network', 'general graph')
('ego', 'one focus node (0)')
('network', 'general graph')
('sankey', 'every node only sends or only receives')
A general graph gets a network, one focus node gets an ego view, and a directed graph where every node only sends or only receives gets a Sankey diagram:
nv.plot.auto(senders_receivers).show()
Graphs over time¶
A mapping {period: graph} becomes an animation with a play button and a slider, one frame per period. Node positions stay fixed across frames, so movement on screen means a change in the graph. Here a Barabási-Albert graph grows by 10 nodes per step. With the same seed, each graph contains the one before it:
growth = {n: nx.barabasi_albert_graph(n, 2, seed=1) for n in range(10, 61, 10)}
print({n: g.number_of_edges() for n, g in growth.items()})
print(nv.plot.choose_kind(growth))
{10: 16, 20: 36, 30: 56, 40: 76, 50: 96, 60: 116}
('network', 'general graph, animated over time')
nv.plot.network(growth, title="A graph growing by preferential attachment").show()
The same works for any mapping of graphs, such as nv.graphs_by(flows, by="time") from a flow table; the notebook "From raw FAOSTAT rows to charts" in this gallery shows that route.