mpld3 networkx d3.js force layout
python, mpld3, networkx, d3.js
mpld3 is a matplotlib to d3 library. It is lightweight and a pure Python / Javascript package, allowing a lot of the matplotlib interface to be accessible in the web. There are a number of examples on their website. Its integration with d3 allows someone familiar with Javascript to use Python and visualize using the power of d3. d3.js is a powerful low level visualization library and there are loads of examples online on the many features it brings to the table.
mpld3 also has the ability to add plugins to add new functionality. I wanted to take a shot at adding a d3.js force layout plugin. The force layout is a powerful visualization tool and NetworkX has a nifty function that will convert the graph along with its attributes into a JSON graph format. I’d played around with this before and figured this would be a nice feature to have, so I’ve worked on it over the weekend and here it is - a NetworkX to d3.js force layout plugin for mpld3. I’ve shared an example below.
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import mpld3
mpld3.enable_notebook()
from mpld3 import plugins
= plt.subplots()
fig, ax
import networkx as nx
=nx.Graph()
G1, color='red', x=0.25, y=0.25, fixed=True, name='Node1')
G.add_node(2, x=0.75, y=0.75, fixed=True)
G.add_node(1,2)
G.add_edge(1,3)
G.add_edge(2,3)
G.add_edge(
= None
pos
connect(fig, NetworkXD3ForceLayout(G, pos, ax)) plugins.
I’ve implemented a sticky version of the force layout, since this is what I wanted. This can be turned off by passing an argument to the plugin. The reason it is called a sticky version is because dragging a node to a new position will fix it at that location. You can double click the node to release it.
These blocks were used as a reference[1].
I’ll run through an explanation of the code briefly.
= plt.subplots() fig, ax
This returns a figure and axes object. This plugin requires a single axes object to be passed to it. The figure and axes object, and everything that is on the axes object is converted to mpld3 plot objects. In theory, you could use NetworkX’s draw function to visualize the graph and mpld3 will render it fine. The only downside to that is that the final output will not be a force layout.
Next we create a graph with the following commands
import networkx as nx
=nx.Graph()
G1, color='red', x=0.25, y=0.25, fixed=True, name='Node1')
G.add_node(2, x=0.75, y=0.75, fixed=True)
G.add_node(1,2)
G.add_edge(1,3)
G.add_edge(2,3) G.add_edge(
I’ve set the color
attribute of the first node to red
. This is an attribute on the node object and will be used by the force layout to color the node. We can also set the (x, y)
coordinates to values for the first and second node. Passing the fixed=True
keyword argument assigns a attribute fixed
with the value True
on the NetworkX graph. When converted to a force layout, this will fix the positions of those nodes.
We are almost done! This registers the plugin with mpld3.
connect(fig, NetworkXD3ForceLayout(G, pos, ax)) plugins.
The pos
argument passed here is None
. I plan to set it up such that you can pass a position dictionary to the plugin and have the plugin assign (x,y)
coordinates when available. You can generate the pos
dictionary using any of NetworkX’s layout functions.
Additional keywords arguments can be passed to the constructor of the NetworkXD3ForceLayout
class. This allows a user to control certain force layout properties like gravity
, linkDistance
, linkStrength
etc. You can also set a default node size or turn off the dragging feature. The full list of attributes that can be passed is found in the docstring. I plan to write a more detailed description in a following post.
Here is another example of a NetworkX graph converted to a force layout. This is Zachary’s Karate Club. Nodes in Mr Hi
’s club are coloured purple and the rest are coloured orange. Node size is also changed based on the number of neighbours.
import matplotlib.pyplot as plt
import networkx as nx
= plt.subplots(1, 1, figsize=(10, 10))
fig, axs = axs
ax
=nx.karate_club_graph()
G= None
pos
for node, data in G.nodes(data=True):
if data['club'] == 'Mr. Hi':
'color'] = 'purple'
data[else:
'color'] = 'orange'
data[
for n, data in G.nodes(data=True):
'size'] = len(G.neighbors(n))
data[
connect(fig,
mpld3.plugins.
NetworkXD3ForceLayout(
G,
pos,
ax,=.5,
gravity=20,
link_distance=-600,
charge=1
friction
) )
See mpld3 documentation for more information.
import mpld3
class NetworkXD3ForceLayout(mpld3.plugins.PluginBase):
"""A NetworkX to D3 Force Layout Plugin"""
= """
JAVASCRIPT mpld3.register_plugin("networkxd3forcelayout", NetworkXD3ForceLayoutPlugin);
NetworkXD3ForceLayoutPlugin.prototype = Object.create(mpld3.Plugin.prototype);
NetworkXD3ForceLayoutPlugin.prototype.constructor = NetworkXD3ForceLayoutPlugin;
NetworkXD3ForceLayoutPlugin.prototype.requiredProps = ["graph",
"ax_id",];
NetworkXD3ForceLayoutPlugin.prototype.defaultProps = { coordinates: "data",
gravity: 1,
charge: -30,
link_strength: 1,
friction: 0.9,
link_distance: 20,
maximum_stroke_width: 2,
minimum_stroke_width: 1,
nominal_stroke_width: 1,
maximum_radius: 10,
minimum_radius: 1,
nominal_radius: 5,
};
function NetworkXD3ForceLayoutPlugin(fig, props){
mpld3.Plugin.call(this, fig, props);
};
var color = d3.scaleOrdinal(d3.schemeCategory10);
NetworkXD3ForceLayoutPlugin.prototype.zoomScaleProp = function (nominal_prop, minimum_prop, maximum_prop) {
var zoom = this.ax.zoom;
scalerFunction = function() {
var prop = nominal_prop;
if (nominal_prop*zoom.scale()>maximum_prop) prop = maximum_prop/zoom.scale();
if (nominal_prop*zoom.scale()<minimum_prop) prop = minimum_prop/zoom.scale();
return prop
}
return scalerFunction;
}
NetworkXD3ForceLayoutPlugin.prototype.setupDefaults = function () {
this.zoomScaleStroke = this.zoomScaleProp(this.props.nominal_stroke_width,
this.props.minimum_stroke_width,
this.props.maximum_stroke_width)
this.zoomScaleRadius = this.zoomScaleProp(this.props.nominal_radius,
this.props.minimum_radius,
this.props.maximum_radius)
}
NetworkXD3ForceLayoutPlugin.prototype.zoomed = function() {
this.tick()
}
NetworkXD3ForceLayoutPlugin.prototype.draw = function(){
plugin = this
DEFAULT_NODE_SIZE = this.props.nominal_radius;
var height = this.fig.height
var width = this.fig.width
var graph = this.props.graph
var gravity = this.props.gravity.toFixed()
var charge = this.props.charge.toFixed()
var link_distance = this.props.link_distance.toFixed()
var link_strength = this.props.link_strength.toFixed()
var friction = this.props.friction.toFixed()
this.ax = mpld3.get_element(this.props.ax_id, this.fig)
var ax = this.ax;
this.ax.elements.push(this)
ax_obj = this.ax;
var width = d3.max(ax.x.range()) - d3.min(ax.x.range()),
height = d3.max(ax.y.range()) - d3.min(ax.y.range());
var color = d3.scaleOrdinal(d3.schemeCategory10);
this.xScale = d3.scaleLinear().domain([0, 1]).range([0, width]) // ax.x;
this.yScale = d3.scaleLinear().domain([0, 1]).range([height, 0]) // ax.y;
this.force = d3.forceSimulation();
this.svg = this.ax.axes.append("g");
for(var i = 0; i < graph.nodes.length; i++){
var node = graph.nodes[i];
if (node.hasOwnProperty('x')) {
node.x = this.ax.x(node.x);
}
if (node.hasOwnProperty('y')) {
node.y = this.ax.y(node.y);
}
}
this.force
.force("link",
d3.forceLink()
.id(function(d) { return d.index })
.strength(link_strength)
.distance(link_distance)
)
.force("collide", d3.forceCollide(function(d){return d.r + 8 }).iterations(16))
.force("charge", d3.forceManyBody().strength(charge))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("y", d3.forceY(0))
.force("x", d3.forceX(0));
this.force.nodes(graph.nodes);
this.force.force("link").links(graph.links);
this.link = this.svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.attr("stroke", "black")
.style("stroke-width", function (d) { return Math.sqrt(d.value); });
this.node = this.svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", function(d) {return d.size === undefined ? DEFAULT_NODE_SIZE : d.size ;})
.style("fill", function (d) { return color(d); });
this.node.append("title")
.text(function (d) { return d.name; });
this.force.on("tick", this.tick.bind(this));
this.setupDefaults()
};
NetworkXD3ForceLayoutPlugin.prototype.tick = function() {
this.link.attr("x1", function (d) { return this.ax.x(this.xScale.invert(d.source.x)); }.bind(this))
.attr("y1", function (d) { return this.ax.y(this.yScale.invert(d.source.y)); }.bind(this))
.attr("x2", function (d) { return this.ax.x(this.xScale.invert(d.target.x)); }.bind(this))
.attr("y2", function (d) { return this.ax.y(this.yScale.invert(d.target.y)); }.bind(this));
this.node.attr("transform", function (d) {
return "translate(" + this.ax.x(this.xScale.invert(d.x)) + "," + this.ax.y(this.yScale.invert(d.y)) + ")";
}.bind(this)
);
}
"""
def __init__(self, graph, ax,
=1,
gravity=20,
link_distance=-30,
charge=5,
node_size=1,
link_strength=0.9):
friction
self.dict_ = {"type": "networkxd3forcelayout",
"graph": graph,
"ax_id": mpld3.utils.get_id(ax),
"gravity": gravity,
"charge": charge,
"friction": friction,
"link_distance": link_distance,
"link_strength": link_strength,
"nominal_radius": node_size}
References
Reuse
Citation
@online{krishnamurthy2016,
author = {Krishnamurthy, Dheepak},
title = {Mpld3 Networkx D3.js Force Layout},
date = {2016-10-02},
url = {https://kdheepak.com/blog/mpld3-networkx-d3js-force-layout/},
langid = {en}
}