├── .gitignore ├── LICENSE ├── README.md ├── d3networkx ├── __init__.py ├── eventful_dict.py ├── eventful_graph.py ├── widget.js └── widget.py ├── examples ├── demo factor.ipynb ├── demo generators.ipynb ├── demo simple.ipynb └── demo twitter.ipynb └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 IPython development team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ipython-d3networkx 2 | 3 | An IPython notebook widget that uses D3.js and NetworkX to make really cool, interactive, force directed graphs. 4 | 5 | ## Installation 6 | 7 | In a terminal/commandline inside this directory, run 8 | 9 | ``` 10 | pip install . 11 | ``` 12 | 13 | For a development install, run 14 | 15 | ``` 16 | pip install -e . 17 | ``` 18 | 19 | ## Examples 20 | 21 | Inside the `examples` directory you'll find some examples of how to use the widget. 22 | 23 | - `demo simple.ipynb` 24 | **Start here** for a demonstration of how the API can be used. 25 | - `demo generators.ipynb` 26 | This example uses built-in NetworkX generators to render some interesting graphs. 27 | - `demo factor.ipynb` 28 | This is a small example that factors a number between 0-100. 29 | - `demo twitter.ipynb` 30 | This example renders Twitter retweets in realtime. 31 | -------------------------------------------------------------------------------- /d3networkx/__init__.py: -------------------------------------------------------------------------------- 1 | from .widget import ForceDirectedGraph 2 | from .eventful_graph import EventfulGraph, empty_eventfulgraph_hook -------------------------------------------------------------------------------- /d3networkx/eventful_dict.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | class EventfulDict(dict): 4 | """Eventful dictionary""" 5 | 6 | def __init__(self, *args, **kwargs): 7 | """Sleep is an optional float that allows you to tell the 8 | dictionary to hang for the given amount of seconds on each 9 | event. This is usefull for animations.""" 10 | self._sleep = kwargs.get('sleep', 0.0) 11 | self._add_callbacks = [] 12 | self._del_callbacks = [] 13 | self._set_callbacks = [] 14 | dict.__init__(self, *args, **kwargs) 15 | 16 | def on_add(self, callback, remove=False): 17 | self._register_callback(self._add_callbacks, callback, remove) 18 | def on_del(self, callback, remove=False): 19 | self._register_callback(self._del_callbacks, callback, remove) 20 | def on_set(self, callback, remove=False): 21 | self._register_callback(self._set_callbacks, callback, remove) 22 | def _register_callback(self, callback_list, callback, remove=False): 23 | if callable(callback): 24 | if remove and callback in callback_list: 25 | callback_list.remove(callback) 26 | elif not remove and not callback in callback_list: 27 | callback_list.append(callback) 28 | else: 29 | raise Exception('Callback must be callable.') 30 | 31 | def _handle_add(self, key, value): 32 | self._try_callbacks(self._add_callbacks, key, value) 33 | self._try_sleep() 34 | def _handle_del(self, key): 35 | self._try_callbacks(self._del_callbacks, key) 36 | self._try_sleep() 37 | def _handle_set(self, key, value): 38 | self._try_callbacks(self._set_callbacks, key, value) 39 | self._try_sleep() 40 | def _try_callbacks(self, callback_list, *pargs, **kwargs): 41 | for callback in callback_list: 42 | callback(*pargs, **kwargs) 43 | 44 | def _try_sleep(self): 45 | if self._sleep > 0.0: 46 | time.sleep(self._sleep) 47 | 48 | def __setitem__(self, key, value): 49 | return_val = None 50 | exists = False 51 | if key in self: 52 | exists = True 53 | 54 | # If the user sets the property to a new dict, make the dict 55 | # eventful and listen to the changes of it ONLY if it is not 56 | # already eventful. Any modification to this new dict will 57 | # fire a set event of the parent dict. 58 | if isinstance(value, dict) and not isinstance(value, EventfulDict): 59 | new_dict = EventfulDict(value) 60 | 61 | def handle_change(*pargs, **kwargs): 62 | self._try_callbacks(self._set_callbacks, key, dict.__getitem__(self, key)) 63 | 64 | new_dict.on_add(handle_change) 65 | new_dict.on_del(handle_change) 66 | new_dict.on_set(handle_change) 67 | return_val = dict.__setitem__(self, key, new_dict) 68 | else: 69 | return_val = dict.__setitem__(self, key, value) 70 | 71 | if exists: 72 | self._handle_set(key, value) 73 | else: 74 | self._handle_add(key, value) 75 | return return_val 76 | 77 | def __delitem__(self, key): 78 | return_val = dict.__delitem__(self, key) 79 | self._handle_del(key) 80 | return return_val 81 | 82 | def pop(self, key): 83 | return_val = dict.pop(self, key) 84 | if key in self: 85 | self._handle_del(key) 86 | return return_val 87 | 88 | def popitem(self): 89 | popped = dict.popitem(self) 90 | if popped is not None and popped[0] is not None: 91 | self._handle_del(popped[0]) 92 | return popped 93 | 94 | def update(self, other_dict): 95 | for (key, value) in other_dict.items(): 96 | self[key] = value 97 | 98 | def clear(self): 99 | for key in list(self.keys()): 100 | del self[key] -------------------------------------------------------------------------------- /d3networkx/eventful_graph.py: -------------------------------------------------------------------------------- 1 | """NetworkX graphs do not have events that can be listened to. In order to 2 | watch the NetworkX graph object for changes a custom eventful graph object must 3 | be created. The custom eventful graph object will inherit from the base graph 4 | object and use special eventful dictionaries instead of standard Python dict 5 | instances. Because NetworkX nests dictionaries inside dictionaries, it's 6 | important that the eventful dictionary is capable of recognizing when a 7 | dictionary value is set to another dictionary instance. When this happens, the 8 | eventful dictionary needs to also make the new dictionary an eventful 9 | dictionary. This allows the eventful dictionary to listen to changes made to 10 | dictionaries within dictionaries.""" 11 | import networkx 12 | from networkx.generators.classic import empty_graph 13 | 14 | from eventful_dict import EventfulDict 15 | 16 | class EventfulGraph(networkx.Graph): 17 | 18 | _constructed_callback = None 19 | 20 | @staticmethod 21 | def on_constructed(callback): 22 | """Register a callback to be called when a graph is constructed.""" 23 | if callback is None or callable(callback): 24 | EventfulGraph._constructed_callback = callback 25 | 26 | def __init__(self, *pargs, **kwargs): 27 | """Initialize a graph with edges, name, graph attributes. 28 | 29 | Parameters 30 | sleep: float 31 | optional float that allows you to tell the 32 | dictionary to hang for the given amount of seconds on each 33 | event. This is usefull for animations.""" 34 | super(EventfulGraph, self).__init__(*pargs, **kwargs) 35 | 36 | # Override internal dictionaries with custom eventful ones. 37 | sleep = kwargs.get('sleep', 0.0) 38 | self.graph = EventfulDict(self.graph, sleep=sleep) 39 | self.node = EventfulDict(self.node, sleep=sleep) 40 | self.adj = EventfulDict(self.adj, sleep=sleep) 41 | 42 | # Notify callback of construction event. 43 | if EventfulGraph._constructed_callback: 44 | EventfulGraph._constructed_callback(self) 45 | 46 | 47 | def empty_eventfulgraph_hook(*pargs, **kwargs): 48 | def wrapped(*wpargs, **wkwargs): 49 | """Wrapper for networkx.generators.classic.empty_graph(...)""" 50 | wkwargs['create_using'] = EventfulGraph(*pargs, **kwargs) 51 | return empty_graph(*wpargs, **wkwargs) 52 | return wrapped 53 | -------------------------------------------------------------------------------- /d3networkx/widget.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | var d3 = require('https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.1/d3.min.js'); 3 | var utils = require('base/js/utils'); 4 | var widget = require('widgets/js/widget'); 5 | 6 | // Define the D3ForceDirectedGraphView 7 | var D3ForceDirectedGraphView = widget.DOMWidgetView.extend({ 8 | 9 | /** 10 | * Render the widget content 11 | */ 12 | render: function(){ 13 | this.guid = 'd3force' + utils.uuid(); 14 | this.setElement($('
', {id: this.guid})); 15 | 16 | this.model.on('msg:custom', this.on_msg, this); 17 | this.has_drawn = false; 18 | 19 | // Wait for element to be added to the DOM 20 | this.once('displayed', this.update, this); 21 | }, 22 | 23 | /** 24 | * Adds a node if it doesn't exist yet 25 | * @param {string} id - node id 26 | * @return {object} node, either the new node or the node that already 27 | * existed with the id specified. 28 | */ 29 | try_add_node: function(id){ 30 | var index = this.find_node(id); 31 | if (index == -1) { 32 | var node = {id: id}; 33 | this.nodes.push(node); 34 | return node; 35 | } else { 36 | return this.nodes[index]; 37 | } 38 | }, 39 | 40 | /** 41 | * Update a node's attributes 42 | * @param {object} node 43 | * @param {object} attributes - dictionary of attribute key/values 44 | */ 45 | update_node: function(node, attributes) { 46 | if (node !== null) { 47 | for (var key in attributes) { 48 | if (attributes.hasOwnProperty(key)) { 49 | node[key] = attributes[key]; 50 | } 51 | } 52 | this._update_circle(d3.select('#' + this.guid + node.id)); 53 | this._update_text(d3.select('#' + this.guid + node.id + '-text')); 54 | } 55 | }, 56 | 57 | /** 58 | * Remove a node by id 59 | * @param {string} id 60 | */ 61 | remove_node: function(id){ 62 | this.remove_links_to(id); 63 | 64 | var found_index = this.find_node(id); 65 | if (found_index != -1) { 66 | this.nodes.splice(found_index, 1); 67 | } 68 | }, 69 | 70 | /** 71 | * Find a node's index by id 72 | * @param {string} id 73 | * @return {integer} node index or -1 if not found 74 | */ 75 | find_node: function(id){ 76 | for (var i = 0; i < this.nodes.length; i++) { 77 | if (this.nodes[i].id == id) return i; 78 | } 79 | return -1; 80 | }, 81 | 82 | /** 83 | * Find a link's index by source and destination node ids. 84 | * @param {string} source_id - source node id 85 | * @param {string} target_id - destination node id 86 | * @return {integer} link index or -1 if not found 87 | */ 88 | find_link: function(source_id, target_id){ 89 | for (var i = 0; i < this.links.length; i++) { 90 | var link = this.links[i]; 91 | if (link.source.id == source_id && link.target.id == target_id) { 92 | return i; 93 | } 94 | } 95 | return -1; 96 | }, 97 | 98 | /** 99 | * Adds a link if the link could not be found. 100 | * @param {string} source_id - source node id 101 | * @param {string} target_id - destination node id 102 | * @return {object} link 103 | */ 104 | try_add_link: function(source_id, target_id){ 105 | var index = this.find_link(source_id, target_id); 106 | if (index == -1) { 107 | var source_node = this.try_add_node(source_id); 108 | var target_node = this.try_add_node(target_id); 109 | var new_link = {source: source_node, target: target_node}; 110 | this.links.push(new_link); 111 | return new_link; 112 | } else { 113 | return this.links[index]; 114 | } 115 | }, 116 | 117 | /** 118 | * Updates a link with attributes 119 | * @param {object} link 120 | * @param {object} attributes - dictionary of attribute key/value pairs 121 | */ 122 | update_link: function(link, attributes){ 123 | if (link) { 124 | for (var key in attributes) { 125 | if (attributes.hasOwnProperty(key)) { 126 | link[key] = attributes[key]; 127 | } 128 | } 129 | this._update_edge(d3.select('#' + this.guid + link.source.id + "-" + link.target.id)); 130 | } 131 | }, 132 | 133 | /** 134 | * Remove links with a given source node id. 135 | * @param {string} source_id - source node id 136 | */ 137 | remove_links: function(source_id){ 138 | var found_indicies = []; 139 | var i; 140 | for (i = 0; i < this.links.length; i++) { 141 | if (this.links[i].source.id == source_id) { 142 | found_indicies.push(i); 143 | } 144 | } 145 | 146 | // Remove the indicies in reverse order. 147 | found_indicies.reverse(); 148 | for (i = 0; i < found_indicies.length; i++) { 149 | this.links.splice(found_indicies[i], 1); 150 | } 151 | }, 152 | 153 | /** 154 | * Remove links to or from a given node id. 155 | * @param {string} id - node id 156 | */ 157 | remove_links_to: function(id){ 158 | var found_indicies = []; 159 | var i; 160 | for (i = 0; i < this.links.length; i++) { 161 | if (this.links[i].source.id == id || this.links[i].target.id == id) { 162 | found_indicies.push(i); 163 | } 164 | } 165 | 166 | // Remove the indicies in reverse order. 167 | found_indicies.reverse(); 168 | for (i = 0; i < found_indicies.length; i++) { 169 | this.links.splice(found_indicies[i], 1); 170 | } 171 | }, 172 | 173 | /** 174 | * Handles custom widget messages 175 | * @param {object} content - msg content 176 | */ 177 | on_msg: function(content){ 178 | this.update(); 179 | 180 | var dict = content.dict; 181 | var action = content.action; 182 | var key = content.key; 183 | 184 | if (dict=='node') { 185 | if (action=='add' || action=='set') { 186 | this.update_node(this.try_add_node(key), content.value); 187 | } else if (action=='del') { 188 | this.remove_node(key); 189 | } 190 | 191 | } else if (dict=='adj') { 192 | if (action=='add' || action=='set') { 193 | var links = content.value; 194 | for (var target_id in links) { 195 | if (links.hasOwnProperty(target_id)) { 196 | this.update_link(this.try_add_link(key, target_id), links[target_id]); 197 | } 198 | } 199 | } else if (action=='del') { 200 | this.remove_links(key); 201 | } 202 | } 203 | this.render_d3(); 204 | }, 205 | 206 | /** 207 | * Render the d3 graph 208 | */ 209 | render_d3: function() { 210 | var node = this.svg.selectAll(".gnode"), 211 | link = this.svg.selectAll(".link"); 212 | 213 | link = link.data(this.force.links(), function(d) { return d.source.id + "-" + d.target.id; }); 214 | this._update_edge(link.enter().insert("line", ".gnode")); 215 | link.exit().remove(); 216 | 217 | node = node.data(this.force.nodes(), function(d) { return d.id;}); 218 | 219 | var gnode = node.enter() 220 | .append("g") 221 | .attr('class', 'gnode') 222 | .call(this.force.drag); 223 | this._update_circle(gnode.append("circle")); 224 | this._update_text(gnode.append("text")); 225 | node.exit().remove(); 226 | 227 | this.force.start(); 228 | }, 229 | 230 | /** 231 | * Updates a d3 rendered circle 232 | * @param {D3Node} circle 233 | */ 234 | _update_circle: function(circle) { 235 | var that = this; 236 | 237 | circle 238 | .attr("id", function(d) { return that.guid + d.id; }) 239 | .attr("class", function(d) { return "node " + d.id; }) 240 | .attr("r", function(d) { 241 | if (d.r === undefined) { 242 | return 8; 243 | } else { 244 | return d.r; 245 | } 246 | 247 | }) 248 | .style("fill", function(d) { 249 | if (d.fill === undefined) { 250 | return that.color(d.group); 251 | } else { 252 | return d.fill; 253 | } 254 | 255 | }) 256 | .style("stroke", function(d) { 257 | if (d.stroke === undefined) { 258 | return "#FFF"; 259 | } else { 260 | return d.stroke; 261 | } 262 | 263 | }) 264 | .style("stroke-width", function(d) { 265 | if (d.strokewidth === undefined) { 266 | return "#FFF"; 267 | } else { 268 | return d.strokewidth; 269 | } 270 | 271 | }) 272 | .attr('dx', 0) 273 | .attr('dy', 0); 274 | }, 275 | 276 | /** 277 | * Updates a d3 rendered fragment of text 278 | * @param {D3Node} text 279 | */ 280 | _update_text: function(text) { 281 | var that = this; 282 | 283 | text 284 | .attr("id", function(d) { return that.guid + d.id + '-text'; }) 285 | .text(function(d) { 286 | if (d.label) { 287 | return d.label; 288 | } else { 289 | return ''; 290 | } 291 | }) 292 | .style("font-size",function(d) { 293 | if (d.font_size) { 294 | return d.font_size; 295 | } else { 296 | return '11pt'; 297 | } 298 | }) 299 | .attr("text-anchor", "middle") 300 | .style("fill", function(d) { 301 | if (d.color) { 302 | return d.color; 303 | } else { 304 | return 'white'; 305 | } 306 | }) 307 | .attr('dx', function(d) { 308 | if (d.dx) { 309 | return d.dx; 310 | } else { 311 | return 0; 312 | } 313 | }) 314 | .attr('dy', function(d) { 315 | if (d.dy) { 316 | return d.dy; 317 | } else { 318 | return 5; 319 | } 320 | }) 321 | .style("pointer-events", 'none'); 322 | }, 323 | 324 | /** 325 | * Updates a d3 rendered edge 326 | * @param {D3Node} edge 327 | */ 328 | _update_edge: function(edge) { 329 | var that = this; 330 | edge 331 | .attr("id", function(d) { return that.guid + d.source.id + "-" + d.target.id; }) 332 | .attr("class", "link") 333 | .style("stroke-width", function(d) { 334 | if (d.strokewidth === undefined) { 335 | return "1.5px"; 336 | } else { 337 | return d.strokewidth; 338 | } 339 | 340 | }) 341 | .style('stroke', function(d) { 342 | if (d.stroke === undefined) { 343 | return "#999"; 344 | } else { 345 | return d.stroke; 346 | } 347 | 348 | }); 349 | }, 350 | 351 | /** 352 | * Handles animation 353 | */ 354 | tick: function() { 355 | var gnode = this.svg.selectAll(".gnode"), 356 | link = this.svg.selectAll(".link"); 357 | 358 | link.attr("x1", function(d) { return d.source.x; }) 359 | .attr("y1", function(d) { return d.source.y; }) 360 | .attr("x2", function(d) { return d.target.x; }) 361 | .attr("y2", function(d) { return d.target.y; }); 362 | 363 | // Translate the groups 364 | gnode.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); 365 | }, 366 | 367 | /** 368 | * Handles when the widget traits change. 369 | */ 370 | update: function(){ 371 | if (!this.has_drawn) { 372 | this.has_drawn = true; 373 | var width = this.model.get('width'), 374 | height = this.model.get('height'); 375 | 376 | this.color = d3.scale.category20(); 377 | 378 | this.nodes = []; 379 | this.links = []; 380 | 381 | this.force = d3.layout.force() 382 | .nodes(this.nodes) 383 | .links(this.links) 384 | .charge(function (d) { 385 | if (d.charge === undefined) { 386 | return -280; 387 | } else { 388 | return d.charge; 389 | } 390 | }) 391 | .linkDistance(function (d) { 392 | if (d.distance === undefined) { 393 | return 30; 394 | } else { 395 | return d.distance; 396 | } 397 | }) 398 | .linkStrength(function (d) { 399 | if (d.strength === undefined) { 400 | return 0.3; 401 | } else { 402 | return d.strength; 403 | } 404 | }) 405 | .size([width, height]) 406 | .on("tick", $.proxy(this.tick, this)); 407 | 408 | this.svg = d3.select("#" + this.guid).append("svg") 409 | .attr("width", width) 410 | .attr("height", height); 411 | } 412 | 413 | var that = this; 414 | setTimeout(function() { 415 | that.render_d3(); 416 | }, 0); 417 | return D3ForceDirectedGraphView.__super__.update.apply(this); 418 | }, 419 | 420 | }); 421 | 422 | return { 423 | D3ForceDirectedGraphView: D3ForceDirectedGraphView 424 | }; 425 | }); -------------------------------------------------------------------------------- /d3networkx/widget.py: -------------------------------------------------------------------------------- 1 | from IPython.html import widgets # Widget definitions 2 | from IPython.utils.traitlets import Unicode, CInt, CFloat # Import the base Widget class and the traitlets Unicode class. 3 | 4 | # Define our ForceDirectedGraph and its target model and default view. 5 | class ForceDirectedGraph(widgets.DOMWidget): 6 | _view_module = Unicode('nbextensions/d3networkx/widget', sync=True) 7 | _view_name = Unicode('D3ForceDirectedGraphView', sync=True) 8 | 9 | width = CInt(400, sync=True) 10 | height = CInt(300, sync=True) 11 | charge = CFloat(270., sync=True) 12 | distance = CInt(30., sync=True) 13 | strength = CInt(0.3, sync=True) 14 | 15 | def __init__(self, eventful_graph, *pargs, **kwargs): 16 | widgets.DOMWidget.__init__(self, *pargs, **kwargs) 17 | 18 | self._eventful_graph = eventful_graph 19 | self._send_dict_changes(eventful_graph.graph, 'graph') 20 | self._send_dict_changes(eventful_graph.node, 'node') 21 | self._send_dict_changes(eventful_graph.adj, 'adj') 22 | 23 | def _ipython_display_(self, *pargs, **kwargs): 24 | 25 | # Show the widget, then send the current state 26 | widgets.DOMWidget._ipython_display_(self, *pargs, **kwargs) 27 | for (key, value) in self._eventful_graph.graph.items(): 28 | self.send({'dict': 'graph', 'action': 'add', 'key': key, 'value': value}) 29 | for (key, value) in self._eventful_graph.node.items(): 30 | self.send({'dict': 'node', 'action': 'add', 'key': key, 'value': value}) 31 | for (key, value) in self._eventful_graph.adj.items(): 32 | self.send({'dict': 'adj', 'action': 'add', 'key': key, 'value': value}) 33 | 34 | def _send_dict_changes(self, eventful_dict, dict_name): 35 | def key_add(key, value): 36 | self.send({'dict': dict_name, 'action': 'add', 'key': key, 'value': value}) 37 | def key_set(key, value): 38 | self.send({'dict': dict_name, 'action': 'set', 'key': key, 'value': value}) 39 | def key_del(key): 40 | self.send({'dict': dict_name, 'action': 'del', 'key': key}) 41 | eventful_dict.on_add(key_add) 42 | eventful_dict.on_set(key_set) 43 | eventful_dict.on_del(key_del) 44 | -------------------------------------------------------------------------------- /examples/demo factor.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "from IPython.display import display\n", 12 | "from IPython.html import widgets\n", 13 | "from d3networkx import ForceDirectedGraph, EventfulGraph" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "## Prime Factor Finder" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "When an eventful graph is created, create a widget to view it." 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": { 34 | "collapsed": true 35 | }, 36 | "outputs": [], 37 | "source": [ 38 | "def create_widget(graph):\n", 39 | " display(ForceDirectedGraph(graph))\n", 40 | "EventfulGraph.on_constructed(create_widget)" 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "Code that populates the graph." 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "metadata": { 54 | "collapsed": false 55 | }, 56 | "outputs": [], 57 | "source": [ 58 | "BACKGROUND = '#6B8A87'\n", 59 | "PARENT_COLOR = '#3E5970'\n", 60 | "FACTOR_COLOR = '#424357'\n", 61 | "EDGE_COLOR = '#000000'\n", 62 | "PRIME_COLOR = '#333241'\n", 63 | "CHARGE = -200\n", 64 | "MIN_NODE_RADIUS = 15.0\n", 65 | "START_NODE_RADIUS = 65.0\n", 66 | "\n", 67 | "is_int = lambda x: int(x) == x\n", 68 | "factor = lambda x: [i + 1 for i in range(x-1) if i != 0 and is_int(x / (float(i) + 1.0))]\n", 69 | "calc_node_size = lambda x, start_x: max(float(x)/start_x * START_NODE_RADIUS, MIN_NODE_RADIUS)\n", 70 | "calc_edge_length = lambda x, parent_x, start_x: calc_node_size(x, start_x) + calc_node_size(parent_x, start_x)\n", 71 | " \n", 72 | "def add_node(graph, value, **kwargs):\n", 73 | " graph.add_node(len(graph.node), charge=CHARGE, strokewidth=0, value=value, label=value, font_size='18pt', dy='8', **kwargs)\n", 74 | " return len(graph.node) - 1\n", 75 | " \n", 76 | "def add_child_node(graph, x, number, start_number, parent):\n", 77 | " index = add_node(graph, x, fill=FACTOR_COLOR, r='%.2fpx' % calc_node_size(x, start_number))\n", 78 | " graph.add_edge(index, parent, distance=calc_edge_length(x, number, start_number), stroke=EDGE_COLOR, strokewidth='3px')\n", 79 | " return index\n", 80 | "\n", 81 | "def plot_primes(number, start_number=None, parent=None, graph=None, delay=0.0):\n", 82 | " start_number = start_number or number\n", 83 | " if graph is None:\n", 84 | " graph = EventfulGraph(sleep=delay)\n", 85 | " graph.node.clear()\n", 86 | " parent = parent or add_node(graph, number, fill=PARENT_COLOR, r='%.2fpx' % START_NODE_RADIUS)\n", 87 | " \n", 88 | " factors = factor(number)\n", 89 | " if len(factors) == 0:\n", 90 | " graph.node[parent]['fill'] = PRIME_COLOR\n", 91 | " for x in factors:\n", 92 | " index = add_child_node(graph, x, number, start_number, parent)\n", 93 | " plot_primes(x, start_number, parent=index, graph=graph)" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "metadata": {}, 99 | "source": [ 100 | "GUI for factoring a number." 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": null, 106 | "metadata": { 107 | "collapsed": false 108 | }, 109 | "outputs": [], 110 | "source": [ 111 | "box = widgets.VBox()\n", 112 | "header = widgets.HTML(value=\"

Number Factorizer


\")\n", 113 | "number = widgets.IntSlider(description=\"Number:\", value=100)\n", 114 | "speed = widgets.FloatSlider(description=\"Delay:\", min=0.0, max=0.2, value=0.1, step=0.01)\n", 115 | "\n", 116 | "subbox = widgets.HBox()\n", 117 | "button = widgets.Button(description=\"Calculate\")\n", 118 | "subbox.children = [button]\n", 119 | "\n", 120 | "box.children = [header, number, speed, subbox]\n", 121 | "display(box)\n", 122 | "\n", 123 | "box._dom_classes = ['well', 'well-small']\n", 124 | "\n", 125 | "def handle_caclulate(sender):\n", 126 | " plot_primes(number.value, delay=speed.value)\n", 127 | "button.on_click(handle_caclulate)" 128 | ] 129 | } 130 | ], 131 | "metadata": { 132 | "kernelspec": { 133 | "display_name": "Python 2", 134 | "language": "python", 135 | "name": "python2" 136 | }, 137 | "language_info": { 138 | "codemirror_mode": { 139 | "name": "ipython", 140 | "version": 2 141 | }, 142 | "file_extension": ".py", 143 | "mimetype": "text/x-python", 144 | "name": "python", 145 | "nbconvert_exporter": "python", 146 | "pygments_lexer": "ipython2", 147 | "version": "2.7.6" 148 | } 149 | }, 150 | "nbformat": 4, 151 | "nbformat_minor": 0 152 | } 153 | -------------------------------------------------------------------------------- /examples/demo generators.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "from IPython.html import widgets\n", 12 | "from IPython.display import display\n", 13 | "from d3networkx import ForceDirectedGraph, EventfulGraph, empty_eventfulgraph_hook" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "Hook into the random_graphs NetworkX code. " 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": { 27 | "collapsed": false 28 | }, 29 | "outputs": [], 30 | "source": [ 31 | "from networkx.generators import random_graphs\n", 32 | "from networkx.generators import classic\n", 33 | "\n", 34 | "# Add a listener to the eventful graph's construction method.\n", 35 | "# If an eventful graph is created, build and show a widget\n", 36 | "# for the graph.\n", 37 | "def handle_graph(graph):\n", 38 | " print(graph.graph._sleep)\n", 39 | " graph_widget = ForceDirectedGraph(graph)\n", 40 | " display(graph_widget)\n", 41 | "EventfulGraph.on_constructed(handle_graph)\n", 42 | "\n", 43 | "# Replace the empty graph of the networkx classic module with\n", 44 | "# the eventful graph type.\n", 45 | "random_graphs.empty_graph = empty_eventfulgraph_hook(sleep=0.2)" 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "## Barabasi Albert" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "metadata": { 59 | "collapsed": false 60 | }, 61 | "outputs": [], 62 | "source": [ 63 | "random_graphs.barabasi_albert_graph(15, 1)" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "metadata": { 70 | "collapsed": false 71 | }, 72 | "outputs": [], 73 | "source": [ 74 | "random_graphs.barabasi_albert_graph(15, 2)" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "metadata": { 81 | "collapsed": false 82 | }, 83 | "outputs": [], 84 | "source": [ 85 | "random_graphs.barabasi_albert_graph(10, 5)" 86 | ] 87 | }, 88 | { 89 | "cell_type": "markdown", 90 | "metadata": {}, 91 | "source": [ 92 | "## Newman Watts Strogatz" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "metadata": { 99 | "collapsed": false 100 | }, 101 | "outputs": [], 102 | "source": [ 103 | "random_graphs.newman_watts_strogatz_graph(15, 3, 0.25)" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "## Barbell" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "metadata": { 117 | "collapsed": false 118 | }, 119 | "outputs": [], 120 | "source": [ 121 | "classic.barbell_graph(5,0,create_using=EventfulGraph(sleep=0.1))" 122 | ] 123 | }, 124 | { 125 | "cell_type": "markdown", 126 | "metadata": {}, 127 | "source": [ 128 | "## Circular Ladder" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": null, 134 | "metadata": { 135 | "collapsed": false 136 | }, 137 | "outputs": [], 138 | "source": [ 139 | "classic.circular_ladder_graph(5,create_using=EventfulGraph(sleep=0.1))" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": null, 145 | "metadata": { 146 | "collapsed": false 147 | }, 148 | "outputs": [], 149 | "source": [ 150 | "classic.circular_ladder_graph(10,create_using=EventfulGraph(sleep=0.1))" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": {}, 156 | "source": [ 157 | "## Ladder" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "metadata": { 164 | "collapsed": false 165 | }, 166 | "outputs": [], 167 | "source": [ 168 | "classic.ladder_graph(10,create_using=EventfulGraph(sleep=0.1))" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | "## Star" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": null, 181 | "metadata": { 182 | "collapsed": false 183 | }, 184 | "outputs": [], 185 | "source": [ 186 | "classic.star_graph(10,create_using=EventfulGraph(sleep=0.1))" 187 | ] 188 | } 189 | ], 190 | "metadata": { 191 | "kernelspec": { 192 | "display_name": "Python 2", 193 | "language": "python", 194 | "name": "python2" 195 | }, 196 | "language_info": { 197 | "codemirror_mode": { 198 | "name": "ipython", 199 | "version": 2 200 | }, 201 | "file_extension": ".py", 202 | "mimetype": "text/x-python", 203 | "name": "python", 204 | "nbconvert_exporter": "python", 205 | "pygments_lexer": "ipython2", 206 | "version": "2.7.6" 207 | } 208 | }, 209 | "nbformat": 4, 210 | "nbformat_minor": 0 211 | } 212 | -------------------------------------------------------------------------------- /examples/demo simple.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "from IPython.html import widgets\n", 12 | "from IPython.display import display\n", 13 | "from d3networkx import ForceDirectedGraph, EventfulGraph" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "# Test" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": { 27 | "collapsed": false 28 | }, 29 | "outputs": [], 30 | "source": [ 31 | "G = EventfulGraph()\n", 32 | "d3 = ForceDirectedGraph(G)\n", 33 | "display(d3)" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "The following code creates an animation of some of the plot's properties." 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": null, 46 | "metadata": { 47 | "collapsed": false 48 | }, 49 | "outputs": [], 50 | "source": [ 51 | "# Redisplay\n", 52 | "display(d3)\n", 53 | "\n", 54 | "import time\n", 55 | "G.node.clear()\n", 56 | "G.add_node(1, fill=\"red\", stroke=\"black\", color='black', label='A')\n", 57 | "time.sleep(1.0)\n", 58 | "\n", 59 | "G.add_node(2, fill=\"gold\", stroke=\"black\", color='black', r=20, font_size='24pt', label='B')\n", 60 | "time.sleep(1.0)\n", 61 | "\n", 62 | "G.add_node(3, fill=\"green\", stroke=\"black\", color='white', label='C')\n", 63 | "time.sleep(1.0)\n", 64 | "\n", 65 | "G.add_edges_from([(1,2),(1,3), (2,3)], stroke=\"#aaa\", strokewidth=\"1px\", distance=200, strength=0.5)\n", 66 | "time.sleep(1.0)\n", 67 | "\n", 68 | "G.adj[1][2]['distance'] = 20\n", 69 | "time.sleep(1.0)\n", 70 | "\n", 71 | "G.adj[1][3]['distance'] = 20\n", 72 | "time.sleep(1.0)\n", 73 | "\n", 74 | "G.adj[2][3]['distance'] = 20\n", 75 | "time.sleep(1.0)\n", 76 | "\n", 77 | "G.node[1]['r'] = 16\n", 78 | "time.sleep(0.3)\n", 79 | "G.node[1]['r'] = 8\n", 80 | "G.node[2]['r'] = 16\n", 81 | "time.sleep(0.3)\n", 82 | "G.node[2]['r'] = 20\n", 83 | "G.node[3]['r'] = 16\n", 84 | "time.sleep(0.3)\n", 85 | "G.node[3]['r'] = 8\n", 86 | "\n", 87 | "G.node[1]['fill'] = 'purple'\n", 88 | "time.sleep(0.3)\n", 89 | "G.node[1]['fill'] = 'red'\n", 90 | "G.node[2]['fill'] = 'purple'\n", 91 | "time.sleep(0.3)\n", 92 | "G.node[2]['fill'] = 'gold'\n", 93 | "G.node[3]['fill'] = 'purple'\n", 94 | "time.sleep(0.3)\n", 95 | "G.node[3]['fill'] = 'green'\n", 96 | "time.sleep(1.0)\n", 97 | "\n", 98 | "G.node.clear()" 99 | ] 100 | } 101 | ], 102 | "metadata": { 103 | "kernelspec": { 104 | "display_name": "Python 2", 105 | "language": "python", 106 | "name": "python2" 107 | }, 108 | "language_info": { 109 | "codemirror_mode": { 110 | "name": "ipython", 111 | "version": 2 112 | }, 113 | "file_extension": ".py", 114 | "mimetype": "text/x-python", 115 | "name": "python", 116 | "nbconvert_exporter": "python", 117 | "pygments_lexer": "ipython2", 118 | "version": "2.7.6" 119 | } 120 | }, 121 | "nbformat": 4, 122 | "nbformat_minor": 0 123 | } 124 | -------------------------------------------------------------------------------- /examples/demo twitter.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "from IPython.html import widgets\n", 12 | "from IPython.display import display\n", 13 | "from d3networkx import ForceDirectedGraph, EventfulGraph" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "## Twitter Tweet Watcher" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "This example requires the Python \"twitter\" library to be installed (https://github.com/sixohsix/twitter). You can install Python twitter by running `sudo pip install twitter` from the terminal/commandline." 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": { 34 | "collapsed": false 35 | }, 36 | "outputs": [], 37 | "source": [ 38 | "from twitter import *\n", 39 | "import time, datetime\n", 40 | "import math\n", 41 | "\n", 42 | "twitter_timestamp_format = \"%a %b %d %X +0000 %Y\"" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": { 49 | "collapsed": false 50 | }, 51 | "outputs": [], 52 | "source": [ 53 | "# Sign on to twitter.\n", 54 | "auth = OAuth(\n", 55 | " consumer_key='iQvYfTfuD86fgVWGjPY0UA',\n", 56 | " consumer_secret='C3jjP6vzYzTYoHV4s5NYPGuRkpT5SulKRKTkRmYg',\n", 57 | " token='2218195843-cOPQa0D1Yk3JbvjvsCa0tIYzBOEWxINekmGcEql',\n", 58 | " token_secret='3BFncT1zAvJRN6rj8haCxveZVLZWZ23QeulxzByXWlfoO'\n", 59 | ")\n", 60 | "twitter = Twitter(auth = auth)\n", 61 | "twitter_stream = TwitterStream(auth = auth, block = False)" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": { 68 | "collapsed": false 69 | }, 70 | "outputs": [], 71 | "source": [ 72 | "graph = EventfulGraph()\n", 73 | "d3 = ForceDirectedGraph(graph)\n", 74 | "d3.width = 600\n", 75 | "d3.height = 400\n", 76 | "\n", 77 | "stop_button = widgets.Button(description=\"Stop\")\n", 78 | " \n", 79 | "# Only listen to tweets while they are available and the user\n", 80 | "# doesn't want to stop.\n", 81 | "stop_listening = [False]\n", 82 | "def handle_stop(sender):\n", 83 | " stop_listening[0] = True\n", 84 | " print(\"Service stopped\")\n", 85 | "stop_button.on_click(handle_stop)\n", 86 | "\n", 87 | "def watch_tweets(screen_name=None):\n", 88 | " display(d3)\n", 89 | " display(stop_button)\n", 90 | " graph.node.clear()\n", 91 | " graph.adj.clear()\n", 92 | " start_timestamp = None\n", 93 | " stop_button._dom_classes = ['btn', 'btn-danger']\n", 94 | " \n", 95 | " # Get Barack's tweets\n", 96 | " tweets = twitter.statuses.user_timeline(screen_name=screen_name)\n", 97 | " user_id = twitter.users.lookup(screen_name=screen_name)[0]['id']\n", 98 | " \n", 99 | " # Determine the maximum number of retweets.\n", 100 | " max_retweets = 0.0\n", 101 | " for tweet in tweets:\n", 102 | " max_retweets = float(max(tweet['retweet_count'], max_retweets))\n", 103 | " \n", 104 | " \n", 105 | " def plot_tweet(tweet, parent=None, elapsed_seconds=1.0, color=\"gold\"):\n", 106 | " new_id = tweet['id']\n", 107 | " graph.add_node(\n", 108 | " new_id, \n", 109 | " r=max(float(tweet['retweet_count']) / max_retweets * 30.0, 3.0),\n", 110 | " charge=-60,\n", 111 | " fill = color,\n", 112 | " )\n", 113 | " \n", 114 | " if parent is not None:\n", 115 | " parent_radius = max(float(parent['retweet_count']) / max_retweets * 30.0, 3.0)\n", 116 | " graph.node[parent['id']]['r'] = parent_radius\n", 117 | " \n", 118 | " graph.add_edge(new_id, parent['id'], distance=math.log(elapsed_seconds) * 9.0 + parent_radius)\n", 119 | " graph.node[new_id]['fill'] = 'red'\n", 120 | " \n", 121 | " \n", 122 | " # Plot each tweet.\n", 123 | " for tweet in tweets:\n", 124 | " plot_tweet(tweet)\n", 125 | " \n", 126 | " kernel=get_ipython().kernel\n", 127 | " iterator = twitter_stream.statuses.filter(follow=user_id)\n", 128 | " \n", 129 | " while not stop_listening[0]:\n", 130 | " kernel.do_one_iteration()\n", 131 | " \n", 132 | " for tweet in iterator:\n", 133 | " kernel.do_one_iteration()\n", 134 | " if stop_listening[0] or tweet is None:\n", 135 | " break\n", 136 | " else:\n", 137 | " if 'retweeted_status' in tweet:\n", 138 | " original_tweet = tweet['retweeted_status']\n", 139 | " if original_tweet['id'] in graph.node:\n", 140 | " tweet_timestamp = datetime.datetime.strptime(tweet['created_at'], twitter_timestamp_format) \n", 141 | " if start_timestamp is None:\n", 142 | " start_timestamp = tweet_timestamp\n", 143 | " elapsed_seconds = max((tweet_timestamp - start_timestamp).total_seconds(),1.0)\n", 144 | " \n", 145 | " plot_tweet(tweet, parent=original_tweet, elapsed_seconds=elapsed_seconds)\n", 146 | " elif 'id' in tweet:\n", 147 | " plot_tweet(tweet, color='lime')\n" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": null, 153 | "metadata": { 154 | "collapsed": false 155 | }, 156 | "outputs": [], 157 | "source": [ 158 | "watch_tweets(screen_name=\"justinbieber\")" 159 | ] 160 | } 161 | ], 162 | "metadata": { 163 | "kernelspec": { 164 | "display_name": "Python 2", 165 | "language": "python", 166 | "name": "python2" 167 | }, 168 | "language_info": { 169 | "codemirror_mode": { 170 | "name": "ipython", 171 | "version": 2 172 | }, 173 | "file_extension": ".py", 174 | "mimetype": "text/x-python", 175 | "name": "python", 176 | "nbconvert_exporter": "python", 177 | "pygments_lexer": "ipython2", 178 | "version": "2.7.6" 179 | } 180 | }, 181 | "nbformat": 4, 182 | "nbformat_minor": 0 183 | } 184 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | from setuptools import setup 4 | try: 5 | from ipythonpip import cmdclass 6 | except: 7 | import pip, importlib 8 | pip.main(['install', 'ipython-pip']); cmdclass = importlib.import_module('ipythonpip').cmdclass 9 | 10 | setup( 11 | name='d3networkx', 12 | version='0.1', 13 | description='Visualize networkx graphs using D3.js in the IPython notebook.', 14 | author='Jonathan Frederic', 15 | author_email='jon.freder@gmail.com', 16 | license='MIT License', 17 | url='https://github.com/jdfreder/ipython-d3networkx', 18 | keywords='python ipython javascript d3 networkx d3networkx widget', 19 | classifiers=['Development Status :: 4 - Beta', 20 | 'Programming Language :: Python', 21 | 'License :: OSI Approved :: MIT License'], 22 | packages=['d3networkx'], 23 | include_package_data=True, 24 | install_requires=["ipython-pip"], 25 | cmdclass=cmdclass('d3networkx'), 26 | ) 27 | --------------------------------------------------------------------------------