##// END OF EJS Templates
Basic NetworkX/D3 example
Basic NetworkX/D3 example

File last commit:

r14414:6a21744d
r14414:6a21744d
Show More
D3.ipynb
1221 lines | 39.0 KiB | text/plain | TextLexer

Validate NetworkX version

In [1]:
import networkx as nx
version = float('.'.join(nx.__version__.split('.')[0:2]))
if version < 1.8:
    raise Exception('This notebook requires networkx version 1.8 or later.  Version %s is installed on this machine.' % nx.__version__)

Simple Output Test

In [2]:
from networkx.readwrite import json_graph
import json

def to_d3_json(graph):
    data = json_graph.node_link_data(graph)
    return json.dumps(data)
In [3]:
G = nx.Graph([(1,2)])
to_d3_json(G)
Out[3]:
'{"directed": false, "graph": [], "nodes": [{"id": 1}, {"id": 2}], "links": [{"source": 0, "target": 1}], "multigraph": false}'
In [4]:
G.add_node('test')
to_d3_json(G)
Out[4]:
'{"directed": false, "graph": [], "nodes": [{"id": "test"}, {"id": 1}, {"id": 2}], "links": [{"source": 1, "target": 2}], "multigraph": false}'

Listen To Graph Changes

Create a simple eventfull dictionary.

In [5]:
class EventfulDict(dict):
    
    def __init__(self, *args, **kwargs):
        self._add_callbacks = []
        self._del_callbacks = []
        self._set_callbacks = []
        dict.__init__(self, *args, **kwargs)
        
    def on_add(self, callback, remove=False):
        self._register_callback(self._add_callbacks, callback, remove)
    def on_del(self, callback, remove=False):
        self._register_callback(self._del_callbacks, callback, remove)
    def on_set(self, callback, remove=False):
        self._register_callback(self._set_callbacks, callback, remove)
    def _register_callback(self, callback_list, callback, remove=False):
        if callable(callback):
            if remove and callback in callback_list:
                callback_list.remove(callback)
            elif not remove and not callback in callback_list:
                callback_list.append(callback)
        else:
            raise Exception('Callback must be callable.')

    def _handle_add(self, key, value):
        self._try_callbacks(self._add_callbacks, key, value)
    def _handle_del(self, key):
        self._try_callbacks(self._del_callbacks, key)
    def _handle_set(self, key, value):
        self._try_callbacks(self._set_callbacks, key, value)
    def _try_callbacks(self, callback_list, *pargs, **kwargs):
        for callback in callback_list:
            callback(*pargs, **kwargs)
    
    def __setitem__(self, key, value):
        return_val = None
        exists = False
        if key in self:
            exists = True
            
        # If the user sets the property to a new dict, make the dict
        # eventful and listen to the changes of it ONLY if it is not
        # already eventful.  Any modification to this new dict will
        # fire a set event of the parent dict.
        if isinstance(value, dict) and not isinstance(value, EventfulDict):
            new_dict = EventfulDict(value)
            
            def handle_change(*pargs, **kwargs):
                self._try_callbacks(self._set_callbacks, key, dict.__getitem__(self, key))
                
            new_dict.on_add(handle_change)
            new_dict.on_del(handle_change)
            new_dict.on_set(handle_change)
            return_val = dict.__setitem__(self, key, new_dict)
        else:
            return_val = dict.__setitem__(self, key, value)
        
        if exists:
            self._handle_set(key, value)
        else:
            self._handle_add(key, value)
        return return_val
        

    def __delitem__(self, key):
        return_val = dict.__delitem__(self, key)
        self._handle_del(key)
        return return_val

    
    def pop(self, key):
        return_val = dict.pop(self, key)
        if key in self:
            self._handle_del(key)
        return return_val

    def popitem(self):
        popped = dict.popitem(self)
        if popped is not None and popped[0] is not None:
            self._handle_del(popped[0])
        return popped

    def update(self, other_dict):
        for (key, value) in other_dict.items():
            self[key] = value
            
    def clear(self):
        for key in list(self.keys()):
            del self[key]

