diff --git a/scipost/models.py b/scipost/models.py
index 622b92025e9342cfd9af739abce29d3c79983dbc..adc65c368af225223c848d45f4ad5dee9ecf43ea 100644
--- a/scipost/models.py
+++ b/scipost/models.py
@@ -342,15 +342,14 @@ class Node(models.Model):
 
     def header_as_p(self):
         context = Context({'graph_id': self.graph.id, 'id': self.id, 'name': self.name})
-        output = '<p><a href="{% url \'scipost:graph\' graph_id=graph_id node_id=id %}">{{ name }}</a></p>'
+        output = '<p class="node_p" id="node_id{{ id }}"><a href="{% url \'scipost:graph\' graph_id=graph_id %}">{{ name }}</a></p>'
         template = Template(output)
         return template.render(context)
 
     def contents(self):
         context = Context({'graph_id': self.graph.id, 'id': self.id, 'name': self.name, 
                            'description': self.description, 'annotation': self.annotation})
-        output = '''<h3><a href="{% url 'scipost:graph' graph_id=graph_id node_id=id %}">{{ name }}</a></h3>
-                 <p>{{ description }}</p><p>{{ annotation }}</p>'''
+        output = '<div class="node_contents" id="node_id{{ id }}"><h3>{{ name }}</h3><p>{{ description }}</p><p>{{ annotation }}</p></div>'
         template = Template(output)
         return template.render(context)
 
diff --git a/scipost/templates/scipost/add_graph_node.html b/scipost/templates/scipost/add_graph_node.html
deleted file mode 100644
index 785f9ab509a81d2c2f254d611a4409db1a6fbf37..0000000000000000000000000000000000000000
--- a/scipost/templates/scipost/add_graph_node.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends 'scipost/base.html' %}
-
-{% block pagetitle %}: add graph node{% endblock pagetitle %}
-
-{% block bodysup %}
-
-<section>
-  <h1>Add Node to Graph</h1>
-
-  <form action="{% url 'scipost:add_graph_node' graph_id=graph.id %}" method="post">
-    {% csrf_token %}
-    <table>
-      {{ create_node_form.as_table }}
-    </table>
-    <input type="submit" value="Create Node" />
-  </form>
-
-</section>
-
-{% endblock bodysup %}
diff --git a/scipost/templates/scipost/graph.html b/scipost/templates/scipost/graph.html
index f4c4b1517b59b5866e438617505192fb01bcbc90..cefe7b0373b2f752bfc98f97eb33860087967b48 100644
--- a/scipost/templates/scipost/graph.html
+++ b/scipost/templates/scipost/graph.html
@@ -2,36 +2,160 @@
 
 {% block pagetitle %}: graph{% endblock pagetitle %}
 
+{% block headsup %}
+
+<style>
+.link {
+  fill: none;
+  stroke: #666;
+  stroke-width: 1.5px;
+}
+
+circle {
+  fill: #ccc;
+  stroke: #333;
+  stroke-width: 1.5px;
+}
+
+text {
+  font: 12px sans-serif;
+  pointer-events: none;
+  text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
+}
+</style>
+
+{% endblock headsup %}
+
 {% block bodysup %}
 
