The New Best Python Package for Visualising Network Graphs

Introduction
In this article, I will introduce to you a Python package I stumbled upon that is, in my humble opinion, the BEST tool I have seen so far for visualising network graphs.
Readers who are data scientists in need of a compact yet powerful visualisation package for quick prototyping, exploratory data analysis or debugging their network models are best suited for the contents below.
The package that we will be inspecting is called: [gravis](https://robert-haas.github.io/gravis-docs/)
I personally use Graph Neural Networks a lot in my day-to-day job, and quite frankly, I am annoyed that I didn't know about this package earlier as it would have saved me a lot of time and energy trying to work around the shortcomings of the packages (ipysigma
and pyvis
) that I wrote about here:
What makes a network visualisation package the best?
A visualisation package needs to:
- Create a fully interactive visualisation, where I can click on nodes and edges and view its attributes, plus drag and drop them.
- Convenient to implement – doesn't require too much code (like Dash), but powerful and flexible enough for most use cases.
- Moderately good scalability to the number of nodes and edges – we're not making something for prod, but we need it to handle hundreds of nodes at least.
- Compatible with commonly used network packages in Python such as
networkx
.
What will we be testing the package on?
Broadly speaking, we can qualify graphs according to whether they are homogenous or heterogeneous, or have directed or undirected edges.
We will therefore test the package on two types of graphs that we will generate using networkx
,
(1) A homogenous, undirected graph
(2) A heterogeneous, directed multigraph
as these are the two extremes of what one might encounter. If you are unfamiliar with the terminology, I suggest you visit my previous article and have a quick read of the introduction.
TL;DR
- Homogenous = 1 type of node
- Heterogeneous = multiple types of nodes and/or edges
- Undirected = Edges have no direction
- Directed = Edges have a direction
- Multi-graph = Two nodes can have multiple edges between them
Set Up and Installation
The package can be simply installed via pip:
pip install gravis
We also need to install the below packages in order to generate our test graphs.
pip install numpy, matplotlib, networkx
You can also find all the code I use below in this repository.
GitHub – bl3e967/medium-articles: The accompanying code to my medium articles.
Homogenous Undirected Network Example
So, to start us off, we need a graph to plot. We will write a simple graph generator function which will return a networkx.Graph
object. We will add attributes to its node and edges to simulate data a data scientist might see in their work.
Graph Generator
We use a random graph generator, for which I have selected the networkx.dual_barabasi_albert_graph
method to simulate a scale-free network.
We add node level attributes such as degree
, betweenness_centrality
, and some made up features with random numbers which we imaginatively call feature1
, feature2
, feature3
.
We do the same for the edges as well, and add features feature1
and feature2
.
We finally label each node with a uuid
(Universally Unique IDentifier) to make things look more like real data.
def get_new_test_graph():
NUM_NODES = 50
p = 0.5
seed = 1
test_graph = nx.dual_barabasi_albert_graph(n=NUM_NODES, p=p, seed=seed, m1=2, m2=1)
# add node properties
nx.set_node_attributes(test_graph, dict(test_graph.degree()), name='degree')
nx.set_node_attributes(test_graph, nx.betweenness_centrality(test_graph), name='betweenness_centrality')
for node, data in test_graph.nodes(data=True):
data['node_identifier'] = str(uuid.uuid4())
data['feature1'] = np.random.random()
data['feature2'] = np.random.randint(0, high=100)
data['feature3'] = 1 if np.random.random() > 0.5 else 0
# add edge properties
for _, _, data in test_graph.edges(data=True):
data['feature1'] = np.random.random()
data['feature2'] = np.random.randint(0, high=100)
return test_graph
When we plot the graph using networkx
we get the following:
test_graph = get_new_test_graph()
nx.draw(test_graph)

Plotting with gravis
We now move on to plotting this graph using gravis
. It is very simple to get going with this package and in full, it looks like this.
import gravis as gv
gv.d3(
test_graph,
# graph specs
graph_height=500,
# node specs
node_size_data_source="betweenness_centrality",
use_node_size_normalization=True,
node_size_normalization_min=15,
node_size_normalization_max=35,
show_node_label=True,
node_label_data_source='node_identifier',
# edge specs
edge_size_data_source='feature1',
use_edge_size_normalization=True,
edge_size_normalization_min=1,
edge_size_normalization_max=5,
# force-directed graph specs
many_body_force_strength=-500
)
Let's break this down into chunks.
Let's assume we want to scale our node sizes according to their betweenness_centrality
values and our edge thicknesses by their feature1
values.
We set use_node_size_normalization=True
such that the node sizes are set according to normalised values of the betweenness_centrality
, and we define node_size_normalization_min
and node_size_normalisation_max
to set the min and max node sizes we want.
# node specs
node_size_data_source="betweenness_centrality",
use_node_size_normalization=True,
node_size_normalization_min=15,
node_size_normalization_max=35,
We use equivalent arguments for edges to control their thickness:
# edge specs
edge_size_data_source='feature1',
use_edge_size_normalization=True,
edge_size_normalization_min=1,
edge_size_normalization_max=5,
Finally, I set the many_body_force_strength
parameter to -500
to make the edges longer than it is by default, to make the graph clearer to see.
The resulting plot is shown below.