Test the eventful dictionary.

In [6]:
a = EventfulDict()
In [7]:
def echo_dict_events(eventful_dict, prefix=''):
    def key_add(key, value):
        print prefix + 'add (%s, %s)' % (key, str(value))
    def key_set(key, value):
        print prefix + 'set (%s, %s)' % (key, str(value))
    def key_del(key):
        print prefix + 'del %s' % key
    eventful_dict.on_add(key_add)
    eventful_dict.on_set(key_set)
    eventful_dict.on_del(key_del)
    
echo_dict_events(a)
In [8]:
a['a'] = 'hello'
add (a, hello)
In [9]:
a['a'] = 'goodbye'
set (a, goodbye)
In [10]:
b = {'c': 'yay', 'd': 'no'}
a.update(b)
add (c, yay)
add (d, no)
In [11]:
a
Out[11]:
{'a': 'goodbye', 'c': 'yay', 'd': 'no'}
In [12]:
a.pop('a')
Out[12]:
'goodbye'
In [13]:
a.popitem()
del c
Out[13]:
('c', 'yay')
In [14]:
a['e'] = {}
add (e, {})
In [15]:
a['e']['a'] = 0
set (e, {'a': 0})
In [16]:
a['e']['b'] = 1
set (e, {'a': 0, 'b': 1})
In [17]:
a.clear()
del e
del d

Override the NetworkX Graph object to make an eventful graph.

In [18]:
class EventfulGraph(nx.Graph):
    
    def __init__(self, *pargs, **kwargs):
        """Initialize a graph with edges, name, graph attributes."""
        super(EventfulGraph, self).__init__(*pargs, **kwargs)
        
        self.graph = EventfulDict(self.graph)
        self.node = EventfulDict(self.node)
        self.adj = EventfulDict(self.adj)
        
In [19]:
def echo_graph_events(eventful_graph):
    for key in ['graph', 'node', 'adj']:
        echo_dict_events(getattr(eventful_graph, key), prefix=key+' ')
In [20]:
G = EventfulGraph()
echo_graph_events(G)
In [21]:
to_d3_json(G)
Out[21]:
'{"directed": false, "graph": [], "nodes": [], "links": [], "multigraph": false}'
In [22]:
G.add_node('hello')
G.add_node('goodbye')
G.add_edges_from([(1,2),(1,3)])
to_d3_json(G)
adj add (hello, {})
node add (hello, {})
adj add (goodbye, {})
node add (goodbye, {})
adj add (1, {})
node add (1, {})
adj add (2, {})
node add (2, {})
adj set (1, {2: {}})
adj set (2, {1: {}})
adj add (3, {})
node add (3, {})
adj set (1, {2: {}, 3: {}})
adj set (3, {1: {}})
Out[22]:
'{"directed": false, "graph": [], "nodes": [{"id": 1}, {"id": 2}, {"id": 3}, {"id": "hello"}, {"id": "goodbye"}], "links": [{"source": 0, "target": 1}, {"source": 0, "target": 2}], "multigraph": false}'
In [23]:
G.adj
Out[23]:
{1: {2: {}, 3: {}}, 2: {1: {}}, 3: {1: {}}, 'goodbye': {}, 'hello': {}}
In [24]:
G.node
Out[24]:
{1: {}, 2: {}, 3: {}, 'goodbye': {}, 'hello': {}}

Custom Widget

In [25]:
%%html
<div id="d3loadindicator" style="background: red; color: white;"><center>Loading D3...<center></div>
<script>
    $.getScript('http://d3js.org/d3.v3.min.js', function(){
        $('#d3loadindicator')
            .css('background', 'green')
            .html('<center>D3 Loaded Successfully</center>');
    });
</script>
Loading D3...
In [26]:
from IPython.html import widgets # Widget definitions
from IPython.display import display # Used to display widgets in the notebook
In [27]:
# Import the base Widget class and the traitlets Unicode class.
from IPython.html.widgets import Widget
from IPython.utils.traitlets import Unicode

