| <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> |
| <style> |
| #slice_interaction_chart_placeholder { |
| text-align: center; |
| color:#fff; |
| position: relative; |
| height: 100%; |
| width: 100%; |
| } |
| .dependencyWheel { |
| font: 10px sans-serif; |
| } |
| form .btn-primary { |
| margin-top: 25px; |
| } |
| .labeltext { |
| color: #fff; |
| } |
| #circle circle { |
| fill: none; |
| pointer-events: all; |
| } |
| path.chord { |
| stroke: #000; |
| stroke-width: .10px; |
| transition: opacity 0.3s; |
| } |
| #circle:hover path.fade { |
| opacity: 0; |
| } |
| a { |
| text-decoration: none; |
| border-bottom: 1px dotted #666; |
| color: #999; |
| } |
| .more a { |
| color: #666; |
| } |
| .by a { |
| color: #fff; |
| } |
| a:hover { |
| color: #45b8e2; |
| } |
| a:not(:hover) { |
| text-decoration: none; |
| } |
| text { |
| fill: black; |
| } |
| svg { |
| font-size: 12px; |
| font-weight: bold; |
| color: #999; |
| font-family:'Arial', sans-serif; |
| min-height: 100%; |
| min-width: 100%; |
| } |
| button:disabled { |
| color:red; |
| background-color: lightyellow; |
| } |
| .sliceinteractions_column { |
| display: table-cell;
|
| padding: 10px;
|
| } |
| #interactions_function { |
| width: 125px; |
| } |
| |
| </style> |
| |
| <div class="row"> |
| <div class="sliceinteractions_column"> |
| <select id="interactions_function"> |
| <option value="networks">networks</option> |
| <option value="users">users</option> |
| <option value="owner sites">sites</option> |
| <option value="sliver_sites">sliver_sites</option> |
| <option value="sliver_nodes">sliver_nodes</option> |
| </select> |
| </div> |
| <div class="sliceinteractions_column"> |
| <h3 id="sliceEngagementTitle">Slice Interactions</h3> |
| </div> |
| </div> |
| |
| <div id="slice_interaction_chart_placeholder"></div> |
| |
| <script> |
| |
| // Chord Diagram for showing Collaboration between users found in an anchor query |
| // Collaboration View |
| // |
| |
| var width = 600, |
| height = 600, |
| outerRadius = Math.min(width, height) / 2 - 100, |
| innerRadius = outerRadius - 18; |
| |
| //create number formatting functions |
| var formatPercent = d3.format("%"); |
| var numberWithCommas = d3.format("0,f"); |
| |
| //define the default chord layout parameters |
| //within a function that returns a new layout object; |
| //that way, you can create multiple chord layouts |
| //that are the same except for the data. |
| function getDefaultLayout() { |
| return d3.layout.chord() |
| .sortSubgroups(d3.descending) |
| .sortChords(d3.ascending); |
| } |
| var last_layout; //store layout between updates |
| var g; |
| var arc; |
| var path; |
| |
| function init_visualization() { |
| arc = d3.svg.arc() |
| .innerRadius(innerRadius) |
| .outerRadius(outerRadius); |
| |
| path = d3.svg.chord() |
| .radius(innerRadius); |
| |
| |
| /*** Initialize the visualization ***/ |
| g = d3.select("#slice_interaction_chart_placeholder").append("svg") |
| .attr("width", width) |
| .attr("height", height) |
| .append("g") |
| .attr("id", "circle") |
| .attr("transform", |
| "translate(" + width / 2 + "," + height / 2 + ")"); |
| //the entire graphic will be drawn within this <g> element, |
| //so all coordinates will be relative to the center of the circle |
| |
| g.append("circle") |
| .attr("r", outerRadius); |
| } |
| |
| $( document ).ready(function() { |
| init_visualization(); |
| $('#interactions_function').change(function() { |
| updateInteractions(); |
| }); |
| updateInteractions(); |
| }); |
| |
| function updateInteractions() { |
| $( "#sliceEngagementTitle" ).html("<h3>Loading...</h3>"); |
| $.ajax({ |
| url : "/admin/sliceinteractions/" + $("#interactions_function :selected").text() + "/", |
| dataType : 'json', |
| type : 'GET', |
| success: function(newData) |
| { |
| $( "#sliceEngagementTitle" ).html("<h3>" + newData["title"] + "</h3>"); |
| updateChords(newData["groups"], newData["matrix"], newData["objectName"]) |
| } |
| }); |
| } |
| |
| |
| /* Create OR update a chord layout from a data matrix */ |
| function updateChords( users, matrix, objectName ) { |
| |
| /* Compute chord layout. */ |
| layout = getDefaultLayout(); //create a new layout object |
| layout.matrix(matrix); |
| |
| /* Create/update "group" elements */ |
| var groupG = g.selectAll("g.group") |
| .data(layout.groups(), function (d) { |
| return d.index; |
| //use a key function in case the |
| //groups are sorted differently between updates |
| }); |
| |
| groupG.exit() |
| .transition() |
| .duration(1500) |
| .attr("opacity", 0) |
| .remove(); //remove after transitions are complete |
| |
| var newGroups = groupG.enter().append("g") |
| .attr("class", "group"); |
| //the enter selection is stored in a variable so we can |
| //enter the <path>, <text>, and <title> elements as well |
| |
| |
| //Create the title tooltip for the new groups |
| newGroups.append("title"); |
| |
| //Update the (tooltip) title text based on the data |
| groupG.select("title") |
| .text(function(d, i) { |
| return "Slice (" + users[i].name + |
| ") " |
| ; |
| }); |
| |
| //create the arc paths and set the constant attributes |
| //(those based on the group index, not on the value) |
| newGroups.append("path") |
| .attr("id", function (d) { |
| return "group" + d.index; |
| //using d.index and not i to maintain consistency |
| //even if groups are sorted |
| }) |
| .style("fill", function (d) { |
| return users[d.index].color; |
| }); |
| |
| //update the paths to match the layout |
| groupG.select("path") |
| .transition() |
| .duration(1500) |
| .attr("opacity", 0.5) //optional, just to observe the transition |
| .attrTween("d", arcTween( last_layout )) |
| // .transition().duration(100).attr("opacity", 1) //reset opacity |
| ; |
| |
| //create the group labels |
| newGroups.append("svg:text") |
| .attr("xlink:href", function (d) { |
| return "#group" + d.index; |
| }) |
| .attr("dy", ".35em") |
| .attr("color", "#fff") |
| .text(function (d) { |
| return users[d.index].name; |
| }); |
| |
| //position group labels to match layout |
| groupG.select("text") |
| .transition() |
| .duration(1500) |
| .attr("transform", function(d) { |
| d.angle = (d.startAngle + d.endAngle) / 2; |
| //store the midpoint angle in the data object |
| |
| return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" + |
| " translate(" + (innerRadius + 26) + ")" + |
| (d.angle > Math.PI ? " rotate(180)" : " rotate(0)"); |
| //include the rotate zero so that transforms can be interpolated |
| }) |
| .attr("text-anchor", function (d) { |
| return d.angle > Math.PI ? "end" : "begin"; |
| }); |
| |
| |
| /* Create/update the chord paths */ |
| var chordPaths = g.selectAll("path.chord") |
| .data(layout.chords(), chordKey ); |
| //specify a key function to match chords |
| //between updates |
| |
| |
| //create the new chord paths |
| var newChords = chordPaths.enter() |
| .append("path") |
| .attr("class", "chord"); |
| |
| // Add title tooltip for each new chord. |
| newChords.append("title"); |
| |
| // Update all chord title texts |
| chordPaths.select("title") |
| .text(function(d) { |
| if (users[d.target.index].name !== users[d.source.index].name) { |
| return [numberWithCommas(d.source.value), |
| " " + objectName + " in common between \n", |
| users[d.source.index].name, |
| " and ", |
| users[d.target.index].name, |
| "\n" |
| ].join(""); |
| //joining an array of many strings is faster than |
| //repeated calls to the '+' operator, |
| //and makes for neater code! |
| } |
| else { //source and target are the same |
| return numberWithCommas(d.source.value) |
| + " " + objectName + " are only in Slice (" |
| + users[d.source.index].name + ")"; |
| } |
| }); |
| |
| //handle exiting paths: |
| chordPaths.exit().transition() |
| .duration(1500) |
| .attr("opacity", 0) |
| .remove(); |
| |
| //update the path shape |
| chordPaths.transition() |
| .duration(1500) |
| //.attr("opacity", 0.5) //optional, just to observe the transition |
| .style("fill", function (d) { |
| return users[d.source.index].color; |
| }) |
| .attrTween("d", chordTween(last_layout)) |
| //.transition().duration(100).attr("opacity", 1) //reset opacity |
| ; |
| |
| // XXX SMBAKER: The way the text was added with newGroups, it's only |
| // computed when a node is created. This is a problem if we redraw the |
| // graph with a different set of nodes, because the old labels will |
| // stick. So, I added this, which *seems* to cause the labels to be |
| // recomputed. |
| groupG.selectAll("text") |
| .text(function (d) { |
| return users[d.index].name; |
| }); |
| |
| //add the mouseover/fade out behaviour to the groups |
| //this is reset on every update, so it will use the latest |
| //chordPaths selection |
| groupG.on("mouseover", function(d) { |
| chordPaths.classed("fade", function (p) { |
| //returns true if *neither* the source or target of the chord |
| //matches the group that has been moused-over |
| return ((p.source.index != d.index) && (p.target.index != d.index)); |
| }); |
| }); |
| //the "unfade" is handled with CSS :hover class on g#circle |
| //you could also do it using a mouseout event: |
| /* |
| g.on("mouseout", function() { |
| if (this == g.node() ) |
| //only respond to mouseout of the entire circle |
| //not mouseout events for sub-components |
| chordPaths.classed("fade", false); |
| }); |
| */ |
| |
| // XXX smbaker: there's a bug where if you hilight a slice of the chord |
| // graph, and then update the data, the freshly drawn graph is missing |
| // some of the chords. Flipping the fade bit seems to fix that. |
| chordPaths.classed("fade", true); |
| chordPaths.classed("fade", false); |
| |
| last_layout = layout; //save for next update |
| |
| // }); //end of d3.json |
| } |
| |
| function arcTween(oldLayout) { |
| //this function will be called once per update cycle |
| |
| //Create a key:value version of the old layout's groups array |
| //so we can easily find the matching group |
| //even if the group index values don't match the array index |
| //(because of sorting) |
| var oldGroups = {}; |
| if (oldLayout) { |
| oldLayout.groups().forEach( function(groupData) { |
| oldGroups[ groupData.index ] = groupData; |
| }); |
| } |
| |
| return function (d, i) { |
| var tween; |
| var old = oldGroups[d.index]; |
| if (old) { //there's a matching old group |
| tween = d3.interpolate(old, d); |
| } |
| else { |
| //create a zero-width arc object |
| var emptyArc = {startAngle:d.startAngle, |
| endAngle:d.startAngle}; |
| tween = d3.interpolate(emptyArc, d); |
| } |
| |
| return function (t) { |
| return arc( tween(t) ); |
| }; |
| }; |
| } |
| |
| function chordKey(data) { |
| return (data.source.index < data.target.index) ? |
| data.source.index + "-" + data.target.index: |
| data.target.index + "-" + data.source.index; |
| |
| //create a key that will represent the relationship |
| //between these two groups *regardless* |
| //of which group is called 'source' and which 'target' |
| } |
| function chordTween(oldLayout) { |
| //this function will be called once per update cycle |
| |
| //Create a key:value version of the old layout's chords array |
| //so we can easily find the matching chord |
| //(which may not have a matching index) |
| |
| var oldChords = {}; |
| |
| if (oldLayout) { |
| oldLayout.chords().forEach( function(chordData) { |
| oldChords[ chordKey(chordData) ] = chordData; |
| }); |
| } |
| |
| return function (d, i) { |
| //this function will be called for each active chord |
| |
| var tween; |
| var old = oldChords[ chordKey(d) ]; |
| if (old) { |
| //old is not undefined, i.e. |
| //there is a matching old chord value |
| |
| //check whether source and target have been switched: |
| if (d.source.index != old.source.index ){ |
| //swap source and target to match the new data |
| old = { |
| source: old.target, |
| target: old.source |
| }; |
| } |
| |
| tween = d3.interpolate(old, d); |
| } |
| else { |
| //create a zero-width chord object |
| /* XXX SMBAKER: the code commented out below was causing an error, |
| so I replaced it with the following code from stacktrace |
| if (oldLayout) { |
| var oldGroups = oldLayout.groups().filter(function(group) { |
| return ( (group.index == d.source.index) || |
| (group.index == d.target.index) ) |
| }); |
| old = {source:oldGroups[0], |
| target:oldGroups[1] || oldGroups[0] }; |
| //the OR in target is in case source and target are equal |
| //in the data, in which case only one group will pass the |
| //filter function |
| |
| if (d.source.index != old.source.index ){ |
| //swap source and target to match the new data |
| old = { |
| source: old.target, |
| target: old.source |
| }; |
| } |
| } |
| else old = d; |
| |
| var emptyChord = { |
| source: { startAngle: old.source.startAngle, |
| endAngle: old.source.startAngle}, |
| target: { startAngle: old.target.startAngle, |
| endAngle: old.target.startAngle} |
| }; |
| tween = d3.interpolate( emptyChord, d );*/ |
| |
| //create a zero-width chord object |
| var emptyChord = {
|
| source: { startAngle: d.source.startAngle,
|
| endAngle: d.source.startAngle},
|
| target: { startAngle: d.target.startAngle,
|
| endAngle: d.target.startAngle}
|
| };
|
| tween = d3.interpolate( emptyChord, d ); |
| } |
| |
| return function (t) { |
| //this function calculates the intermediary shapes |
| return path(tween(t)); |
| }; |
| }; |
| } |
| |
| |
| /* Activate the buttons and link to data sets */ |
| d3.select("#ReadersButton").on("click", function () { |
| updateChords( "#readinfo" ); |
| //replace this with a file url as appropriate |
| |
| //enable other buttons, disable this one |
| disableButton(this); |
| }); |
| |
| d3.select("#ContributorsButton").on("click", function() { |
| updateChords( "#contributorinfo" ); |
| disableButton(this); |
| }); |
| |
| d3.select("#AllUsersButton").on("click", function() { |
| updateChords( "#allinfo" ); |
| disableButton(this); |
| }); |
| function disableButton(buttonNode) { |
| d3.selectAll("button") |
| .attr("disabled", function(d) { |
| return this === buttonNode? "true": null; |
| }); |
| } |
| |
| </script> |