Adding Colour
In addition to scaling the node sizes by betweenness_centrality
, I also want to colour them according to this as well.
We can achieve this simply by adding a color
attribute to the nodes. I can use named colours such as ‘red', ‘blue', ‘green', and I won't bother trying to do that here as that is too trivial. Let's try and use a colour scale instead.
I want to scale the values according to the winter
colourmap found in matplotlib

I have this very helpful MplColorHelper
class that I have used many times in my projects, that will convert a numeric value into an RGB
string.
class MplColorHelper:
def __init__(self, cmap_name, start_val, stop_val):
self.cmap_name = cmap_name
self.cmap = plt.get_cmap(cmap_name)
self.norm = mpl.colors.Normalize(vmin=start_val, vmax=stop_val)
self.scalarMap = cm.ScalarMappable(norm=self.norm, cmap=self.cmap)
def get_rgba(self, val):
return self.scalarMap.to_rgba(val, bytes=True)
def get_rgb_str(self, val):
r, g, b, a = self.get_rgba(val)
return f"rgb({r},{g},{b})"
All I need to specify is the min-max values we want to scale the colormap over.
So, to colour nodes according to betweenness_centrality
we do the below:
# the matplotlib colourmap we want to use
CM_NAME = "winter"
# initialise colour helper
vals = nx.get_node_attributes(test_graph, 'betweenness_centrality').values()
betweenness_min, betweenness_max = min(vals), max(vals)
node_colors = MplColorHelper(CM_NAME, betweenness_min, betweenness_max)
# get rgb string for each node
for node, data in test_graph.nodes(data=True):
data['color'] = node_colors.get_rgb_str(data['betweenness_centrality'])
and for edges, let's colour them according to feature1
.
# initialise colour helper
vals = nx.get_edge_attributes(test_graph, 'feature1').values()
val_min, val_max = min(vals), max(vals)
edge_colors = MplColorHelper(CM_NAME, val_min, val_max)
# get rgb string for each node
for u, v, data in test_graph.edges(data=True):
data['color'] = edge_colors.get_rgb_str(data['feature1'])
And voila, let there be colour:

