D3.ipynb
1232 lines
| 39.3 KiB
| text/plain
|
TextLexer
In [1]:
from __future__ import print_function # py 2.7 compat
Validate NetworkX version¶
In [2]:
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 [3]:
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 [4]:
G = nx.Graph([(1,2)])
to_d3_json(G)
Out[4]:
In [5]:
G.add_node('test')
to_d3_json(G)
Out[5]:
Listen To Graph Changes¶
Create a simple eventfull dictionary.
In [6]:
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 [7]:
a = EventfulDict()
In [8]:
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 [9]:
a['a'] = 'hello'
In [10]:
a['a'] = 'goodbye'
In [11]:
b = {'c': 'yay', 'd': 'no'}
a.update(b)
In [12]:
a
Out[12]:
In [13]:
a.pop('a')
Out[13]:
In [14]:
a.popitem()
Out[14]:
In [15]:
a['e'] = {}
In [16]:
a['e']['a'] = 0
In [17]:
a['e']['b'] = 1
In [18]:
a.clear()
Override the NetworkX Graph object to make an eventful graph.
In [19]:
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 [20]:
def echo_graph_events(eventful_graph):
for key in ['graph', 'node', 'adj']:
echo_dict_events(getattr(eventful_graph, key), prefix=key+' ')
In [21]:
G = EventfulGraph()
echo_graph_events(G)
In [22]:
to_d3_json(G)
Out[22]:
In [23]:
G.add_node('hello')
G.add_node('goodbye')
G.add_edges_from([(1,2),(1,3)])
to_d3_json(G)
Out[23]:
In [24]:
G.adj
Out[24]:
In [25]:
G.node
Out[25]:
Custom Widget¶
In [26]:
%%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>
In [27]:
from IPython.html import widgets # Widget definitions
from IPython.display import display # Used to display widgets in the notebook
In [28]:
# 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 [29]:
%%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);
});
In [36]:
G = EventfulGraph()
G.add_node('hello')
G.add_node('goodbye')
G.add_edges_from([(1,2),(1,3)])
to_d3_json(G)
Out[36]:
In [37]:
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 [38]:
G.add_node('beep')
G.add_node('beep2')
In [39]:
G.remove_node('beep')
In [40]:
G.add_edges_from([(4,5),(4,6)])
In [41]:
import time
for i in range(30):
G.add_edges_from([(7+i, 8+i), (7+i, 9+i)])
time.sleep(0.1)
In [42]:
for i in range(30):
G.add_edge(7, i+8)
time.sleep(0.25)
In [ ]:
G.clear()