Merge branch 'master' of ssh://git.planet-lab.org/git/plstackapi into observer3.0
[plstackapi.git] / planetstack / templates / admin / dashboard / slice_interactions.html
1 <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
2 <style>
3 #slice_interaction_chart_placeholder {
4     text-align: center;
5     color:#fff;
6     position: relative;
7     height: 100%;
8     width: 100%;
9 }
10 .dependencyWheel {
11     font: 10px sans-serif;
12 }
13 form .btn-primary {
14     margin-top: 25px;
15 }
16 .labeltext {
17     color: #fff;
18 }
19 #circle circle {
20     fill: none;
21     pointer-events: all;
22 }
23 path.chord {
24     stroke: #000;
25     stroke-width: .10px;
26     transition: opacity 0.3s;
27 }
28 #circle:hover path.fade {
29     opacity: 0;
30 }
31 a {
32     text-decoration: none;
33     border-bottom: 1px dotted #666;
34     color: #999;
35 }
36 .more a {
37     color: #666;
38 }
39 .by a {
40     color: #fff;
41 }
42 a:hover {
43     color: #45b8e2;
44 }
45 a:not(:hover) {
46     text-decoration: none;
47 }
48 text {
49     fill: black;
50 }
51 svg {
52     font-size: 12px;
53     font-weight: bold;
54     color: #999;
55     font-family:'Arial', sans-serif;
56     min-height: 100%;
57     min-width: 100%;
58 }
59 button:disabled {
60     color:red;
61     background-color: lightyellow;
62 }
63 .sliceinteractions_column {
64   display: table-cell;\r
65   padding: 10px;\r
66 }
67 #interactions_function {
68   width: 125px;
69 }
70
71 </style>
72
73 <div class="row">
74     <div class="sliceinteractions_column">
75     <select id="interactions_function">
76         <option value="networks">networks</option>
77         <option value="users">users</option>
78         <option value="owner sites">sites</option>
79         <option value="sliver_sites">sliver_sites</option>
80         <option value="sliver_nodes">sliver_nodes</option>
81     </select>
82     </div>
83     <div class="sliceinteractions_column">
84     <h3 id="sliceEngagementTitle">Slice Interactions</h3>
85     </div>
86 </div>
87
88 <div id="slice_interaction_chart_placeholder"></div>
89
90 <script>
91
92 // Chord Diagram for showing Collaboration between users found in an anchor query
93 // Collaboration View
94 //
95
96 var width = 600,
97     height = 600,
98     outerRadius = Math.min(width, height) / 2 - 100,
99     innerRadius = outerRadius - 18;
100
101 //create number formatting functions
102 var formatPercent = d3.format("%");
103 var numberWithCommas = d3.format("0,f");
104
105 //define the default chord layout parameters
106 //within a function that returns a new layout object;
107 //that way, you can create multiple chord layouts
108 //that are the same except for the data.
109 function getDefaultLayout() {
110     return d3.layout.chord()
111     .sortSubgroups(d3.descending)
112     .sortChords(d3.ascending);
113 }
114 var last_layout; //store layout between updates
115 var g;
116 var arc;
117 var path;
118
119 function init_visualization() {
120     arc = d3.svg.arc()
121         .innerRadius(innerRadius)
122         .outerRadius(outerRadius);
123
124     path = d3.svg.chord()
125         .radius(innerRadius);
126
127
128     /*** Initialize the visualization ***/
129     g = d3.select("#slice_interaction_chart_placeholder").append("svg")
130             .attr("width", width)
131             .attr("height", height)
132         .append("g")
133             .attr("id", "circle")
134             .attr("transform",
135                   "translate(" + width / 2 + "," + height / 2 + ")");
136     //the entire graphic will be drawn within this <g> element,
137     //so all coordinates will be relative to the center of the circle
138
139     g.append("circle")
140         .attr("r", outerRadius);
141 }
142
143 $( document ).ready(function() {
144     init_visualization();
145     $('#interactions_function').change(function() {
146          updateInteractions();
147      });
148     updateInteractions();
149 });
150
151 function updateInteractions() {
152  $( "#sliceEngagementTitle" ).html("<h3>Loading...</h3>");
153  $.ajax({
154     url : "/admin/sliceinteractions/" + $("#interactions_function :selected").text() + "/",
155     dataType : 'json',
156     type : 'GET',
157     success: function(newData)
158     {
159         $( "#sliceEngagementTitle" ).html("<h3>" + newData["title"] + "</h3>");
160         updateChords(newData["groups"], newData["matrix"], newData["objectName"])
161     }
162    });
163 }
164
165
166 /* Create OR update a chord layout from a data matrix */
167 function updateChords( users, matrix, objectName ) {
168
169     /* Compute chord layout. */
170     layout = getDefaultLayout(); //create a new layout object
171     layout.matrix(matrix);
172  
173     /* Create/update "group" elements */
174     var groupG = g.selectAll("g.group")
175         .data(layout.groups(), function (d) {
176             return d.index; 
177             //use a key function in case the 
178             //groups are sorted differently between updates
179         });
180     
181     groupG.exit()
182         .transition()
183             .duration(1500)
184             .attr("opacity", 0)
185             .remove(); //remove after transitions are complete
186
187     var newGroups = groupG.enter().append("g")
188         .attr("class", "group");
189     //the enter selection is stored in a variable so we can
190     //enter the <path>, <text>, and <title> elements as well
191
192     
193     //Create the title tooltip for the new groups
194     newGroups.append("title");
195     
196     //Update the (tooltip) title text based on the data
197     groupG.select("title")
198         .text(function(d, i) {
199             return "Slice (" + users[i].name +
200                 ") "
201                 ;
202         });
203
204     //create the arc paths and set the constant attributes
205     //(those based on the group index, not on the value)
206     newGroups.append("path")
207         .attr("id", function (d) {
208             return "group" + d.index;
209             //using d.index and not i to maintain consistency
210             //even if groups are sorted
211         })
212         .style("fill", function (d) {
213             return users[d.index].color;
214         });
215     
216     //update the paths to match the layout
217     groupG.select("path") 
218         .transition()
219             .duration(1500)
220             .attr("opacity", 0.5) //optional, just to observe the transition
221         .attrTween("d", arcTween( last_layout ))
222        //     .transition().duration(100).attr("opacity", 1) //reset opacity
223         ;
224     
225     //create the group labels
226     newGroups.append("svg:text")
227         .attr("xlink:href", function (d) {
228             return "#group" + d.index;
229         })
230         .attr("dy", ".35em")
231         .attr("color", "#fff")
232         .text(function (d) {
233             return users[d.index].name;
234         });
235
236     //position group labels to match layout
237     groupG.select("text")
238         .transition()
239             .duration(1500)
240             .attr("transform", function(d) {
241                 d.angle = (d.startAngle + d.endAngle) / 2;
242                 //store the midpoint angle in the data object
243                 
244                 return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" +
245                     " translate(" + (innerRadius + 26) + ")" + 
246                     (d.angle > Math.PI ? " rotate(180)" : " rotate(0)"); 
247                 //include the rotate zero so that transforms can be interpolated
248             })
249             .attr("text-anchor", function (d) {
250                 return d.angle > Math.PI ? "end" : "begin";
251             });
252     
253     
254     /* Create/update the chord paths */
255     var chordPaths = g.selectAll("path.chord")
256         .data(layout.chords(), chordKey );
257             //specify a key function to match chords
258             //between updates
259         
260     
261     //create the new chord paths
262     var newChords = chordPaths.enter()
263         .append("path")
264         .attr("class", "chord");
265     
266     // Add title tooltip for each new chord.
267     newChords.append("title");
268     
269     // Update all chord title texts
270     chordPaths.select("title")
271         .text(function(d) {
272             if (users[d.target.index].name !== users[d.source.index].name) {
273                 return [numberWithCommas(d.source.value),
274                         " " + objectName + " in common between \n",
275                         users[d.source.index].name,
276                         " and ",
277                         users[d.target.index].name,
278                         "\n"
279                         ].join("");
280                     //joining an array of many strings is faster than
281                     //repeated calls to the '+' operator,
282                     //and makes for neater code!
283             }
284             else { //source and target are the same
285                 return numberWithCommas(d.source.value)
286                     + " " + objectName + " are only in Slice ("
287                     + users[d.source.index].name + ")";
288             }
289         });
290
291     //handle exiting paths:
292     chordPaths.exit().transition()
293         .duration(1500)
294         .attr("opacity", 0)
295         .remove();
296
297     //update the path shape
298     chordPaths.transition()
299         .duration(1500)
300         //.attr("opacity", 0.5) //optional, just to observe the transition
301         .style("fill", function (d) {
302             return users[d.source.index].color;
303         })
304         .attrTween("d", chordTween(last_layout))
305         //.transition().duration(100).attr("opacity", 1) //reset opacity
306     ;
307
308     // XXX SMBAKER: The way the text was added with newGroups, it's only
309     //   computed when a node is created. This is a problem if we redraw the
310     //   graph with a different set of nodes, because the old labels will
311     //   stick. So, I added this, which *seems* to cause the labels to be
312     //   recomputed.
313     groupG.selectAll("text")
314         .text(function (d) {
315             return users[d.index].name;
316         });
317
318     //add the mouseover/fade out behaviour to the groups
319     //this is reset on every update, so it will use the latest
320     //chordPaths selection
321     groupG.on("mouseover", function(d) {
322         chordPaths.classed("fade", function (p) {
323             //returns true if *neither* the source or target of the chord
324             //matches the group that has been moused-over
325             return ((p.source.index != d.index) && (p.target.index != d.index));
326         });
327     });
328     //the "unfade" is handled with CSS :hover class on g#circle
329     //you could also do it using a mouseout event:
330     /*
331     g.on("mouseout", function() {
332         if (this == g.node() )
333             //only respond to mouseout of the entire circle
334             //not mouseout events for sub-components
335             chordPaths.classed("fade", false);
336     });
337     */
338
339     // XXX smbaker: there's a bug where if you hilight a slice of the chord
340     //   graph, and then update the data, the freshly drawn graph is missing
341     //   some of the chords. Flipping the fade bit seems to fix that.
342     chordPaths.classed("fade", true);
343     chordPaths.classed("fade", false);
344
345     last_layout = layout; //save for next update
346     
347 //  }); //end of d3.json
348 }
349
350 function arcTween(oldLayout) {
351     //this function will be called once per update cycle
352     
353     //Create a key:value version of the old layout's groups array
354     //so we can easily find the matching group 
355     //even if the group index values don't match the array index
356     //(because of sorting)
357     var oldGroups = {};
358     if (oldLayout) {
359         oldLayout.groups().forEach( function(groupData) {
360             oldGroups[ groupData.index ] = groupData;
361         });
362     }
363     
364     return function (d, i) {
365         var tween;
366         var old = oldGroups[d.index];
367         if (old) { //there's a matching old group
368             tween = d3.interpolate(old, d);
369         }
370         else {
371             //create a zero-width arc object
372             var emptyArc = {startAngle:d.startAngle,
373                             endAngle:d.startAngle};
374             tween = d3.interpolate(emptyArc, d);
375         }
376         
377         return function (t) {
378             return arc( tween(t) );
379         };
380     };
381 }
382
383 function chordKey(data) {
384     return (data.source.index < data.target.index) ?
385         data.source.index  + "-" + data.target.index:
386         data.target.index  + "-" + data.source.index;
387     
388     //create a key that will represent the relationship
389     //between these two groups *regardless*
390     //of which group is called 'source' and which 'target'
391 }
392 function chordTween(oldLayout) {
393     //this function will be called once per update cycle
394     
395     //Create a key:value version of the old layout's chords array
396     //so we can easily find the matching chord 
397     //(which may not have a matching index)
398     
399     var oldChords = {};
400     
401     if (oldLayout) {
402         oldLayout.chords().forEach( function(chordData) {
403             oldChords[ chordKey(chordData) ] = chordData;
404         });
405     }
406     
407     return function (d, i) {
408         //this function will be called for each active chord
409         
410         var tween;
411         var old = oldChords[ chordKey(d) ];
412         if (old) {
413             //old is not undefined, i.e.
414             //there is a matching old chord value
415             
416             //check whether source and target have been switched:
417             if (d.source.index != old.source.index ){
418                 //swap source and target to match the new data
419                 old = {
420                     source: old.target,
421                     target: old.source
422                 };
423             }
424             
425             tween = d3.interpolate(old, d);
426         }
427         else {
428             //create a zero-width chord object
429 /*          XXX SMBAKER: the code commented out below was causing an error,
430                   so I replaced it with the following code from stacktrace
431             if (oldLayout) {
432                 var oldGroups = oldLayout.groups().filter(function(group) {
433                         return ( (group.index == d.source.index) ||
434                                  (group.index == d.target.index) )
435                     });
436                 old = {source:oldGroups[0],
437                            target:oldGroups[1] || oldGroups[0] };
438                     //the OR in target is in case source and target are equal
439                     //in the data, in which case only one group will pass the
440                     //filter function
441
442                 if (d.source.index != old.source.index ){
443                     //swap source and target to match the new data
444                     old = {
445                         source: old.target,
446                         target: old.source
447                     };
448                 }
449             }
450             else old = d;
451
452             var emptyChord = {
453                 source: { startAngle: old.source.startAngle,
454                          endAngle: old.source.startAngle},
455                 target: { startAngle: old.target.startAngle,
456                          endAngle: old.target.startAngle}
457             };
458             tween = d3.interpolate( emptyChord, d );*/
459
460             //create a zero-width chord object
461             var emptyChord = {\r
462                 source: { startAngle: d.source.startAngle,\r
463                          endAngle: d.source.startAngle},\r
464                 target: { startAngle: d.target.startAngle,\r
465                          endAngle: d.target.startAngle}\r
466             };\r
467             tween = d3.interpolate( emptyChord, d );
468         }
469
470         return function (t) {
471             //this function calculates the intermediary shapes
472             return path(tween(t));
473         };
474     };
475 }
476
477
478 /* Activate the buttons and link to data sets */
479 d3.select("#ReadersButton").on("click", function () {
480     updateChords( "#readinfo" );
481     //replace this with a file url as appropriate
482     
483     //enable other buttons, disable this one
484     disableButton(this);
485 });
486
487 d3.select("#ContributorsButton").on("click", function() {
488     updateChords( "#contributorinfo" );
489     disableButton(this);
490 });
491
492 d3.select("#AllUsersButton").on("click", function() {
493     updateChords( "#allinfo" );
494     disableButton(this);
495 });
496 function disableButton(buttonNode) {
497     d3.selectAll("button")
498         .attr("disabled", function(d) {
499             return this === buttonNode? "true": null;
500         });
501 }
502
503 </script>