+
+<script src="//d3js.org/d3.v3.min.js"></script>
+<script>
+
+$(document).ready(function(){
+$(".node_contents").hide();
+$("#NodeForm").hide();
+$("#NodeFormButton").click(function(){
+$("#NodeForm").toggle();
+});
+});
+
+var links;
+d3.json("{% url 'scipost:api_graph' graph_id=graph.id %}", function(error, json) {
+  if (error) return console.warn(error);
+  links = json;
+
+var nodes = {};
+
+// Compute the distinct nodes from the links.
+links.forEach(function(link) {
+  link.source = nodes[link.from] || (nodes[link.from] = {name: link.from, id: link.from_id});
+  link.target = nodes[link.to] || (nodes[link.to] = {name: link.to, id: link.to_id});
+});
+
+var width = 700,
+    height = 300;
+
+var force = d3.layout.force()
+    .nodes(d3.values(nodes))
+    .links(links)
+    .size([width, height])
+    .linkDistance(100)
+    .charge(-300)
+    .on("tick", tick)
+    .start();
+
+var svg = d3.select("#graphic").append("svg")
+    .attr("width", width)
+    .attr("height", height);
+
+svg.append("defs").append("marker")
+    .attr("id", "arrowhead")
+    .attr("viewBox", "0 -5 10 10")
+    .attr("refX", 15)
+    .attr("refY", -1.5)
+    .attr("markerWidth", 6)
+    .attr("markerHeight", 6)
+    .attr("orient", "auto")
+    .append("path")
+     .attr("d", "M0,-5L10,0L0,5");
+
+var path = svg.append("g").selectAll("path")
+    .data(force.links())
+  .enter().append("path")
+    .attr("class", "link")
+    .attr("marker-end", "url(#arrowhead)");
+
+var circle = svg.append("g").selectAll("circle")
+    .data(force.nodes())
+  .enter().append("circle")
+    .attr("r", 6)
+    .attr("id", function(d) { return d.id; })
+    .call(force.drag);
+
+circle.on("click", function(){
+  d3.selectAll("circle").style("fill", "#ccc");
+  d3.select(this).style("fill", "blue");
+  $(".node_contents").hide();
+  $("#node_id" + $(this).attr("id")).show();
+});
+
+circle.on("dblclick", function()
+  {d3.select(this).style("fill", "red");});
+
+var text = svg.append("g").selectAll("text")
+    .data(force.nodes())
+  .enter().append("text")
+    .attr("x", 8)
+    .attr("y", ".31em")
+    .text(function(d) { return d.name; });
+
+// Use elliptical arc path segments to doubly-encode directionality.
+function tick() {
+  path.attr("d", linkArc);
+  circle.attr("transform", transform);
+  text.attr("transform", transform);
+}
+
+function linkArc(d) {
+  var dx = d.target.x - d.source.x,
+      dy = d.target.y - d.source.y,
+      dr = Math.sqrt(dx * dx + dy * dy);
+  return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
+}
+
+function transform(d) {
+  return "translate(" + d.x + "," + d.y + ")";
+}
+
+
+});
+
+
+</script>
+
+
+
 <section>
   <h1>Graph</h1>
   {{ graph.contents }}
 
-  <hr class="hr6"/>
-
-  {% if node.arcs_in.all %}
-  <h3>Pointed to from:</h3>
-  <ul>
-    {% for node_in in node.arcs_in.all %}
-    {{ node_in.header_as_p }}
-    {% endfor %}
-  </ul>
-  {% endif %}
+  <div id="graphic"></div>
 
-  {{ node.contents }}
-
-  {% if nodes_out %}
-  <h3>Pointing to:</h3>
-  <ul>
-    {% for node_out in nodes_out.all %}
-    {{ node_out.header_as_p }}
-    {% endfor %}
-  </ul>
-  {% endif %}
+  <button id="NodeFormButton"><h1>Add Node to Graph</h1> (show/hide form)</button>
+  <form action="{% url 'scipost:graph' graph_id=graph.id %}" method="post" id="NodeForm">
+    {% csrf_token %}
+    <table>
+      {{ create_node_form.as_table }}
+    </table>
+    <input type="submit" value="Create Node" />
+  </form>
 
   <hr class="hr6"/>
-  <h3><a href="{% url 'scipost:add_graph_node' graph_id=node.graph.id %}">Add a Node</a></h3>
+
+  {% for node in nodes %}
+  {{ node.contents }}
+  {% endfor %}
 
 </section>
 
