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