{ "metadata": { "name": "", "signature": "sha256:ede1914c829ea58d870d034aff22e287103c7cc4c3088691da42cd4a5274f7c2" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Dynamic visualization of graphs using d3.js" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from IPython.html import widgets\n", "from IPython.display import display\n", "from eventful_graph import EventfulGraph\n", "from widget_forcedirectedgraph import ForceDirectedGraphWidget, publish_js\n", "publish_js()" ], "language": "python", "metadata": {}, "outputs": [ { "javascript": [ "require([\"//cdnjs.cloudflare.com/ajax/libs/d3/3.4.1/d3.min.js\", \"notebook/js/widgetmanager\"], function(d3, WidgetManager){\n", "\n", " // Define the D3ForceDirectedGraphView\n", " var D3ForceDirectedGraphView = IPython.DOMWidgetView.extend({\n", " \n", " render: function(){\n", " this.guid = 'd3force' + IPython.utils.uuid();\n", " this.setElement($('
', {id: this.guid}));\n", " \n", " this.model.on('msg:custom', this.on_msg, this);\n", " this.has_drawn = false;\n", " \n", " // Wait for element to be added to the DOM\n", " var that = this;\n", " setTimeout(function() {\n", " that.update();\n", " }, 0);\n", " },\n", " \n", " try_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", " update_node: function(node, attributes) {\n", " if (node !== null) {\n", " for (var key in attributes) {\n", " node[key] = attributes[key];\n", " }\n", " this._update_circle(d3.select('#' + this.guid + node.id));\n", " this._update_text(d3.select('#' + this.guid + node.id + '-text'));\n", " }\n", " },\n", " \n", " remove_node: function(id){\n", " this.remove_links_to(id);\n", " \n", " var found_index = this.find_node(id);\n", " if (found_index>=0) {\n", " this.nodes.splice(found_index, 1);\n", " }\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", " find_link: function(source_id, target_id){\n", " for (var index in this.links) {\n", " if (this.links[index].source.id == source_id && this.links[index].target.id == target_id) {\n", " return index;\n", " }\n", " }\n", " return -1;\n", " },\n", " \n", " try_add_link: function(source_id, target_id){\n", " var index = this.find_link(source_id, target_id);\n", " if (index == -1) {\n", " var source_node = this.try_add_node(source_id);\n", " var target_node = this.try_add_node(target_id);\n", " var new_link = {source: source_node, target: target_node};\n", " this.links.push(new_link);\n", " return new_link;\n", " } else {\n", " return this.links[index]\n", " }\n", " },\n", " \n", " update_link: function(link, attributes){\n", " if (link != null) {\n", " for (var key in attributes) {\n", " link[key] = attributes[key];\n", " }\n", " this._update_edge(d3.select('#' + this.guid + link.source.id + \"-\" + link.target.id));\n", " }\n", " },\n", " \n", " remove_links: function(source_id){\n", " var found_indicies = [];\n", " for (var index in this.links) {\n", " if (this.links[index].source.id == source_id) {\n", " found_indicies.push(index);\n", " }\n", " }\n", " found_indicies.reverse();\n", " \n", " for (var index in found_indicies) {\n", " this.links.splice(index, 1);\n", " };\n", " },\n", " \n", " remove_links_to: function(id){\n", " var found_indicies = [];\n", " for (var index in this.links) {\n", " if (this.links[index].source.id == id || this.links[index].target.id == id) {\n", " found_indicies.push(index);\n", " }\n", " }\n", " found_indicies.reverse();\n", " \n", " for (var index in found_indicies) {\n", " this.links.splice(index, 1);\n", " };\n", " },\n", " \n", " on_msg: function(content){\n", " this.update();\n", " \n", " var dict = content.dict;\n", " var action = content.action;\n", " var key = content.key;\n", " \n", " if (dict=='node') {\n", " if (action=='add' || action=='set') {\n", " this.update_node(this.try_add_node(key), content.value)\n", " } else if (action=='del') {\n", " this.remove_node(key);\n", " }\n", " \n", " } else if (dict=='adj') {\n", " if (action=='add' || action=='set') {\n", " var links = content.value;\n", " for (var target_id in links) {\n", " this.update_link(this.try_add_link(key, target_id), links[target_id]);\n", " }\n", " } else if (action=='del') {\n", " this.remove_links(key);\n", " }\n", " }\n", " this.start();\n", " },\n", " \n", " start: function() {\n", " var node = this.svg.selectAll(\".gnode\"),\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", " this._update_edge(link.enter().insert(\"line\", \".gnode\"))\n", " link.exit().remove();\n", " \n", " var node = node.data(this.force.nodes(), function(d) { return d.id;});\n", " var that = this;\n", "\n", " var gnode = node.enter()\n", " .append(\"g\")\n", " .attr('class', 'gnode')\n", " .call(this.force.drag);\n", " this._update_circle(gnode.append(\"circle\"));\n", " this._update_text(gnode.append(\"text\"));\n", " node.exit().remove();\n", " \n", " this.force.start();\n", " },\n", "\n", " _update_circle: function(circle) {\n", " var that = this;\n", "\n", " circle\n", " .attr(\"id\", function(d) { return that.guid + d.id; })\n", " .attr(\"class\", function(d) { return \"node \" + d.id; })\n", " .attr(\"r\", function(d) {\n", " if (d.r == undefined) {\n", " return 8; \n", " } else {\n", " return d.r;\n", " }\n", " \n", " })\n", " .style(\"fill\", function(d) {\n", " if (d.fill == undefined) {\n", " return that.color(d.group); \n", " } else {\n", " return d.fill;\n", " }\n", " \n", " })\n", " .style(\"stroke\", function(d) {\n", " if (d.stroke == undefined) {\n", " return \"#FFF\"; \n", " } else {\n", " return d.stroke;\n", " }\n", " \n", " })\n", " .style(\"stroke-width\", function(d) {\n", " if (d.strokewidth == undefined) {\n", " return \"#FFF\"; \n", " } else {\n", " return d.strokewidth;\n", " }\n", " \n", " })\n", " .attr('dx', 0)\n", " .attr('dy', 0);\n", " },\n", " \n", " _update_text: function(text) {\n", " var that = this;\n", "\n", " text\n", " .attr(\"id\", function(d) { return that.guid + d.id + '-text'; })\n", " .text(function(d) { \n", " if (d.label) {\n", " return d.label;\n", " } else {\n", " return '';\n", " }\n", " })\n", " .style(\"font-size\",function(d) { \n", " if (d.font_size) {\n", " return d.font_size;\n", " } else {\n", " return '11pt';\n", " }\n", " })\n", " .attr(\"text-anchor\", \"middle\")\n", " .style(\"fill\", function(d) { \n", " if (d.color) {\n", " return d.color;\n", " } else {\n", " return 'white';\n", " }\n", " })\n", " .attr('dx', function(d) { \n", " if (d.dx) {\n", " return d.dx;\n", " } else {\n", " return 0;\n", " }\n", " })\n", " .attr('dy', function(d) { \n", " if (d.dy) {\n", " return d.dy;\n", " } else {\n", " return 5;\n", " }\n", " })\n", " .style(\"pointer-events\", 'none');\n", " },\n", " \n", " _update_edge: function(edge) {\n", " var that = this;\n", " edge\n", " .attr(\"id\", function(d) { return that.guid + d.source.id + \"-\" + d.target.id; })\n", " .attr(\"class\", \"link\")\n", " .style(\"stroke-width\", function(d) {\n", " if (d.strokewidth == undefined) {\n", " return \"1.5px\"; \n", " } else {\n", " return d.strokewidth;\n", " }\n", " \n", " })\n", " .style('stroke', function(d) {\n", " if (d.stroke == undefined) {\n", " return \"#999\"; \n", " } else {\n", " return d.stroke;\n", " }\n", " \n", " });\n", " },\n", " \n", " tick: function() {\n", " var gnode = this.svg.selectAll(\".gnode\"),\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", " // Translate the groups\n", " gnode.attr(\"transform\", function(d) { return \"translate(\" + d.x + \",\" + d.y + \")\"; }); \n", " },\n", " \n", " update: function(){\n", " if (!this.has_drawn) {\n", " this.has_drawn = true;\n", " var width = this.model.get('width'),\n", " height = this.model.get('height');\n", " \n", " this.color = d3.scale.category20();\n", " \n", " this.nodes = [];\n", " this.links = [];\n", " \n", " var that = this;\n", " this.force = d3.layout.force()\n", " .nodes(this.nodes)\n", " .links(this.links)\n", " .charge(function (d) {\n", " if (d.charge === undefined) {\n", " return -280;\n", " } else {\n", " return d.charge;\n", " }\n", " })\n", " .linkDistance(function (d) {\n", " if (d.distance === undefined) {\n", " return 30;\n", " } else {\n", " return d.distance;\n", " }\n", " })\n", " .linkStrength(function (d) {\n", " if (d.strength === undefined) {\n", " return 0.3;\n", " } else {\n", " return d.strength;\n", " }\n", " })\n", " .size([width, height])\n", " .on(\"tick\", $.proxy(this.tick, this));\n", " \n", " this.svg = d3.select(\"#\" + this.guid).append(\"svg\")\n", " .attr(\"width\", width)\n", " .attr(\"height\", height);\n", " }\n", "\n", " var that = this;\n", " setTimeout(function() {\n", " that.start();\n", " }, 0);\n", " return D3ForceDirectedGraphView.__super__.update.apply(this);\n", " },\n", " \n", " });\n", " \n", " // Register the D3ForceDirectedGraphView with the widget manager.\n", " WidgetManager.register_widget_view('D3ForceDirectedGraphView', D3ForceDirectedGraphView);\n", "});" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 1 }, { "cell_type": "markdown", "metadata": {}, "source": [ "[d3.js](http://d3js.org) has excellent force directed graph layout algorithms. But most network/graph data is not in the browser.\n", "\n", "[NetworkX](http://networkx.github.io/) is a Python library that provides graph data structures, algorithms and visualization.\n", "\n", "In this example, we demonstrate how it is possible to hook up NetworkX graph data strutures in Python to d3 based visualizations in a way that allows the **dynamic** visualization of graphs.\n", "\n", "**The goal of this demo is to show how Widgets allow state to be sent to the browser in real time.**\n", "\n", "Here is an outline of the moving parts:\n", "\n", "* `EventfulGraph` is a subclass of `networkx.Graph` that can trigger events when the graph data changes\n", " - Add and remove node and edges\n", " - Metadata/property/attributes changes (color, radius, label, etc.)\n", "* `ForceDirectedGraphWidget` is an IPython widget that watches an `EventfulGraph` for events and synchronizes the graph data to a JavaScript model in the browser\n", "* The JavaScript view for the `ForceDirectedGraphWidget` uses d3's force directed layout algorithms.\n", "* Python code can modify the graph object and the view will update in real time." ] }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "A simple example" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here we setup the Widgets and create an `EventfulGraph`:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "floating_container = widgets.PopupWidget(default_view_name='ModalView')\n", "floating_container.description = \"Dynamic D3 rendering of a NetworkX graph\"\n", "floating_container.button_text = \"Render Window\"\n", "floating_container.set_css({\n", " 'width': '420px',\n", " 'height': '350px'}, selector='modal')\n", "\n", "G = EventfulGraph()\n", "d3 = ForceDirectedGraphWidget(G)\n", "\n", "floating_container.children = [d3]\n", "display(floating_container)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 2 }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following code animates some of the graphs's attributes:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import time\n", "\n", "G.add_node(1, fill=\"red\", stroke=\"black\", color='black', label='A')\n", "time.sleep(1.0)\n", "\n", "G.add_node(2, fill=\"gold\", stroke=\"black\", color='black', r=20, font_size='24pt', label='B')\n", "time.sleep(1.0)\n", "\n", "G.add_node(3, fill=\"green\", stroke=\"black\", color='white', label='C')\n", "time.sleep(1.0)\n", "\n", "G.add_edges_from([(1,2),(1,3), (2,3)], stroke=\"#aaa\", strokewidth=\"1px\", distance=200, strength=0.5)\n", "time.sleep(1.0)\n", "\n", "G.adj[1][2]['distance'] = 20\n", "time.sleep(1.0)\n", "\n", "G.adj[1][3]['distance'] = 20\n", "time.sleep(1.0)\n", "\n", "G.adj[2][3]['distance'] = 20\n", "time.sleep(1.0)\n", "\n", "G.node[1]['r'] = 16\n", "time.sleep(0.3)\n", "G.node[1]['r'] = 8\n", "G.node[2]['r'] = 16\n", "time.sleep(0.3)\n", "G.node[2]['r'] = 20\n", "G.node[3]['r'] = 16\n", "time.sleep(0.3)\n", "G.node[3]['r'] = 8\n", "\n", "G.node[1]['fill'] = 'purple'\n", "time.sleep(0.3)\n", "G.node[1]['fill'] = 'red'\n", "G.node[2]['fill'] = 'purple'\n", "time.sleep(0.3)\n", "G.node[2]['fill'] = 'gold'\n", "G.node[3]['fill'] = 'purple'\n", "time.sleep(0.3)\n", "G.node[3]['fill'] = 'green'\n", "time.sleep(1.0)\n", "\n", "G.node.clear()\n", "time.sleep(1.0)\n", "\n", "floating_container.close()\n" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 3 }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Visualizing integer factoring" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this example, we visualize the factoring of small integers. We use a simple algorithm:\n", "\n", "* For a given integer find all of its divisors in increasing order: $10 \\rightarrow 2, 5$\n", "* For all of those divisors recursively find *their* divisors\n", "* Continue until all of the divisors are prime\n", "* Nodes in the graph are the divisors\n", "* Edges are added between an integer and each of its divisors" ] }, { "cell_type": "code", "collapsed": false, "input": [ "BACKGROUND = '#FFFFFF'\n", "PARENT_COLOR = '#3E5970'\n", "FACTOR_COLOR = '#424357'\n", "EDGE_COLOR = '#000000'\n", "PRIME_COLOR = '#FF5555'\n", "\n", "existing_graphs = []\n", "\n", "def handle_graph(graph):\n", " if len(existing_graphs) > 0:\n", " for graph_popup in existing_graphs:\n", " graph_popup.close()\n", " del existing_graphs[:]\n", " \n", " floating_container = widgets.ContainerWidget()\n", " floating_container.description = \"Factors\"\n", " floating_container.button_text = \"Factors\"\n", " floating_container.set_css({\n", " 'width': '620px',\n", " 'height': '450px'}, selector='modal')\n", " \n", " d3 = ForceDirectedGraphWidget(graph)\n", " d3.charge = -400.\n", " floating_container.children = [d3]\n", " floating_container.set_css('background', BACKGROUND)\n", " d3.width = 600\n", " d3.height = 400\n", " display(floating_container)\n", " existing_graphs.append(floating_container)\n", "EventfulGraph.on_constructed(handle_graph)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 4 }, { "cell_type": "code", "collapsed": false, "input": [ "CHARGE = -200\n", "MIN_NODE_RADIUS = 15.0\n", "START_NODE_RADIUS = 65.0\n", "is_int = lambda x: int(x) == x\n", "factor = lambda x: [i + 1 for i in range(x-1) if i != 0 and is_int(x / (float(i) + 1.0))]\n", "calc_node_size = lambda x, start_x: max(float(x)/start_x * START_NODE_RADIUS, MIN_NODE_RADIUS)\n", "calc_edge_length = lambda x, parent_x, start_x: calc_node_size(x, start_x) + calc_node_size(parent_x, start_x)\n", " \n", "def add_node(graph, value, **kwargs):\n", " graph.add_node(len(graph.node), charge=CHARGE, strokewidth=0, value=value, label=value, font_size='18pt', dy='8', **kwargs)\n", " return len(graph.node) - 1\n", " \n", "def add_child_node(graph, x, number, start_number, parent):\n", " index = add_node(graph, x, fill=FACTOR_COLOR, r='%.2fpx' % calc_node_size(x, start_number))\n", " graph.add_edge(index, parent, distance=calc_edge_length(x, number, start_number), stroke=EDGE_COLOR, strokewidth='3px')\n", " return index\n", "\n", "def plot_primes(number, start_number=None, parent=None, graph=None, delay=0.0):\n", " start_number = start_number or number\n", " graph = graph or EventfulGraph(sleep=delay)\n", " parent = parent or add_node(graph, number, fill=PARENT_COLOR, r='%.2fpx' % START_NODE_RADIUS)\n", " \n", " factors = factor(number)\n", " if len(factors) == 0:\n", " graph.node[parent]['fill'] = PRIME_COLOR\n", " for x in factors:\n", " index = add_child_node(graph, x, number, start_number, parent)\n", " plot_primes(x, start_number, parent=index, graph=graph)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 5 }, { "cell_type": "code", "collapsed": false, "input": [ "box = widgets.ContainerWidget()\n", "header = widgets.HTMLWidget(value=\"

Integer Factorizer


\")\n", "number = widgets.IntSliderWidget(description=\"Number:\", value=100)\n", "speed = widgets.FloatSliderWidget(description=\"Delay:\", min=0.0, max=0.2, value=0.1, step=0.01)\n", "\n", "subbox = widgets.ContainerWidget()\n", "button = widgets.ButtonWidget(description=\"Calculate\")\n", "subbox.children = [button]\n", "\n", "box.children = [header, number, speed, subbox]\n", "display(box)\n", "\n", "box.add_class('align-center')\n", "box.add_class('center')\n", "box.add_class('well well-small')\n", "box.set_css('width', 'auto')\n", "\n", "subbox.remove_class('vbox')\n", "subbox.add_class('hbox')\n", "# subbox.add_class('end')\n", "\n", "def handle_caclulate(sender):\n", " plot_primes(number.value, delay=speed.value)\n", "button.on_click(handle_caclulate)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 6 }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 6 }, { "cell_type": "code", "collapsed": false, "input": [], "language": "python", "metadata": {}, "outputs": [] } ], "metadata": {} } ] }