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 // 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
313 groupG.selectAll("text")
315 return users[d.index].name;
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));
328 //the "unfade" is handled with CSS :hover class on g#circle
329 //you could also do it using a mouseout event:
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);
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);
345 last_layout = layout; //save for next update
347 // }); //end of d3.json
350 function arcTween(oldLayout) {
351 //this function will be called once per update cycle
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)
359 oldLayout.groups().forEach( function(groupData) {
360 oldGroups[ groupData.index ] = groupData;
364 return function (d, i) {
366 var old = oldGroups[d.index];
367 if (old) { //there's a matching old group
368 tween = d3.interpolate(old, d);
371 //create a zero-width arc object
372 var emptyArc = {startAngle:d.startAngle,
373 endAngle:d.startAngle};
374 tween = d3.interpolate(emptyArc, d);
377 return function (t) {
378 return arc( tween(t) );
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;
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'
392 function chordTween(oldLayout) {
393 //this function will be called once per update cycle
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)
402 oldLayout.chords().forEach( function(chordData) {
403 oldChords[ chordKey(chordData) ] = chordData;
407 return function (d, i) {
408 //this function will be called for each active chord
411 var old = oldChords[ chordKey(d) ];
413 //old is not undefined, i.e.
414 //there is a matching old chord value
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
425 tween = d3.interpolate(old, d);
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
432 var oldGroups = oldLayout.groups().filter(function(group) {
433 return ( (group.index == d.source.index) ||
434 (group.index == d.target.index) )
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
442 if (d.source.index != old.source.index ){
443 //swap source and target to match the new data
453 source: { startAngle: old.source.startAngle,
454 endAngle: old.source.startAngle},
455 target: { startAngle: old.target.startAngle,
456 endAngle: old.target.startAngle}
458 tween = d3.interpolate( emptyChord, d );*/
460 //create a zero-width chord object
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
467 tween = d3.interpolate( emptyChord, d );
470 return function (t) {
471 //this function calculates the intermediary shapes
472 return path(tween(t));
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
483 //enable other buttons, disable this one
487 d3.select("#ContributorsButton").on("click", function() {
488 updateChords( "#contributorinfo" );
492 d3.select("#AllUsersButton").on("click", function() {
493 updateChords( "#allinfo" );
496 function disableButton(buttonNode) {
497 d3.selectAll("button")
498 .attr("disabled", function(d) {
499 return this === buttonNode? "true": null;