blob: 3ddaa1f5eec4341980698c4c82549250b34c0a58 [file] [log] [blame]
Scott Baker4ee5b6d2014-03-27 09:17:59 -07001<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
2<style>
3#slice_interaction_chart_placeholder {
4 text-align: center;
Scott Baker4ee5b6d2014-03-27 09:17:59 -07005 color:#fff;
6 position: relative;
7 height: 100%;
8 width: 100%;
9}
10.dependencyWheel {
11 font: 10px sans-serif;
12}
13form .btn-primary {
14 margin-top: 25px;
15}
16.labeltext {
17 color: #fff;
18}
19#circle circle {
20 fill: none;
21 pointer-events: all;
22}
23path.chord {
24 stroke: #000;
25 stroke-width: .10px;
26 transition: opacity 0.3s;
27}
28#circle:hover path.fade {
29 opacity: 0;
30}
31a {
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}
42a:hover {
43 color: #45b8e2;
44}
45a:not(:hover) {
46 text-decoration: none;
47}
48text {
49 fill: black;
50}
51svg {
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}
59button:disabled {
60 color:red;
61 background-color: lightyellow;
62}
Scott Baker6b654202014-05-27 16:55:00 -070063.sliceinteractions_column {
64 display: table-cell;
65 padding: 10px;
66}
67#interactions_function {
68 width: 125px;
Scott Baker4ee5b6d2014-03-27 09:17:59 -070069}
70
71</style>
Scott Baker6b654202014-05-27 16:55:00 -070072
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
Scott Baker4ee5b6d2014-03-27 09:17:59 -070088<div id="slice_interaction_chart_placeholder"></div>
89
90<script>
Scott Baker4ee5b6d2014-03-27 09:17:59 -070091
92// Chord Diagram for showing Collaboration between users found in an anchor query
93// Collaboration View
94//
95
96var width = 600,
97 height = 600,
98 outerRadius = Math.min(width, height) / 2 - 100,
99 innerRadius = outerRadius - 18;
100
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700101//create number formatting functions
102var formatPercent = d3.format("%");
103var numberWithCommas = d3.format("0,f");
104
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700105//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.
109function getDefaultLayout() {
110 return d3.layout.chord()
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700111 .sortSubgroups(d3.descending)
112 .sortChords(d3.ascending);
Scott Baker6b654202014-05-27 16:55:00 -0700113}
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700114var last_layout; //store layout between updates
Scott Baker6b654202014-05-27 16:55:00 -0700115var g;
116var arc;
117var path;
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700118
Scott Baker6b654202014-05-27 16:55:00 -0700119function init_visualization() {
120 arc = d3.svg.arc()
121 .innerRadius(innerRadius)
122 .outerRadius(outerRadius);
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700123
Scott Baker6b654202014-05-27 16:55:00 -0700124 path = d3.svg.chord()
125 .radius(innerRadius);
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700126
127
Scott Baker6b654202014-05-27 16:55:00 -0700128 /*** 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
151function 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}
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700164
165
166/* Create OR update a chord layout from a data matrix */
Scott Baker6b654202014-05-27 16:55:00 -0700167function updateChords( users, matrix, objectName ) {
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700168
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700169 /* 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
Scott Baker6b654202014-05-27 16:55:00 -0700186
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700187 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) {
Scott Baker6b654202014-05-27 16:55:00 -0700199 return "Slice (" + users[i].name +
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700200 ") "
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),
Scott Baker6b654202014-05-27 16:55:00 -0700274 " " + objectName + " in common between \n",
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700275 users[d.source.index].name,
276 " and ",
277 users[d.target.index].name,
278 "\n"
Scott Baker6b654202014-05-27 16:55:00 -0700279 ].join("");
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700280 //joining an array of many strings is faster than
Scott Baker6b654202014-05-27 16:55:00 -0700281 //repeated calls to the '+' operator,
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700282 //and makes for neater code!
Scott Baker6b654202014-05-27 16:55:00 -0700283 }
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700284 else { //source and target are the same
Scott Baker6b654202014-05-27 16:55:00 -0700285 return numberWithCommas(d.source.value)
286 + " " + objectName + " are only in Slice ("
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700287 + 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
Scott Baker5d95e322014-05-27 20:30:37 -0700308 // 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
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700318 //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 */
Scott Baker6b654202014-05-27 16:55:00 -0700338
Scott Baker5d95e322014-05-27 20:30:37 -0700339 // 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
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700345 last_layout = layout; //save for next update
346
347// }); //end of d3.json
348}
349
350function 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
383function 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}
392function 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
Scott Baker6b654202014-05-27 16:55:00 -0700429/* XXX SMBAKER: the code commented out below was causing an error,
430 so I replaced it with the following code from stacktrace
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700431 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
Scott Baker6b654202014-05-27 16:55:00 -0700441
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700442 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;
Scott Baker6b654202014-05-27 16:55:00 -0700451
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700452 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 };
Scott Baker6b654202014-05-27 16:55:00 -0700458 tween = d3.interpolate( emptyChord, d );*/
459
460 //create a zero-width chord object
461 var emptyChord = {
462 source: { startAngle: d.source.startAngle,
463 endAngle: d.source.startAngle},
464 target: { startAngle: d.target.startAngle,
465 endAngle: d.target.startAngle}
466 };
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700467 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 */
479d3.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
487d3.select("#ContributorsButton").on("click", function() {
488 updateChords( "#contributorinfo" );
489 disableButton(this);
490});
491
492d3.select("#AllUsersButton").on("click", function() {
493 updateChords( "#allinfo" );
494 disableButton(this);
495});
496function disableButton(buttonNode) {
497 d3.selectAll("button")
498 .attr("disabled", function(d) {
499 return this === buttonNode? "true": null;
500 });
501}
502
503</script>