From ab6111db8cb6ae9078399378502c1c9fa5dc78a3 Mon Sep 17 00:00:00 2001
From: "J.-S. Caux" <J.S.Caux@uva.nl>
Date: Sat, 14 May 2016 17:05:02 +0200
Subject: [PATCH] Improve graphs: zooming and panning

---
 scipost/admin.py                     | 21 ++++++-
 scipost/forms.py                     |  5 +-
 scipost/models.py                    | 37 ++++++------
 scipost/templates/scipost/graph.html | 86 +++++++++++++++++-----------
 scipost/views.py                     | 50 ++++++++++------
 5 files changed, 124 insertions(+), 75 deletions(-)

diff --git a/scipost/admin.py b/scipost/admin.py
index d0ef3f8ae..4c855d93e 100644
--- a/scipost/admin.py
+++ b/scipost/admin.py
@@ -36,6 +36,23 @@ admin.site.register(List, ListAdmin)
 
 admin.site.register(Team)
 
-admin.site.register(Graph)
+#admin.site.register(Graph)
 
-admin.site.register(Node)
+#admin.site.register(Node)
+
+#admin.site.register(Arc)
+
+class NodeInline(admin.StackedInline):
+    model = Node
+
+class ArcInline(admin.StackedInline):
+    model = Arc
+    
+class GraphAdmin(GuardedModelAdmin):
+    inlines = [
+        NodeInline,
+        ArcInline,
+        ]
+    search_fields = ['last_name', 'email']
+
+admin.site.register(Graph, GraphAdmin)
diff --git a/scipost/forms.py b/scipost/forms.py
index 8075d64ea..5ac2b48e0 100644
--- a/scipost/forms.py
+++ b/scipost/forms.py
@@ -166,13 +166,14 @@ class CreateNodeForm(forms.ModelForm):
         fields = ['name', 'description']
 
 
-class CreateLinkForm(forms.Form):
+class CreateArcForm(forms.Form):
     source = forms.ModelChoiceField(queryset=None)
     target = forms.ModelChoiceField(queryset=None)
+    length = forms.ChoiceField(choices=ARC_LENGTHS)
 
     def __init__(self, *args, **kwargs):
         graph = kwargs.pop('graph')
-        super(CreateLinkForm, self).__init__(*args, **kwargs)
+        super(CreateArcForm, self).__init__(*args, **kwargs)
         self.fields['source'].queryset = Node.objects.filter(graph=graph)
         self.fields['target'].queryset = Node.objects.filter(graph=graph)
     
diff --git a/scipost/models.py b/scipost/models.py
index 1cb8231b9..344870f0f 100644
--- a/scipost/models.py
+++ b/scipost/models.py
@@ -413,22 +413,6 @@ class Graph(models.Model):
         return template.render(context)
 
 
