├── d3networkx
├── __init__.py
├── widget.py
├── eventful_graph.py
├── eventful_dict.py
└── widget.js
├── .gitignore
├── README.md
├── setup.py
├── LICENSE
└── examples
├── demo simple.ipynb
├── demo factor.ipynb
├── demo generators.ipynb
└── demo twitter.ipynb
/d3networkx/__init__.py:
--------------------------------------------------------------------------------
1 | from .widget import ForceDirectedGraph
2 | from .eventful_graph import EventfulGraph, empty_eventfulgraph_hook
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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]
--------------------------------------------------------------------------------
/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 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 |
--------------------------------------------------------------------------------
/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 | });
--------------------------------------------------------------------------------