blob: 6fafc5c029083dd7e25b2325f26809504e5e5155 [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
308 //add the mouseover/fade out behaviour to the groups
309 //this is reset on every update, so it will use the latest
310 //chordPaths selection
311 groupG.on("mouseover", function(d) {
312 chordPaths.classed("fade", function (p) {
313 //returns true if *neither* the source or target of the chord
314 //matches the group that has been moused-over
315 return ((p.source.index != d.index) && (p.target.index != d.index));
316 });
317 });
318 //the "unfade" is handled with CSS :hover class on g#circle
319 //you could also do it using a mouseout event:
320 /*
321 g.on("mouseout", function() {
322 if (this == g.node() )
323 //only respond to mouseout of the entire circle
324 //not mouseout events for sub-components
325 chordPaths.classed("fade", false);
326 });
327 */
Scott Baker6b654202014-05-27 16:55:00 -0700328
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700329 last_layout = layout; //save for next update
330
331// }); //end of d3.json
332}
333
334function arcTween(oldLayout) {
335 //this function will be called once per update cycle
336
337 //Create a key:value version of the old layout's groups array
338 //so we can easily find the matching group
339 //even if the group index values don't match the array index
340 //(because of sorting)
341 var oldGroups = {};
342 if (oldLayout) {
343 oldLayout.groups().forEach( function(groupData) {
344 oldGroups[ groupData.index ] = groupData;
345 });
346 }
347
348 return function (d, i) {
349 var tween;
350 var old = oldGroups[d.index];
351 if (old) { //there's a matching old group
352 tween = d3.interpolate(old, d);
353 }
354 else {
355 //create a zero-width arc object
356 var emptyArc = {startAngle:d.startAngle,
357 endAngle:d.startAngle};
358 tween = d3.interpolate(emptyArc, d);
359 }
360
361 return function (t) {
362 return arc( tween(t) );
363 };
364 };
365}
366
367function chordKey(data) {
368 return (data.source.index < data.target.index) ?
369 data.source.index + "-" + data.target.index:
370 data.target.index + "-" + data.source.index;
371
372 //create a key that will represent the relationship
373 //between these two groups *regardless*
374 //of which group is called 'source' and which 'target'
375}
376function chordTween(oldLayout) {
377 //this function will be called once per update cycle
378
379 //Create a key:value version of the old layout's chords array
380 //so we can easily find the matching chord
381 //(which may not have a matching index)
382
383 var oldChords = {};
384
385 if (oldLayout) {
386 oldLayout.chords().forEach( function(chordData) {
387 oldChords[ chordKey(chordData) ] = chordData;
388 });
389 }
390
391 return function (d, i) {
392 //this function will be called for each active chord
393
394 var tween;
395 var old = oldChords[ chordKey(d) ];
396 if (old) {
397 //old is not undefined, i.e.
398 //there is a matching old chord value
399
400 //check whether source and target have been switched:
401 if (d.source.index != old.source.index ){
402 //swap source and target to match the new data
403 old = {
404 source: old.target,
405 target: old.source
406 };
407 }
408
409 tween = d3.interpolate(old, d);
410 }
411 else {
412 //create a zero-width chord object
Scott Baker6b654202014-05-27 16:55:00 -0700413/* XXX SMBAKER: the code commented out below was causing an error,
414 so I replaced it with the following code from stacktrace
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700415 if (oldLayout) {
416 var oldGroups = oldLayout.groups().filter(function(group) {
417 return ( (group.index == d.source.index) ||
418 (group.index == d.target.index) )
419 });
420 old = {source:oldGroups[0],
421 target:oldGroups[1] || oldGroups[0] };
422 //the OR in target is in case source and target are equal
423 //in the data, in which case only one group will pass the
424 //filter function
Scott Baker6b654202014-05-27 16:55:00 -0700425
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700426 if (d.source.index != old.source.index ){
427 //swap source and target to match the new data
428 old = {
429 source: old.target,
430 target: old.source
431 };
432 }
433 }
434 else old = d;
Scott Baker6b654202014-05-27 16:55:00 -0700435
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700436 var emptyChord = {
437 source: { startAngle: old.source.startAngle,
438 endAngle: old.source.startAngle},
439 target: { startAngle: old.target.startAngle,
440 endAngle: old.target.startAngle}
441 };
Scott Baker6b654202014-05-27 16:55:00 -0700442 tween = d3.interpolate( emptyChord, d );*/
443
444 //create a zero-width chord object
445 var emptyChord = {
446 source: { startAngle: d.source.startAngle,
447 endAngle: d.source.startAngle},
448 target: { startAngle: d.target.startAngle,
449 endAngle: d.target.startAngle}
450 };
Scott Baker4ee5b6d2014-03-27 09:17:59 -0700451 tween = d3.interpolate( emptyChord, d );
452 }
453
454 return function (t) {
455 //this function calculates the intermediary shapes
456 return path(tween(t));
457 };
458 };
459}
460
461
462/* Activate the buttons and link to data sets */
463d3.select("#ReadersButton").on("click", function () {
464 updateChords( "#readinfo" );
465 //replace this with a file url as appropriate
466
467 //enable other buttons, disable this one
468 disableButton(this);
469});
470
471d3.select("#ContributorsButton").on("click", function() {
472 updateChords( "#contributorinfo" );
473 disableButton(this);
474});
475
476d3.select("#AllUsersButton").on("click", function() {
477 updateChords( "#allinfo" );
478 disableButton(this);
479});
480function disableButton(buttonNode) {
481 d3.selectAll("button")
482 .attr("disabled", function(d) {
483 return this === buttonNode? "true": null;
484 });
485}
486
487</script>