-# ARC_LENGTHS = [
-#     (4, '4'), (8, '8'), (16, '16'), (32, '32'), (64, '64'), (128, '128')
-#     ]
-
-# class Arc(models.Model):
-#     """
-#     Arc of a graph, linking two nodes.
-#     The length is user-adjustable.
-#     """
-#     graph = models.ForeignKey(Graph, default=None)
-#     added_by = models.ForeignKey(Contributor, default=None)
-#     created = models.DateTimeField(default=timezone.now)
-#     node_from = models.ForeignKey(Node)
-#     node_to = models.ForeignKey(Node)
-#     length = models.PositiveSmallIntegerField(choices=ARC_LENGTHS)
-
 class Node(models.Model):
     """
     Node of a graph (directed).
@@ -439,7 +423,7 @@ class Node(models.Model):
     added_by = models.ForeignKey(Contributor, default=None)
     created = models.DateTimeField(default=timezone.now)
     name = models.CharField(max_length=100)
-    arcs_in = models.ManyToManyField('self', blank=True, related_name='node_arcs_in', symmetrical=False) # arcs from another node pointing into this node
+# REPLACED BY CLASS Arc   arcs_in = models.ManyToManyField('self', blank=True, related_name='node_arcs_in', symmetrical=False) # arcs from another node pointing into this node
     description = models.TextField(blank=True, null=True)
     submissions = models.ManyToManyField('submissions.Submission', blank=True, related_name='node_submissions')
     commentaries = models.ManyToManyField('commentaries.Commentary', blank=True, related_name='node_commentaries')
@@ -469,3 +453,22 @@ class Node(models.Model):
         output = '<div style="font-size: 60%">' + self.contents + '</div>'
         template = Template(output)
         return template.render()
+
+
+ARC_LENGTHS = [
+#    (4, '4'), (8, '8'), (16, '16'), (32, '32'), (64, '64'), (128, '128')
+    (1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5'), (6, '6'), (7, '7'), (8, '8'),
+    ]
+
+class Arc(models.Model):
+    """
+    Arc of a graph, linking two nodes.
+    The length is user-adjustable.
+    """
+    graph = models.ForeignKey(Graph, default=None)
+    added_by = models.ForeignKey(Contributor, default=None)
+    created = models.DateTimeField(default=timezone.now)
+    source = models.ForeignKey(Node, related_name='source')
+    target = models.ForeignKey(Node, related_name='target')
+    length = models.PositiveSmallIntegerField(choices=ARC_LENGTHS, default=32)
+
diff --git a/scipost/templates/scipost/graph.html b/scipost/templates/scipost/graph.html
index ac3ec9121..19679c37d 100644
--- a/scipost/templates/scipost/graph.html
+++ b/scipost/templates/scipost/graph.html
@@ -5,7 +5,13 @@
 {% block headsup %}
 
 <style>
-.link {
+
+  .overlay {
+  fill: none;
+  pointer-events: all;
+  }
+
+.arc {
   fill: none;
   stroke: #666;
   stroke-width: 1.5px;
@@ -38,28 +44,29 @@ $(".node_contents").hide();
 $(".delete_node").hide();
 
 $("#NodeForm").hide();
-$("#LinkForm").hide();
+$("#ArcForm").hide();
 $("#ManageTeamsForm").hide();
 
 $("#ManageTeamsFormButton").click(function(){
 $("#ManageTeamsForm").toggle();
 $("#NodeForm").hide();
-$("#LinkForm").hide();
+$("#ArcForm").hide();
 });
 
 $("#NodeFormButton").click(function(){
 $("#ManageTeamsForm").hide();
 $("#NodeForm").toggle();
-$("#LinkForm").hide();
+$("#ArcForm").hide();
 });
 
-$("#LinkFormButton").click(function(){
+$("#ArcFormButton").click(function(){
 $("#ManageTeamsForm").hide();
 $("#NodeForm").hide();
-$("#LinkForm").toggle();
+$("#ArcForm").toggle();
 });
 });
 
+
 d3.json("{% url 'scipost:api_graph' graph_id=graph.id %}", function(error, json) {
   if (error) return console.warn(error);
 
@@ -71,23 +78,32 @@ nodesjson.forEach(function(node) {
   nodes[node.name].y = 150 + 100 * Math.random();
 });
 
-var links = json['links'];
-links.forEach(function(link) {
-  link.source = nodes[link.source];
-  link.target = nodes[link.target];
+var arcsjson = json['arcs'];
+var arcs = [];
+arcsjson.forEach(function(arc) {
+  arcs[arc.id] = arc;
+  arcs[arc.id].source = nodes[arc.source];
+  arcs[arc.id].target = nodes[arc.target];
+  arcs[arc.id].length = arc.length;
 });
 
+function arclength(arc) {
+  return arc.length * 50;
+}
+
 var width = 600;
 var height = 300;
 
 var force = d3.layout.force()
     .nodes(d3.values(nodes))
-    .links(links)
+//    .links(arcs)
+    .links(d3.values(arcs))
     .size([width, height])
     .friction(0.9)
     .linkStrength(1)
-    .linkDistance(80)
-    .charge(-400)
+//    .linkDistance(80)
+    .linkDistance(arclength)
+    .charge(-40)
     .gravity(0.1)
     .theta(0.8)
     .alpha(0.1)
@@ -95,20 +111,23 @@ var force = d3.layout.force()
     .start();
 
 
-//    .call(d3.behavior.zoom().on("zoom", rescale))
-function rescale() {
-  trans=d3.event.translate;
-  scale=d3.event.scale;
-
-  vis.attr("transform",
-      "translate(" + trans + ")"
-      + " scale(" + scale + ")");
-}
-
-
 var svg = d3.select("#graphic").append("svg")
     .attr("width", width)
-    .attr("height", height);
+.attr("height", height)
+.append("g")
+.call(d3.behavior.zoom().on("zoom", zoom))
+.on("dblclick.zoom", null)
+//.on("mousedown.zoom", null)
+.append("g");
+
+svg.append("rect")
+.attr("class", "overlay")
+.attr("width", width)
+.attr("height", height);
+
+function zoom() {
+svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
+}
 
 svg.append("defs").append("marker")
     .attr("id", "arrowhead")
@@ -124,7 +143,7 @@ svg.append("defs").append("marker")
 var path = svg.append("g").selectAll("path")
     .data(force.links())
   .enter().append("path")
-    .attr("class", "link")
+    .attr("class", "arc")
     .attr("marker-end", "url(#arrowhead)");
 
 var circle = svg.append("g").selectAll("circle")
@@ -132,7 +151,8 @@ var circle = svg.append("g").selectAll("circle")
   .enter().append("circle")
     .attr("r", 6)
     .attr("id", function(d) { return d.id; })
-    .call(force.drag);
+.call(force.drag);
+
 
 circle.on("click", function(){
   d3.selectAll("circle").style("fill", "#ccc");
@@ -142,10 +162,6 @@ circle.on("click", function(){
   $(".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")
@@ -214,13 +230,13 @@ function transform(d) {
 	</table>
 	<input type="submit" value="Create Node" />
       </form>
-      <button class="GraphButton" id="LinkFormButton"><h1>Add a Link</h1></button>
-      <form action="{% url 'scipost:graph' graph_id=graph.id %}" method="post" id="LinkForm">
+      <button class="GraphButton" id="ArcFormButton"><h1>Add an Arc</h1></button>
+      <form action="{% url 'scipost:graph' graph_id=graph.id %}" method="post" id="ArcForm">
 	{% csrf_token %}
 	<table>
-	  {{ create_link_form.as_table }}
+	  {{ create_arc_form.as_table }}
 	</table>
-	<input type="submit" value="Create Link" />
+	<input type="submit" value="Create Arc" />
       </form>
     </div>
     <div class="col-10">
diff --git a/scipost/views.py b/scipost/views.py
index a86d55780..e214c2a04 100644
--- a/scipost/views.py
+++ b/scipost/views.py
@@ -872,13 +872,14 @@ def create_graph(request):
 def graph(request, graph_id):
     graph = get_object_or_404(Graph, pk=graph_id)
     nodes = Node.objects.filter(graph=graph)
+    arcs = Arc.objects.filter(graph=graph)
     if request.method == "POST":
         attach_teams_form = ManageTeamsForm(request.POST, 
                                             contributor=request.user.contributor, 
                                             initial={'teams_with_access': graph.teams_with_access.all()}
                                             )
         create_node_form = CreateNodeForm(request.POST)
-        create_link_form = CreateLinkForm(request.POST, graph=graph)
+        create_arc_form = CreateArcForm(request.POST, graph=graph)
         if attach_teams_form.has_changed() and attach_teams_form.is_valid():
             graph.teams_with_access = attach_teams_form.cleaned_data['teams_with_access']
             graph.save()
@@ -889,22 +890,28 @@ def graph(request, graph_id):
                            name=create_node_form.cleaned_data['name'],
                            description=create_node_form.cleaned_data['description'])
             newnode.save()
-        elif create_link_form.has_changed() and create_link_form.is_valid():
-            sourcenode = create_link_form.cleaned_data['source']
-            targetnode = create_link_form.cleaned_data['target']
+        elif create_arc_form.has_changed() and create_arc_form.is_valid():
+            sourcenode = create_arc_form.cleaned_data['source']
+            targetnode = create_arc_form.cleaned_data['target']
             if sourcenode != targetnode:
-                targetnode.arcs_in.add(sourcenode)
-                targetnode.save()            
+                newarc = Arc(graph=graph,
+                             added_by=request.user.contributor,
+                             created=timezone.now(),
+                             source=sourcenode,
+                             target=targetnode,
+                             length=create_arc_form.cleaned_data['length']
+                             )
+                newarc.save()            
     else:
         attach_teams_form = ManageTeamsForm(contributor=request.user.contributor, 
                                             initial={'teams_with_access': graph.teams_with_access.all()}
                                             )
         create_node_form = CreateNodeForm()
-        create_link_form = CreateLinkForm(graph=graph)
+        create_arc_form = CreateArcForm(graph=graph)
     context = {'graph': graph, 'nodes': nodes, 
                'attach_teams_form': attach_teams_form,
                'create_node_form': create_node_form,
-               'create_link_form': create_link_form}
+               'create_arc_form': create_arc_form}
     return render(request, 'scipost/graph.html', context)
 
 
@@ -920,9 +927,9 @@ def edit_graph_node(request, node_id):
             node.description=edit_node_form.cleaned_data['description']
             node.save()
             create_node_form = CreateNodeForm()
-            create_link_form = CreateLinkForm(graph=node.graph)
+            create_arc_form = CreateArcForm(graph=node.graph)
             context =  {'create_node_form': create_node_form,
-                        'create_link_form': create_link_form}
+                        'create_arc_form': create_arc_form}
             return redirect(reverse('scipost:graph', kwargs={'graph_id': node.graph.id}), context)
     else:
         edit_node_form = CreateNodeForm(instance=node)
@@ -938,10 +945,9 @@ def delete_graph_node(request, node_id):
         raise PermissionDenied
     else:
         # Remove all the graph arcs 
-        nodes = Node.objects.filter(graph=node.graph)
-        for othernode in nodes:
-            othernode.arcs_in.remove(node)
-            othernode.save()
+        Arc.objects.filter(source=node).delete()
+        Arc.objects.filter(target=node).delete()
+        # Delete node itself
         node.delete()
     return redirect(reverse('scipost:graph', kwargs={'graph_id': node.graph.id}))
 
@@ -951,11 +957,17 @@ 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)
+    arcs = Arc.objects.filter(graph=graph)
     nodesjson = []
-    links = []
+    arcsjson = []
     for node in nodes:
         nodesjson.append({'name': node.name, 'id': node.id})
-        for origin in node.arcs_in.all():
-            links.append({'source': origin.name, 'source_id': origin.id, 
-                          'target': node.name, 'target_id': node.id})
-    return JsonResponse({'nodes': nodesjson, 'links': links}, safe=False)
+#        for origin in node.arcs_in.all():
+#            links.append({'source': origin.name, 'source_id': origin.id, 
+#                          'target': node.name, 'target_id': node.id})
+    for arc in arcs:
+        arcsjson.append({'id': arc.id,
+                         'source': arc.source.name, 'source_id': arc.source.id,
+                         'target': arc.target.name, 'target_id': arc.target.id,
+                         'length': arc.length})
+    return JsonResponse({'nodes': nodesjson, 'arcs': arcsjson}, safe=False)
-- 
GitLab