diff --git a/examples/widgets/D3.ipynb b/examples/widgets/D3.ipynb
new file mode 100644
index 0000000..ae9963e
--- /dev/null
+++ b/examples/widgets/D3.ipynb
@@ -0,0 +1,1222 @@
+{
+ "metadata": {
+ "name": ""
+ },
+ "nbformat": 3,
+ "nbformat_minor": 0,
+ "worksheets": [
+ {
+ "cells": [
+ {
+ "cell_type": "heading",
+ "level": 1,
+ "metadata": {},
+ "source": [
+ "Validate NetworkX version"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "import networkx as nx\n",
+ "version = float('.'.join(nx.__version__.split('.')[0:2]))\n",
+ "if version < 1.8:\n",
+ " raise Exception('This notebook requires networkx version 1.8 or later. Version %s is installed on this machine.' % nx.__version__)"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 1
+ },
+ {
+ "cell_type": "heading",
+ "level": 1,
+ "metadata": {},
+ "source": [
+ "Simple Output Test"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "from networkx.readwrite import json_graph\n",
+ "import json\n",
+ "\n",
+ "def to_d3_json(graph):\n",
+ " data = json_graph.node_link_data(graph)\n",
+ " return json.dumps(data)"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 2
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "G = nx.Graph([(1,2)])\n",
+ "to_d3_json(G)"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "metadata": {},
+ "output_type": "pyout",
+ "prompt_number": 3,
+ "text": [
+ "'{\"directed\": false, \"graph\": [], \"nodes\": [{\"id\": 1}, {\"id\": 2}], \"links\": [{\"source\": 0, \"target\": 1}], \"multigraph\": false}'"
+ ]
+ }
+ ],
+ "prompt_number": 3
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "G.add_node('test')\n",
+ "to_d3_json(G)"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "metadata": {},
+ "output_type": "pyout",
+ "prompt_number": 4,
+ "text": [
+ "'{\"directed\": false, \"graph\": [], \"nodes\": [{\"id\": \"test\"}, {\"id\": 1}, {\"id\": 2}], \"links\": [{\"source\": 1, \"target\": 2}], \"multigraph\": false}'"
+ ]
+ }
+ ],
+ "prompt_number": 4
+ },
+ {
+ "cell_type": "heading",
+ "level": 1,
+ "metadata": {},
+ "source": [
+ "Listen To Graph Changes"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Create a simple eventfull dictionary."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "class EventfulDict(dict):\n",
+ " \n",
+ " def __init__(self, *args, **kwargs):\n",
+ " self._add_callbacks = []\n",
+ " self._del_callbacks = []\n",
+ " self._set_callbacks = []\n",
+ " dict.__init__(self, *args, **kwargs)\n",
+ " \n",
+ " def on_add(self, callback, remove=False):\n",
+ " self._register_callback(self._add_callbacks, callback, remove)\n",
+ " def on_del(self, callback, remove=False):\n",
+ " self._register_callback(self._del_callbacks, callback, remove)\n",
+ " def on_set(self, callback, remove=False):\n",
+ " self._register_callback(self._set_callbacks, callback, remove)\n",
+ " def _register_callback(self, callback_list, callback, remove=False):\n",
+ " if callable(callback):\n",
+ " if remove and callback in callback_list:\n",
+ " callback_list.remove(callback)\n",
+ " elif not remove and not callback in callback_list:\n",
+ " callback_list.append(callback)\n",
+ " else:\n",
+ " raise Exception('Callback must be callable.')\n",
+ "\n",
+ " def _handle_add(self, key, value):\n",
+ " self._try_callbacks(self._add_callbacks, key, value)\n",
+ " def _handle_del(self, key):\n",
+ " self._try_callbacks(self._del_callbacks, key)\n",
+ " def _handle_set(self, key, value):\n",
+ " self._try_callbacks(self._set_callbacks, key, value)\n",
+ " def _try_callbacks(self, callback_list, *pargs, **kwargs):\n",
+ " for callback in callback_list:\n",
+ " callback(*pargs, **kwargs)\n",
+ " \n",
+ " def __setitem__(self, key, value):\n",
+ " return_val = None\n",
+ " exists = False\n",
+ " if key in self:\n",
+ " exists = True\n",
+ " \n",
+ " # If the user sets the property to a new dict, make the dict\n",
+ " # eventful and listen to the changes of it ONLY if it is not\n",
+ " # already eventful. Any modification to this new dict will\n",
+ " # fire a set event of the parent dict.\n",
+ " if isinstance(value, dict) and not isinstance(value, EventfulDict):\n",
+ " new_dict = EventfulDict(value)\n",
+ " \n",
+ " def handle_change(*pargs, **kwargs):\n",
+ " self._try_callbacks(self._set_callbacks, key, dict.__getitem__(self, key))\n",
+ " \n",
+ " new_dict.on_add(handle_change)\n",
+ " new_dict.on_del(handle_change)\n",
+ " new_dict.on_set(handle_change)\n",
+ " return_val = dict.__setitem__(self, key, new_dict)\n",
+ " else:\n",
+ " return_val = dict.__setitem__(self, key, value)\n",
+ " \n",
+ " if exists:\n",
+ " self._handle_set(key, value)\n",
+ " else:\n",
+ " self._handle_add(key, value)\n",
+ " return return_val\n",
+ " \n",
+ "\n",
+ " def __delitem__(self, key):\n",
+ " return_val = dict.__delitem__(self, key)\n",
+ " self._handle_del(key)\n",
+ " return return_val\n",
+ "\n",
+ " \n",
+ " def pop(self, key):\n",
+ " return_val = dict.pop(self, key)\n",
+ " if key in self:\n",
+ " self._handle_del(key)\n",
+ " return return_val\n",
+ "\n",
+ " def popitem(self):\n",
+ " popped = dict.popitem(self)\n",
+ " if popped is not None and popped[0] is not None:\n",
+ " self._handle_del(popped[0])\n",
+ " return popped\n",
+ "\n",
+ " def update(self, other_dict):\n",
+ " for (key, value) in other_dict.items():\n",
+ " self[key] = value\n",
+ " \n",
+ " def clear(self):\n",
+ " for key in list(self.keys()):\n",
+ " del self[key]\n"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 5
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Test the eventful dictionary."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "a = EventfulDict()"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 6
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "def echo_dict_events(eventful_dict, prefix=''):\n",
+ " def key_add(key, value):\n",
+ " print prefix + 'add (%s, %s)' % (key, str(value))\n",
+ " def key_set(key, value):\n",
+ " print prefix + 'set (%s, %s)' % (key, str(value))\n",
+ " def key_del(key):\n",
+ " print prefix + 'del %s' % key\n",
+ " eventful_dict.on_add(key_add)\n",
+ " eventful_dict.on_set(key_set)\n",
+ " eventful_dict.on_del(key_del)\n",
+ " \n",
+ "echo_dict_events(a)"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 7
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "a['a'] = 'hello'"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "stream",
+ "stream": "stdout",
+ "text": [
+ "add (a, hello)\n"
+ ]
+ }
+ ],
+ "prompt_number": 8
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "a['a'] = 'goodbye'"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "stream",
+ "stream": "stdout",
+ "text": [
+ "set (a, goodbye)\n"
+ ]
+ }
+ ],
+ "prompt_number": 9
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "b = {'c': 'yay', 'd': 'no'}\n",
+ "a.update(b)"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "stream",
+ "stream": "stdout",
+ "text": [
+ "add (c, yay)\n",
+ "add (d, no)\n"
+ ]
+ }
+ ],
+ "prompt_number": 10
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "a"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "metadata": {},
+ "output_type": "pyout",
+ "prompt_number": 11,
+ "text": [
+ "{'a': 'goodbye', 'c': 'yay', 'd': 'no'}"
+ ]
+ }
+ ],
+ "prompt_number": 11
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "a.pop('a')"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "metadata": {},
+ "output_type": "pyout",
+ "prompt_number": 12,
+ "text": [
+ "'goodbye'"
+ ]
+ }
+ ],
+ "prompt_number": 12
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "a.popitem()"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "stream",
+ "stream": "stdout",
+ "text": [
+ "del c\n"
+ ]
+ },
+ {
+ "metadata": {},
+ "output_type": "pyout",
+ "prompt_number": 13,
+ "text": [
+ "('c', 'yay')"
+ ]
+ }
+ ],
+ "prompt_number": 13
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "a['e'] = {}"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "stream",
+ "stream": "stdout",
+ "text": [
+ "add (e, {})\n"
+ ]
+ }
+ ],
+ "prompt_number": 14
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "a['e']['a'] = 0"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "stream",
+ "stream": "stdout",
+ "text": [
+ "set (e, {'a': 0})\n"
+ ]
+ }
+ ],
+ "prompt_number": 15
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "a['e']['b'] = 1"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "stream",
+ "stream": "stdout",
+ "text": [
+ "set (e, {'a': 0, 'b': 1})\n"
+ ]
+ }
+ ],
+ "prompt_number": 16
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "a.clear()"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "stream",
+ "stream": "stdout",
+ "text": [
+ "del e\n",
+ "del d\n"
+ ]
+ }
+ ],
+ "prompt_number": 17
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Override the NetworkX Graph object to make an eventful graph."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "class EventfulGraph(nx.Graph):\n",
+ " \n",
+ " def __init__(self, *pargs, **kwargs):\n",
+ " \"\"\"Initialize a graph with edges, name, graph attributes.\"\"\"\n",
+ " super(EventfulGraph, self).__init__(*pargs, **kwargs)\n",
+ " \n",
+ " self.graph = EventfulDict(self.graph)\n",
+ " self.node = EventfulDict(self.node)\n",
+ " self.adj = EventfulDict(self.adj)\n",
+ " "
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 18
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "def echo_graph_events(eventful_graph):\n",
+ " for key in ['graph', 'node', 'adj']:\n",
+ " echo_dict_events(getattr(eventful_graph, key), prefix=key+' ')"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 19
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "G = EventfulGraph()\n",
+ "echo_graph_events(G)"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 20
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "to_d3_json(G)"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "metadata": {},
+ "output_type": "pyout",
+ "prompt_number": 21,
+ "text": [
+ "'{\"directed\": false, \"graph\": [], \"nodes\": [], \"links\": [], \"multigraph\": false}'"
+ ]
+ }
+ ],
+ "prompt_number": 21
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "G.add_node('hello')\n",
+ "G.add_node('goodbye')\n",
+ "G.add_edges_from([(1,2),(1,3)])\n",
+ "to_d3_json(G)"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "stream",
+ "stream": "stdout",
+ "text": [
+ "adj add (hello, {})\n",
+ "node add (hello, {})\n",
+ "adj add (goodbye, {})\n",
+ "node add (goodbye, {})\n",
+ "adj add (1, {})\n",
+ "node add (1, {})\n",
+ "adj add (2, {})\n",
+ "node add (2, {})\n",
+ "adj set (1, {2: {}})\n",
+ "adj set (2, {1: {}})\n",
+ "adj add (3, {})\n",
+ "node add (3, {})\n",
+ "adj set (1, {2: {}, 3: {}})\n",
+ "adj set (3, {1: {}})\n"
+ ]
+ },
+ {
+ "metadata": {},
+ "output_type": "pyout",
+ "prompt_number": 22,
+ "text": [
+ "'{\"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}'"
+ ]
+ }
+ ],
+ "prompt_number": 22
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "G.adj"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "metadata": {},
+ "output_type": "pyout",
+ "prompt_number": 23,
+ "text": [
+ "{1: {2: {}, 3: {}}, 2: {1: {}}, 3: {1: {}}, 'goodbye': {}, 'hello': {}}"
+ ]
+ }
+ ],
+ "prompt_number": 23
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "G.node"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "metadata": {},
+ "output_type": "pyout",
+ "prompt_number": 24,
+ "text": [
+ "{1: {}, 2: {}, 3: {}, 'goodbye': {}, 'hello': {}}"
+ ]
+ }
+ ],
+ "prompt_number": 24
+ },
+ {
+ "cell_type": "heading",
+ "level": 1,
+ "metadata": {},
+ "source": [
+ "Custom Widget"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "%%html\n",
+ "
Loading D3...\n",
+ ""
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "html": [
+ "Loading D3...\n",
+ ""
+ ],
+ "metadata": {},
+ "output_type": "display_data",
+ "text": [
+ ""
+ ]
+ }
+ ],
+ "prompt_number": 25
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "from IPython.html import widgets # Widget definitions\n",
+ "from IPython.display import display # Used to display widgets in the notebook"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 26
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "# Import the base Widget class and the traitlets Unicode class.\n",
+ "from IPython.html.widgets import Widget\n",
+ "from IPython.utils.traitlets import Unicode\n",
+ "\n",
+ "# Define our ForceDirectedGraphWidget and its target model and default view.\n",
+ "class ForceDirectedGraphWidget(Widget):\n",
+ " target_name = Unicode('ForceDirectedGraphModel')\n",
+ " default_view_name = Unicode('D3ForceDirectedGraphView')\n",
+ " \n",
+ " _keys = ['initial_json']\n",
+ " initial_json = Unicode()\n",
+ " \n",
+ " def __init__(self, eventful_graph, *pargs, **kwargs):\n",
+ " Widget.__init__(self, *pargs, **kwargs)\n",
+ " \n",
+ " self._eventful_graph = eventful_graph\n",
+ " self._send_dict_changes(eventful_graph.graph, 'graph')\n",
+ " self._send_dict_changes(eventful_graph.node, 'node')\n",
+ " self._send_dict_changes(eventful_graph.adj, 'adj')\n",
+ " \n",
+ " \n",
+ " def _repr_widget_(self, *pargs, **kwargs):\n",
+ " self.initial_json = to_d3_json(self._eventful_graph)\n",
+ " Widget._repr_widget_(self, *pargs, **kwargs)\n",
+ " \n",
+ " \n",
+ " def _send_dict_changes(self, eventful_dict, dict_name):\n",
+ " def key_add(key, value):\n",
+ " self.send({'dict': dict_name, 'action': 'add', 'key': key, 'value': value})\n",
+ " def key_set(key, value):\n",
+ " self.send({'dict': dict_name, 'action': 'set', 'key': key, 'value': value})\n",
+ " def key_del(key):\n",
+ " self.send({'dict': dict_name, 'action': 'del', 'key': key})\n",
+ " eventful_dict.on_add(key_add)\n",
+ " eventful_dict.on_set(key_set)\n",
+ " eventful_dict.on_del(key_del)\n",
+ " "
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 27
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "%%javascript\n",
+ "\n",
+ "require([\"notebook/js/widget\"], function(){\n",
+ " \n",
+ " // Define the ForceDirectedGraphModel and register it with the widget manager.\n",
+ " var ForceDirectedGraphModel = IPython.WidgetModel.extend({});\n",
+ " IPython.widget_manager.register_widget_model('ForceDirectedGraphModel', ForceDirectedGraphModel);\n",
+ " \n",
+ " // Define the D3ForceDirectedGraphView\n",
+ " var D3ForceDirectedGraphView = IPython.WidgetView.extend({\n",
+ " \n",
+ " render: function(){\n",
+ " this.guid = 'd3force' + IPython.utils.uuid();\n",
+ " this.setElement($('', {id: this.guid}));\n",
+ " this.model.on_msg($.proxy(this.handle_msg, this));\n",
+ " },\n",
+ " \n",
+ " add_node: function(id){\n",
+ " var index = this.find_node(id);\n",
+ " if (index == -1) {\n",
+ " var node = {id: id};\n",
+ " this.nodes.push(node);\n",
+ " return node;\n",
+ " } else {\n",
+ " return this.nodes[index];\n",
+ " }\n",
+ " },\n",
+ " \n",
+ " remove_node: function(id){\n",
+ " var found_index = this.find_node(id);\n",
+ " if (found_index>=0) {\n",
+ " this.nodes.splice(found_index, 1);\n",
+ " }\n",
+ " \n",
+ " clear_links(id);\n",
+ " },\n",
+ " \n",
+ " find_node: function(id){\n",
+ " var found_index = -1;\n",
+ " for (var index in this.nodes) {\n",
+ " if (this.nodes[index].id == id) {\n",
+ " found_index = index;\n",
+ " break;\n",
+ " }\n",
+ " }\n",
+ " return found_index;\n",
+ " },\n",
+ " \n",
+ " clear_links: function(id){\n",
+ " \n",
+ " // Remove existing links\n",
+ " var found_indexs = [];\n",
+ " for (var index in this.links) {\n",
+ " if (this.links[index].source.id == id) {\n",
+ " found_indexs.push(index);\n",
+ " }\n",
+ " }\n",
+ " \n",
+ " for (var index in found_indexs) {\n",
+ " this.links.splice(found_indexs[index], 1);\n",
+ " }\n",
+ " },\n",
+ " \n",
+ " handle_msg: function(content){\n",
+ " var dict = content.dict;\n",
+ " var action = content.action;\n",
+ " var key = content.key;\n",
+ " console.log(dict, action, key);\n",
+ " \n",
+ " if (dict=='node') {\n",
+ " \n",
+ " // Only support node ADD and DEL actions for now...\n",
+ " if (action=='add') {\n",
+ " this.add_node(key)\n",
+ " } else if (action=='del') {\n",
+ " this.remove_node(key);\n",
+ " }\n",
+ " \n",
+ " } else if (dict=='adj') {\n",
+ " this.clear_links(key);\n",
+ " \n",
+ " // Add all links\n",
+ " if (action != 'del') {\n",
+ " var value = content.value;\n",
+ " for (var link_to in value) {\n",
+ " var source_node = this.add_node(key);\n",
+ " var target_node = this.add_node(link_to);\n",
+ " this.links.push({source: source_node, target: target_node});\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " this.start();\n",
+ " },\n",
+ " \n",
+ " start: function() {\n",
+ " var node = this.svg.selectAll(\".node\"),\n",
+ " link = this.svg.selectAll(\".link\");\n",
+ " \n",
+ " var link = link.data(this.force.links(), function(d) { return d.source.id + \"-\" + d.target.id; });\n",
+ " link.enter()\n",
+ " .insert(\"line\", \".node\")\n",
+ " .attr(\"class\", \"link\")\n",
+ " .style(\"stroke-width\", '1.5px')\n",
+ " .style('stroke', '#999');\n",
+ " link.exit().remove();\n",
+ " \n",
+ " var node = node.data(this.force.nodes(), function(d) { return d.id;});\n",
+ " var that = this;\n",
+ " node.enter()\n",
+ " .append(\"circle\")\n",
+ " .attr(\"class\", function(d) { return \"node \" + d.id; })\n",
+ " .attr(\"r\", 8)\n",
+ " .style(\"fill\", function(d) { return that.color(d.group); })\n",
+ " .style(\"stroke\", \"#fff\")\n",
+ " .style(\"stroke-width\", \"1.5px\")\n",
+ " .call(this.force.drag);\n",
+ " node.exit().remove();\n",
+ " \n",
+ " this.force.start();\n",
+ " },\n",
+ " \n",
+ " tick: function() {\n",
+ " var node = this.svg.selectAll(\".node\"),\n",
+ " link = this.svg.selectAll(\".link\");\n",
+ " \n",
+ " link.attr(\"x1\", function(d) { return d.source.x; })\n",
+ " .attr(\"y1\", function(d) { return d.source.y; })\n",
+ " .attr(\"x2\", function(d) { return d.target.x; })\n",
+ " .attr(\"y2\", function(d) { return d.target.y; });\n",
+ " \n",
+ " node.attr(\"cx\", function(d) { return d.x; })\n",
+ " .attr(\"cy\", function(d) { return d.y; });\n",
+ " },\n",
+ " \n",
+ " update: function(){\n",
+ " var initial_json = this.model.get('initial_json');\n",
+ " if (this.initial_json != initial_json) {\n",
+ " this.initial_json = initial_json;\n",
+ " \n",
+ " var width = 860,\n",
+ " height = 400;\n",
+ " \n",
+ " this.color = d3.scale.category20();\n",
+ " \n",
+ " var graph = JSON.parse(initial_json);\n",
+ " this.nodes = [];\n",
+ " $.extend(this.nodes, graph.nodes);\n",
+ " this.links = [];\n",
+ " $.extend(this.links, graph.links);\n",
+ " \n",
+ " var force = d3.layout.force()\n",
+ " .nodes(this.nodes)\n",
+ " .links(this.links)\n",
+ " .charge(-120)\n",
+ " .linkDistance(30)\n",
+ " .size([width, height])\n",
+ " .on(\"tick\", $.proxy(this.tick, this));\n",
+ " this.force = force;\n",
+ " \n",
+ " var svg = d3.select(\"#\" + this.guid).append(\"svg\")\n",
+ " .attr(\"width\", width)\n",
+ " .attr(\"height\", height);\n",
+ " this.svg = svg;\n",
+ " \n",
+ " var that = this;\n",
+ " setTimeout(function() {\n",
+ " that.start();\n",
+ " }, 0);\n",
+ " }\n",
+ " \n",
+ " return IPython.WidgetView.prototype.update.call(this);\n",
+ " },\n",
+ " \n",
+ " });\n",
+ " \n",
+ " // Register the D3ForceDirectedGraphView with the widget manager.\n",
+ " IPython.widget_manager.register_widget_view('D3ForceDirectedGraphView', D3ForceDirectedGraphView);\n",
+ "});"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "javascript": [
+ "\n",
+ "require([\"notebook/js/widget\"], function(){\n",
+ " \n",
+ " // Define the ForceDirectedGraphModel and register it with the widget manager.\n",
+ " var ForceDirectedGraphModel = IPython.WidgetModel.extend({});\n",
+ " IPython.widget_manager.register_widget_model('ForceDirectedGraphModel', ForceDirectedGraphModel);\n",
+ " \n",
+ " // Define the D3ForceDirectedGraphView\n",
+ " var D3ForceDirectedGraphView = IPython.WidgetView.extend({\n",
+ " \n",
+ " render: function(){\n",
+ " this.guid = 'd3force' + IPython.utils.uuid();\n",
+ " this.setElement($('', {id: this.guid}));\n",
+ " this.model.on_msg($.proxy(this.handle_msg, this));\n",
+ " },\n",
+ " \n",
+ " add_node: function(id){\n",
+ " var index = this.find_node(id);\n",
+ " if (index == -1) {\n",
+ " var node = {id: id};\n",
+ " this.nodes.push(node);\n",
+ " return node;\n",
+ " } else {\n",
+ " return this.nodes[index];\n",
+ " }\n",
+ " },\n",
+ " \n",
+ " remove_node: function(id){\n",
+ " var found_index = this.find_node(id);\n",
+ " if (found_index>=0) {\n",
+ " this.nodes.splice(found_index, 1);\n",
+ " }\n",
+ " \n",
+ " clear_links(id);\n",
+ " },\n",
+ " \n",
+ " find_node: function(id){\n",
+ " var found_index = -1;\n",
+ " for (var index in this.nodes) {\n",
+ " if (this.nodes[index].id == id) {\n",
+ " found_index = index;\n",
+ " break;\n",
+ " }\n",
+ " }\n",
+ " return found_index;\n",
+ " },\n",
+ " \n",
+ " clear_links: function(id){\n",
+ " \n",
+ " // Remove existing links\n",
+ " var found_indexs = [];\n",
+ " for (var index in this.links) {\n",
+ " if (this.links[index].source.id == id) {\n",
+ " found_indexs.push(index);\n",
+ " }\n",
+ " }\n",
+ " \n",
+ " for (var index in found_indexs) {\n",
+ " this.links.splice(found_indexs[index], 1);\n",
+ " }\n",
+ " },\n",
+ " \n",
+ " handle_msg: function(content){\n",
+ " var dict = content.dict;\n",
+ " var action = content.action;\n",
+ " var key = content.key;\n",
+ " console.log(dict, action, key);\n",
+ " \n",
+ " if (dict=='node') {\n",
+ " \n",
+ " // Only support node ADD and DEL actions for now...\n",
+ " if (action=='add') {\n",
+ " this.add_node(key)\n",
+ " } else if (action=='del') {\n",
+ " this.remove_node(key);\n",
+ " }\n",
+ " \n",
+ " } else if (dict=='adj') {\n",
+ " this.clear_links(key);\n",
+ " \n",
+ " // Add all links\n",
+ " if (action != 'del') {\n",
+ " var value = content.value;\n",
+ " for (var link_to in value) {\n",
+ " var source_node = this.add_node(key);\n",
+ " var target_node = this.add_node(link_to);\n",
+ " this.links.push({source: source_node, target: target_node});\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " this.start();\n",
+ " },\n",
+ " \n",
+ " start: function() {\n",
+ " var node = this.svg.selectAll(\".node\"),\n",
+ " link = this.svg.selectAll(\".link\");\n",
+ " \n",
+ " var link = link.data(this.force.links(), function(d) { return d.source.id + \"-\" + d.target.id; });\n",
+ " link.enter()\n",
+ " .insert(\"line\", \".node\")\n",
+ " .attr(\"class\", \"link\")\n",
+ " .style(\"stroke-width\", '1.5px')\n",
+ " .style('stroke', '#999');\n",
+ " link.exit().remove();\n",
+ " \n",
+ " var node = node.data(this.force.nodes(), function(d) { return d.id;});\n",
+ " var that = this;\n",
+ " node.enter()\n",
+ " .append(\"circle\")\n",
+ " .attr(\"class\", function(d) { return \"node \" + d.id; })\n",
+ " .attr(\"r\", 8)\n",
+ " .style(\"fill\", function(d) { return that.color(d.group); })\n",
+ " .style(\"stroke\", \"#fff\")\n",
+ " .style(\"stroke-width\", \"1.5px\")\n",
+ " .call(this.force.drag);\n",
+ " node.exit().remove();\n",
+ " \n",
+ " this.force.start();\n",
+ " },\n",
+ " \n",
+ " tick: function() {\n",
+ " var node = this.svg.selectAll(\".node\"),\n",
+ " link = this.svg.selectAll(\".link\");\n",
+ " \n",
+ " link.attr(\"x1\", function(d) { return d.source.x; })\n",
+ " .attr(\"y1\", function(d) { return d.source.y; })\n",
+ " .attr(\"x2\", function(d) { return d.target.x; })\n",
+ " .attr(\"y2\", function(d) { return d.target.y; });\n",
+ " \n",
+ " node.attr(\"cx\", function(d) { return d.x; })\n",
+ " .attr(\"cy\", function(d) { return d.y; });\n",
+ " },\n",
+ " \n",
+ " update: function(){\n",
+ " var initial_json = this.model.get('initial_json');\n",
+ " if (this.initial_json != initial_json) {\n",
+ " this.initial_json = initial_json;\n",
+ " \n",
+ " var width = 860,\n",
+ " height = 400;\n",
+ " \n",
+ " this.color = d3.scale.category20();\n",
+ " \n",
+ " var graph = JSON.parse(initial_json);\n",
+ " this.nodes = [];\n",
+ " $.extend(this.nodes, graph.nodes);\n",
+ " this.links = [];\n",
+ " $.extend(this.links, graph.links);\n",
+ " \n",
+ " var force = d3.layout.force()\n",
+ " .nodes(this.nodes)\n",
+ " .links(this.links)\n",
+ " .charge(-120)\n",
+ " .linkDistance(30)\n",
+ " .size([width, height])\n",
+ " .on(\"tick\", $.proxy(this.tick, this));\n",
+ " this.force = force;\n",
+ " \n",
+ " var svg = d3.select(\"#\" + this.guid).append(\"svg\")\n",
+ " .attr(\"width\", width)\n",
+ " .attr(\"height\", height);\n",
+ " this.svg = svg;\n",
+ " \n",
+ " var that = this;\n",
+ " setTimeout(function() {\n",
+ " that.start();\n",
+ " }, 0);\n",
+ " }\n",
+ " \n",
+ " return IPython.WidgetView.prototype.update.call(this);\n",
+ " },\n",
+ " \n",
+ " });\n",
+ " \n",
+ " // Register the D3ForceDirectedGraphView with the widget manager.\n",
+ " IPython.widget_manager.register_widget_view('D3ForceDirectedGraphView', D3ForceDirectedGraphView);\n",
+ "});"
+ ],
+ "metadata": {},
+ "output_type": "display_data",
+ "text": [
+ ""
+ ]
+ }
+ ],
+ "prompt_number": 28
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "G = EventfulGraph()\n",
+ "G.add_node('hello')\n",
+ "G.add_node('goodbye')\n",
+ "G.add_edges_from([(1,2),(1,3)])\n",
+ "to_d3_json(G)"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "metadata": {},
+ "output_type": "pyout",
+ "prompt_number": 30,
+ "text": [
+ "'{\"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}'"
+ ]
+ }
+ ],
+ "prompt_number": 30
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "floating_container = widgets.ContainerWidget()\n",
+ "floating_container.set_css({\n",
+ " 'position': 'relative',\n",
+ " 'left': '0px',\n",
+ " 'top': '0px',\n",
+ " 'z-index': '999',\n",
+ " 'background': '#FFF',\n",
+ " 'opacity': '0.8'\n",
+ "})\n",
+ "\n",
+ "d3 = ForceDirectedGraphWidget(G, parent=floating_container)\n",
+ "display(floating_container)\n",
+ "\n",
+ "detach_button = widgets.ButtonWidget(description=\"Detach\")\n",
+ "def handle_detach(sender):\n",
+ " if sender.description == \"Detach\":\n",
+ " sender.description = \"Attach\"\n",
+ " floating_container.set_css('position', 'absolute')\n",
+ " else:\n",
+ " sender.description = \"Detach\"\n",
+ " floating_container.set_css('position', 'relative')\n",
+ "detach_button.on_click(handle_detach)\n",
+ "display(detach_button)"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 31
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "G.add_node('beep')\n",
+ "G.add_node('beep2')"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 32
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "G.remove_node('beep')"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 33
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "\n",
+ "G.add_edges_from([(4,5),(4,6)])"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 34
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "import time\n",
+ "for i in range(30):\n",
+ " G.add_edges_from([(7+i, 8+i), (7+i, 9+i)])\n",
+ " time.sleep(0.1)"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 35
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "\n",
+ "for i in range(30):\n",
+ " G.add_edge(7, i+8)\n",
+ " time.sleep(0.25)"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 36
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "G.clear()"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": []
+ }
+ ],
+ "metadata": {}
+ }
+ ]
+}
\ No newline at end of file