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 inlineimport numpy as npimport matplotlib.pyplot as pltimport mpld3mpld3.enable_notebook()
from mpld3 import plugins
fig, ax = plt.subplots()
import networkx as nxG=nx.Graph()G.add_node(1, 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_edge(1,2)G.add_edge(1,3)G.add_edge(2,3)
pos = None
plugins.connect(fig, NetworkXD3ForceLayout(G, pos, ax))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 from SVG Semantic Zooming.
I'll run through an explanation of the code briefly.
fig, ax = plt.subplots()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 nxG=nx.Graph()G.add_node(1, 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_edge(1,2)G.add_edge(1,3)G.add_edge(2,3)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.
plugins.connect(fig, NetworkXD3ForceLayout(G, pos, ax))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 pltimport networkx as nx
fig, axs = plt.subplots(1, 1, figsize=(10, 10))ax = axs
G=nx.karate_club_graph()pos = None
for node, data in G.nodes(data=True): if data['club'] == 'Mr. Hi': data['color'] = 'purple' else: data['color'] = 'orange'
for n, data in G.nodes(data=True): data['size'] = len(G.neighbors(n))
mpld3.plugins.connect(fig, NetworkXD3ForceLayout( G, pos, ax, gravity=.5, link_distance=20, charge=-600, friction=1 ))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, gravity=1, link_distance=20, charge=-30, node_size=5, link_strength=1, friction=0.9):
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}