1 <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
3 #slice_interaction_chart_placeholder {
11 font: 10px sans-serif;
26 transition: opacity 0.3s;
28 #circle:hover path.fade {
32 text-decoration: none;
33 border-bottom: 1px dotted #666;
46 text-decoration: none;
55 font-family:'Arial', sans-serif;
61 background-color: lightyellow;
63 .sliceinteractions_column {
64 display: table-cell;
\r
67 #interactions_function {
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>
83 <div class="sliceinteractions_column">
84 <h3 id="sliceEngagementTitle">Slice Interactions</h3>
88 <div id="slice_interaction_chart_placeholder"></div>
92 // Chord Diagram for showing Collaboration between users found in an anchor query
98 outerRadius = Math.min(width, height) / 2 - 100,
99 innerRadius = outerRadius - 18;
101 //create number formatting functions
102 var formatPercent = d3.format("%");
103 var numberWithCommas = d3.format("0,f");
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);
114 var last_layout; //store layout between updates
119 function init_visualization() {
121 .innerRadius(innerRadius)
122 .outerRadius(outerRadius);
124 path = d3.svg.chord()
125 .radius(innerRadius);
128 /*** Initialize the visualization ***/
129 g = d3.select("#slice_interaction_chart_placeholder").append("svg")
130 .attr("width", width)
131 .attr("height", height)
133 .attr("id", "circle")
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
140 .attr("r", outerRadius);
143 $( document ).ready(function() {
144 init_visualization();
145 $('#interactions_function').change(function() {
146 updateInteractions();
148 updateInteractions();
151 function updateInteractions() {
152 $( "#sliceEngagementTitle" ).html("<h3>Loading...</h3>");
154 url : "/admin/sliceinteractions/" + $("#interactions_function :selected").text() + "/",
157 success: function(newData)
159 $( "#sliceEngagementTitle" ).html("<h3>" + newData["title"] + "</h3>");
160 updateChords(newData["groups"], newData["matrix"], newData["objectName"])
166 /* Create OR update a chord layout from a data matrix */
167 function updateChords( users, matrix, objectName ) {
169 /* Compute chord layout. */
170 layout = getDefaultLayout(); //create a new layout object
171 layout.matrix(matrix);
173 /* Create/update "group" elements */
174 var groupG = g.selectAll("g.group")
175 .data(layout.groups(), function (d) {
177 //use a key function in case the
178 //groups are sorted differently between updates
185 .remove(); //remove after transitions are complete
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
193 //Create the title tooltip for the new groups
194 newGroups.append("title");
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 +
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
212 .style("fill", function (d) {
213 return users[d.index].color;
216 //update the paths to match the layout
217 groupG.select("path")
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
225 //create the group labels
226 newGroups.append("svg:text")
227 .attr("xlink:href", function (d) {
228 return "#group" + d.index;
231 .attr("color", "#fff")
233 return users[d.index].name;
236 //position group labels to match layout
237 groupG.select("text")
240 .attr("transform", function(d) {
241 d.angle = (d.startAngle + d.endAngle) / 2;
242 //store the midpoint angle in the data object
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
249 .attr("text-anchor", function (d) {
250 return d.angle > Math.PI ? "end" : "begin";
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
261 //create the new chord paths
262 var newChords = chordPaths.enter()
264 .attr("class", "chord");
266 // Add title tooltip for each new chord.
267 newChords.append("title");
269 // Update all chord title texts
270 chordPaths.select("title")
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,
277 users[d.target.index].name,
280 //joining an array of many strings is faster than
281 //repeated calls to the '+' operator,
282 //and makes for neater code!
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 + ")";
291 //handle exiting paths:
292 chordPaths.exit().transition()
297 //update the path shape
298 chordPaths.transition()
300 //.attr("opacity", 0.5) //optional, just to observe the transition
301 .style("fill", function (d) {
302 return users[d.source.index].color;
304 .attrTween("d", chordTween(last_layout))
305 //.transition().duration(100).attr("opacity", 1) //reset opacity
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));
318 //the "unfade" is handled with CSS :hover class on g#circle
319 //you could also do it using a mouseout event:
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);
329 last_layout = layout; //save for next update
331 // }); //end of d3.json
334 function arcTween(oldLayout) {
335 //this function will be called once per update cycle
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)
343 oldLayout.groups().forEach( function(groupData) {
344 oldGroups[ groupData.index ] = groupData;
348 return function (d, i) {
350 var old = oldGroups[d.index];
351 if (old) { //there's a matching old group
352 tween = d3.interpolate(old, d);
355 //create a zero-width arc object
356 var emptyArc = {startAngle:d.startAngle,
357 endAngle:d.startAngle};
358 tween = d3.interpolate(emptyArc, d);
361 return function (t) {
362 return arc( tween(t) );
367 function chordKey(data) {
368 return (data.source.index < data.target.index) ?
369 data.source.index + "-" + data.target.index:
370 data.target.index + "-" + data.source.index;
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'
376 function chordTween(oldLayout) {
377 //this function will be called once per update cycle
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)
386 oldLayout.chords().forEach( function(chordData) {
387 oldChords[ chordKey(chordData) ] = chordData;
391 return function (d, i) {
392 //this function will be called for each active chord
395 var old = oldChords[ chordKey(d) ];
397 //old is not undefined, i.e.
398 //there is a matching old chord value
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
409 tween = d3.interpolate(old, d);
412 //create a zero-width chord object
413 /* XXX SMBAKER: the code commented out below was causing an error,
414 so I replaced it with the following code from stacktrace
416 var oldGroups = oldLayout.groups().filter(function(group) {
417 return ( (group.index == d.source.index) ||
418 (group.index == d.target.index) )
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
426 if (d.source.index != old.source.index ){
427 //swap source and target to match the new data
437 source: { startAngle: old.source.startAngle,
438 endAngle: old.source.startAngle},
439 target: { startAngle: old.target.startAngle,
440 endAngle: old.target.startAngle}
442 tween = d3.interpolate( emptyChord, d );*/
444 //create a zero-width chord object
446 source: { startAngle: d.source.startAngle,
\r
447 endAngle: d.source.startAngle},
\r
448 target: { startAngle: d.target.startAngle,
\r
449 endAngle: d.target.startAngle}
\r
451 tween = d3.interpolate( emptyChord, d );
454 return function (t) {
455 //this function calculates the intermediary shapes
456 return path(tween(t));
462 /* Activate the buttons and link to data sets */
463 d3.select("#ReadersButton").on("click", function () {
464 updateChords( "#readinfo" );
465 //replace this with a file url as appropriate
467 //enable other buttons, disable this one
471 d3.select("#ContributorsButton").on("click", function() {
472 updateChords( "#contributorinfo" );
476 d3.select("#AllUsersButton").on("click", function() {
477 updateChords( "#allinfo" );
480 function disableButton(buttonNode) {
481 d3.selectAll("button")
482 .attr("disabled", function(d) {
483 return this === buttonNode? "true": null;