# Define our ForceDirectedGraphWidget and its target model and default view.
class ForceDirectedGraphWidget(Widget):
    target_name = Unicode('ForceDirectedGraphModel')
    default_view_name = Unicode('D3ForceDirectedGraphView')
    
    _keys = ['initial_json']
    initial_json = Unicode()
    
    def __init__(self, eventful_graph, *pargs, **kwargs):
        Widget.__init__(self, *pargs, **kwargs)
        
        self._eventful_graph = eventful_graph
        self._send_dict_changes(eventful_graph.graph, 'graph')
        self._send_dict_changes(eventful_graph.node, 'node')
        self._send_dict_changes(eventful_graph.adj, 'adj')
        
        
    def _repr_widget_(self, *pargs, **kwargs):
        self.initial_json = to_d3_json(self._eventful_graph)
        Widget._repr_widget_(self, *pargs, **kwargs)
        
        
    def _send_dict_changes(self, eventful_dict, dict_name):
        def key_add(key, value):
            self.send({'dict': dict_name, 'action': 'add', 'key': key, 'value': value})
        def key_set(key, value):
            self.send({'dict': dict_name, 'action': 'set', 'key': key, 'value': value})
        def key_del(key):
            self.send({'dict': dict_name, 'action': 'del', 'key': key})
        eventful_dict.on_add(key_add)
        eventful_dict.on_set(key_set)
        eventful_dict.on_del(key_del)
            
In [28]:
%%javascript