diff --git a/scipost/urls.py b/scipost/urls.py
index 0868067d6a9e6a5923e2a855095ab6e7806c6205..9f8571f352205e93a0c94da7430e7c6d4e4af7f3 100644
--- a/scipost/urls.py
+++ b/scipost/urls.py
@@ -68,7 +68,6 @@ urlpatterns = [
 
     # Graphs
     url(r'^create_graph$', views.create_graph, name='create_graph'),
-    url(r'^add_graph_node/(?P<graph_id>[0-9]+)$', views.add_graph_node, name='add_graph_node'),
     url(r'^graph/(?P<graph_id>[0-9]+)$', views.graph, name='graph'),
-    url(r'^graph/(?P<graph_id>[0-9]+)/(?P<node_id>[0-9]+)$', views.graph, name='graph'),
+    url(r'^api/graph/(?P<graph_id>[0-9]+)$', views.api_graph, name='api_graph'),
 ]
diff --git a/scipost/views.py b/scipost/views.py
index 56ee93eb2c3a20492b2db9c3b1530f4bf946c773..0b18758954fcee66d11e78ef2752df79109752db 100644
--- a/scipost/views.py
+++ b/scipost/views.py
@@ -11,7 +11,7 @@ from django.contrib.auth.models import User, Group, Permission
 from django.contrib.auth.views import password_reset, password_reset_confirm
 from django.core.mail import EmailMessage
 from django.core.urlresolvers import reverse
-from django.http import HttpResponse, HttpResponseRedirect
+from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.views.decorators.csrf import csrf_protect
@@ -651,24 +651,9 @@ def create_graph(request):
 
 
 @permission_required('scipost.can_create_graph', raise_exception=True)
-def graph(request, graph_id, node_id=None):
-    graph = get_object_or_404(Graph, pk=graph_id)
-    if node_id is not None:
-        node = get_object_or_404(Node, pk=node_id)
-        nodes_out = Node.objects.filter(graph=graph, arcs_in__in=[node])
-        context = {'graph': graph, 'node': node, 'nodes_out': nodes_out}
-        return render(request, 'scipost/graph.html', context)
-    elif Node.objects.filter(graph=graph).exists():
-        node = Node.objects.filter(graph=graph).first()
-        return redirect(reverse('scipost:graph', kwargs={'graph_id': graph.id, 'node_id': node.id}))
-    else:
-        return redirect(reverse('scipost:add_graph_node', kwargs={'graph_id': graph.id}))
-
-
-@permission_required('scipost.can_create_graph', raise_exception=True)
-def add_graph_node(request, graph_id):
-    """ Adds a node """
+def graph(request, graph_id):
     graph = get_object_or_404(Graph, pk=graph_id)
+    nodes = Node.objects.filter(graph=graph)
     if request.method == "POST":
         create_node_form = CreateNodeForm(request.POST, graph=graph)
         if create_node_form.is_valid():
@@ -684,8 +669,20 @@ def add_graph_node(request, graph_id):
             for outnode in create_node_form.cleaned_data['arcs_out']:
                 outnode.arcs_in.add(newnode)
                 outnode.save()
-            return redirect(reverse('scipost:graph', kwargs={'graph_id': graph.id, 'node_id': newnode.id}))
+            return redirect(reverse('scipost:graph', kwargs={'graph_id': graph.id}))
     else:
         create_node_form = CreateNodeForm(graph=graph)
-    context = {'graph': graph, 'create_node_form': create_node_form}
-    return render(request, 'scipost/add_graph_node.html', context)
+
+    context = {'graph': graph, 'nodes': nodes, 'create_node_form': create_node_form}
+    return render(request, 'scipost/graph.html', context)
+
+
+def api_graph(request, graph_id):
+    """ Produce JSON data to plot graph """
+    graph = get_object_or_404(Graph, pk=graph_id)
+    nodes = Node.objects.filter(graph=graph)
+    links = []
+    for node in nodes:
+        for origin in node.arcs_in.all():
+            links.append({'from': origin.name, 'from_id': origin.id, 'to': node.name, 'to_id': node.id})
+    return JsonResponse(links, safe=False)