Finally, gravis
allows us to display any kind of free text stored on each node or edge using an information bar on the bottom of the visualisation.
We will use this to display our feature values – we format some text with the feature values, and save it to an attribute named click
:
# node features
for node, data in test_graph.nodes(data=True):
data['click'] = (
f"Node: {data['node_identifier']}"
"nNode Features:" +
f"nfeature 1: {data['feature1']:.3f}" +
f"nfeature 2: {data['feature2']:.3f}" +
f"nfeature 3: {data['feature3']:.3f}" +
f"nBetweenness Centrality: {data['betweenness_centrality']:.3f}" +
f"nDegree: {data['degree']}"
)
# edge features
for u, v, data in test_graph.edges(data=True):
data['click'] = (
f"Edge: {test_graph.nodes[u]['node_identifier']} -> {test_graph.nodes[v]['node_identifier']}" +
f"nEdge Features:" +
f"nfeature 1: {data['feature1']}" +
f"nfeature 2: {data['feature2']}"
)
And now we are able to display our feature values very nicely using a panel on the bottom of the screen (I've made the graphs extra large here so you can see it as you would on screen).


We have a fully interactive visualisation that allows us to drag nodes, and a side-bar to change the settings for the node and edge visualisation, labels, and the layout algorithm.
It also allows you to export the visualisation to an image using the settings bar – no extra code required! Finally, if you wished to share this interactive graph with anyone, you can export it as a self-contained HTML file:
fig = gv.d3(test_graph, *args, **kwargs)
fig.export_html("graph_to_export.html")
Heterogeneous Directed Network Example
We now try and visualise a heterogeneous directed network using gravis
.
Graph Generator
Again, we need a function to return such a graph for us. We will use the same function as before, but now using the nx.scale_free_graph
function.
We also add in a node_type
attribute to simulate a heterogeneous graph. The first 25 nodes will be node_type = 0
and the remaining will be node_type = 1
.
def get_new_test_digraph():
NUM_NODES = 50
# We change the graph generator function here
test_graph = nx.scale_free_graph(n=NUM_NODES, seed=0, alpha=0.5, beta=0.2, gamma=0.3)
# add node properties
nx.set_node_attributes(test_graph, dict(test_graph.degree()), name='degree')
nx.set_node_attributes(test_graph, nx.betweenness_centrality(test_graph), name='betweenness_centrality')
for node, data in test_graph.nodes(data=True):
# assign node type so we have heterogeneous graph
data['node_type'] = 0 if node < 25 else 1
# same as before, a add ther node features.
data['node_identifier'] = str(uuid.uuid4())
data['feature1'] = np.random.random()
data['feature2'] = np.random.randint(0, high=100)
data['feature3'] = 1 if np.random.random() > 0.5 else 0
# add edge properties
for u, v, data in test_graph.edges(data=True):
data['feature1'] = np.random.random()
data['feature2'] = np.random.randint(0, high=100)
return test_graph
Plotting this using nx.draw
gives us this:

Plotting with gravis
How we plot for heterogeneous graphs is exactly the same as above. We can use the same methods to:
- generate a test multi-digraph
- add feature values for nodes and edges
- add colour to nodes and edges according to an attribute value
All we need to do now is specify the shape of each node, as now we deal with multiple different node types.
For our graph generator function get_new_test_digraph
, we just need to add this line of code in our for
loop over nodes:
for node, data in test_graph.nodes(data=True):
data['node_type'] = 0 if node < 25 else 1 # add this line
where for our purposes of simulating real-life data, we set the first 25 nodes in our graphs to be of node_type = 0
, else node_type = 1
.
Next, we can use the shape
attribute in gravis to specify the node shapes we want to use for each node.
Here, we will set node_type = 0
to be circles, and rectangles for node_type = 1
.
for node, data in test_graph.nodes(data=True):
data['value'] = data['betweenness_centrality'] # node size
data['label'] = data['node_identifier']
data['title'] = (
f"Node: {data['node_identifier']}"
"nNode Features:" +
f"nfeature 1: {data['feature1']}" +
f"nfeature 2: {data['feature2']}" +
f"nfeature 3: {data['feature3']}" +
f"nBetweenness Centrality: {data['betweenness_centrality']}" +
f"nDegree: {data['degree']}"
)
data['color'] = node_colors.get_rgb_str(data['betweenness_centrality'])
## add this line to specify shape.
data['shape'] = 'dot' if data['node_type'] == 0 else 'rectangle'
We are now ready to plot our network in gravis
. Notice we added the edge_curvature
argument to be non-zero. This is necessary to prevent edges between two nodes from overlapping when there are many of them.
gv.d3(
test_graph,
graph_height=700,
node_size_data_source="betweenness_centrality",
use_node_size_normalization=True,
node_size_normalization_min=15,
node_size_normalization_max=35,
show_node_label=False,
node_label_data_source='node_identifier',
edge_size_data_source='feature1',
use_edge_size_normalization=True,
edge_size_normalization_min=1,
edge_size_normalization_max=5,
# Specify curvature to differentiate between the multiple
# edges between two nodes.
edge_curvature=-0.5,
many_body_force_strength=-500
)

Voila! Simple and easy. You can see the multiple connections between some of the nodes on the left and the top of the graph.
Summary – Why I think Gravis is the best
I have already mentioned that I wrote about two other packages where I insisted they were the best – pyvis
and ipysigma
. I was convinced of this at the time as I had been using them myself for my day-to-day work.
However, they have their unique issues which, in hindsight, made them suboptimal and this was why I covered both of them in the same blog! Depending on the use case, one of them was better than the other and I recommended users on which one to use when.
This is why I was SHOCKED when I found gravis
, as it seemed like someone had thought the exact same thing and developed this package to combine the pros of pyvis
and ipysigma
to make a perfect tool.
Firstly, gravis
provides both a fully interactive UI as you can:
- Not only click on nodes and edges to display information but also allows the user to drag and drop nodes, very useful for complicated networks where the physics engine may need manual tweaking (and lacking in
ipysigma
) - The node or edge information is displayed through a bar on the screen that neatly houses the information and also allows one to copy information from it (a small but huge functionality that was lacking in
pyvis
that gave me a lot of headaches at my work). - Provides a different side-bar through which you can tune the physics engine parameters, change the settings for node and edge visualisation, and buttons through which you can export the graph as images.
- Plus it can display multiple edges between nodes (again, something
ipysigma
was incapable of doing).
Therefore, gravis
removes the need to pick and choose between pyvis
and ipysigma
as it has morphed the two packages together into one, whilst maintaining all of their advantages.
In short, it ticks all the boxes and does its job well.
I highly recommend this package.
Let me know what you think of it in the comments section, and if you like my article please give a clap or share with anyone who might be interested.