require(["notebook/js/widget"], function(){
    
    // Define the ForceDirectedGraphModel and register it with the widget manager.
    var ForceDirectedGraphModel = IPython.WidgetModel.extend({});
    IPython.widget_manager.register_widget_model('ForceDirectedGraphModel', ForceDirectedGraphModel);
    
    // Define the D3ForceDirectedGraphView
    var D3ForceDirectedGraphView = IPython.WidgetView.extend({
        
        render: function(){
            this.guid = 'd3force' + IPython.utils.uuid();
            this.setElement($('<div />', {id: this.guid}));
            this.model.on_msg($.proxy(this.handle_msg, this));
        },
        
        add_node: function(id){
            var index = this.find_node(id);
            if (index == -1) {
                var node = {id: id};
                this.nodes.push(node);
                return node;
            } else {
                return this.nodes[index];
            }
        },
                
        remove_node: function(id){
            var found_index = this.find_node(id);
            if (found_index>=0) {
                this.nodes.splice(found_index, 1);
            }
            
            clear_links(id);
        },
        
        find_node: function(id){
            var found_index = -1;
            for (var index in this.nodes) {
                if (this.nodes[index].id == id) {
                    found_index = index;
                    break;
                }
            }
            return found_index;
        },
        
        clear_links: function(id){
            
            // Remove existing links
            var found_indexs = [];
            for (var index in this.links) {
                if (this.links[index].source.id == id) {
                    found_indexs.push(index);
                }
            }
            
            for (var index in found_indexs) {
                this.links.splice(found_indexs[index], 1);
            }
        },
        
        handle_msg: function(content){
            var dict = content.dict;
            var action = content.action;
            var key = content.key;
            console.log(dict, action, key);
            
            if (dict=='node') {
                
                // Only support node ADD and DEL actions for now...
                if (action=='add') {
                    this.add_node(key)
                } else if (action=='del') {
                    this.remove_node(key);
                }
                
            } else if (dict=='adj') {
                this.clear_links(key);
                
                // Add all links
                if (action != 'del') {
                    var value = content.value;
                    for (var link_to in value) {
                        var source_node = this.add_node(key);
                        var target_node = this.add_node(link_to);
                        this.links.push({source: source_node, target: target_node});
                    }
                }
            }
            this.start();
        },
        
        start: function() {
            var node = this.svg.selectAll(".node"),
                link = this.svg.selectAll(".link");
            
            var link = link.data(this.force.links(), function(d) { return d.source.id + "-" + d.target.id; });
            link.enter()
                .insert("line", ".node")
                .attr("class", "link")
                .style("stroke-width", '1.5px')
                .style('stroke', '#999');
            link.exit().remove();
            
            var node = node.data(this.force.nodes(), function(d) { return d.id;});
            var that = this;
            node.enter()
                .append("circle")
                .attr("class", function(d) { return "node " + d.id; })
                .attr("r", 8)
                .style("fill", function(d) { return that.color(d.group); })
                .style("stroke", "#fff")
                .style("stroke-width", "1.5px")
                .call(this.force.drag);
            node.exit().remove();
            
            this.force.start();
        },
        
        tick: function() {
            var node = this.svg.selectAll(".node"),
                link = this.svg.selectAll(".link");
            
            link.attr("x1", function(d) { return d.source.x; })
                .attr("y1", function(d) { return d.source.y; })
                .attr("x2", function(d) { return d.target.x; })
                .attr("y2", function(d) { return d.target.y; });
        
            node.attr("cx", function(d) { return d.x; })
                .attr("cy", function(d) { return d.y; });
        },
        
        update: function(){
            var initial_json = this.model.get('initial_json');
            if (this.initial_json != initial_json) {
                this.initial_json = initial_json;
                
                var width = 860,
                    height = 400;
                
                this.color = d3.scale.category20();
                
                var graph = JSON.parse(initial_json);
                this.nodes = [];
                $.extend(this.nodes, graph.nodes);
                this.links = [];
                $.extend(this.links, graph.links);
                
                var force = d3.layout.force()
                    .nodes(this.nodes)
                    .links(this.links)
                    .charge(-120)
                    .linkDistance(30)
                    .size([width, height])
                    .on("tick", $.proxy(this.tick, this));
                this.force = force;
                
                var svg = d3.select("#" + this.guid).append("svg")
                    .attr("width", width)
                    .attr("height", height);
                this.svg = svg;
                
                var that = this;
                setTimeout(function() {
                    that.start();
                }, 0);
            }
            
            return IPython.WidgetView.prototype.update.call(this);
        },
        
    });
        
    // Register the D3ForceDirectedGraphView with the widget manager.
    IPython.widget_manager.register_widget_view('D3ForceDirectedGraphView', D3ForceDirectedGraphView);
});
SandBoxed(IPython.core.display.Javascript object)
In [30]:
G = EventfulGraph()
G.add_node('hello')
G.add_node('goodbye')
G.add_edges_from([(1,2),(1,3)])
to_d3_json(G)
Out[30]:
'{"directed": false, "graph": [], "nodes": [{"id": 1}, {"id": 2}, {"id": 3}, {"id": "hello"}, {"id": "goodbye"}], "links": [{"source": 0, "target": 1}, {"source": 0, "target": 2}], "multigraph": false}'
In [31]:
floating_container = widgets.ContainerWidget()
floating_container.set_css({
    'position': 'relative',
    'left': '0px',
    'top': '0px',
    'z-index': '999',
    'background': '#FFF',
    'opacity': '0.8'
})

d3 = ForceDirectedGraphWidget(G, parent=floating_container)
display(floating_container)

detach_button = widgets.ButtonWidget(description="Detach")
def handle_detach(sender):
    if sender.description == "Detach":
        sender.description = "Attach"
        floating_container.set_css('position', 'absolute')
    else:
        sender.description = "Detach"
        floating_container.set_css('position', 'relative')
detach_button.on_click(handle_detach)
display(detach_button)
In [32]:
G.add_node('beep')
G.add_node('beep2')
In [33]:
G.remove_node('beep')
In [34]:
G.add_edges_from([(4,5),(4,6)])
In [35]:
import time
for i in range(30):
    G.add_edges_from([(7+i, 8+i), (7+i, 9+i)])
    time.sleep(0.1)
In [36]:
for i in range(30):
    G.add_edge(7, i+8)
    time.sleep(0.25)
In [ ]:
G.clear()