Ian2400
Ian2400

Reputation: 51

Adding and removing elements from D3 Visualization with animation

long time user first time asking a question here (first time I haven't been able to solve a problem using answers I searched for). I've recreated the uber chord chart in JS D3 with my own data. My implementation can be found here, though that address may not stay good forever so here is the code (forgive a few misalignments throughout):

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script type="text/javascript" src="d3/d3.js"></script>
    <script type="text/javascript" src="d3/d3.layout.js"></script>
    <link type="text/css" rel="stylesheet" href="style.css"/>
  </head>
  <body>
    <div id="body">
      <div id="footer">
        Purdue OIR Testing - Migration
        <div class="hint">mouseover groups to highlight</div>
      </div>
    </div>
    <div id="tooltip"></div>
    <script type="text/javascript">


//import the data and call the draw chords function 
d3.text("migrationdata.csv", function(data) {
    var matrix = d3.csv.parseRows(data).map(function(row) {
        return row.map(function(value) {
            return +value;
        });
    });
    d3.text("headersColors.csv", function(headerdata) {
    var headersColors = d3.csv.parseRows(headerdata);
    var headers = headersColors[1];
    var colors = headersColors[2];

    drawChords(matrix, headers, colors);
 });
});

//create the chord viz
function drawChords (matrix, headers, colors){

    var w = 980,
        h = 800,
        r1 = h / 2,
        r0 = r1 - 110,
        fadeOutA = 0,
        fadeInA = 0.8;

    var fill = d3.scale.category20c();

    var chord = d3.layout.chord()
        .padding(.02)
        .sortSubgroups(d3.descending)
        .sortChords(d3.descending);

    var arc = d3.svg.arc()
        .innerRadius(r0)
        .outerRadius(r0 + 20);

    var svg = d3.select("body").append("svg:svg")
        .attr("width", w)
        .attr("height", h)
      .append("svg:g")
        .attr("id", "circle")
        .attr("transform", "translate(" + w / 2 + "," + h / 2 + ")");

    svg.append("circle")
        .attr("r", r0 + 20);
        //.attr("fill-opacity",0);

    //assign the matrix
      chord.matrix(matrix);

    //create the groups
      var g = svg.selectAll("g.group")
          .data(chord.groups)
        .enter().append("svg:g")
          .attr("class", "group")
          .on("mouseover", mouseover)
          .on("mouseout", function (d) { d3.select("#tooltip").style("visibility", "hidden") });

      g.append("svg:path")
          .style("stroke", function(d) { return colors[d.index]; })
          .style("fill", function(d) { return colors[d.index]; })
          .attr("d", arc);

      g.append("svg:text")
          .each(function(d) { d.angle = (d.startAngle + d.endAngle) / 2; })
          .attr("dy", ".35em")
          .attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
          .attr("transform", function(d) {
            return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
                + "translate(" + (r0 + 26) + ")"
                + (d.angle > Math.PI ? "rotate(180)" : "");
          })
          .text(function(d) { return headers[d.index]; });

      var chordPaths = svg.selectAll("path.chord")
          .data(chord.chords)
        .enter().append("svg:path")
          .attr("class", "chord")
          .style("stroke", function(d) { return d3.rgb(colors[d.source.index]).darker(); })
          .style("fill", function(d) { return colors[d.source.index]; })
          .attr("d", d3.svg.chord().radius(r0))
          .on("mouseover", function (d) {
              d3.select("#tooltip")
                .style("visibility","visible")
                .html(chordTip(d))
                .style("left", (d3.event.pageX - 100) + "px")     
                .style("top", (d3.event.pageY - 100) + "px");  
               })
           .on("mouseout", function (d) { d3.select("#tooltip").style("visibility", "hidden") });

      function chordTip (d) {
        var p = d3.format(".1%"), q = d3.format(",.2r")
        return "Migration Info:<br/>"
          +  headers[d.source.index] + " → " + headers[d.target.index]
          + ": " + Math.round(d.source.value) + "<br/>"
          + headers[d.target.index] + " → " + headers[d.source.index]
          + ": " + Math.round(d.target.value) + "<br/>";
      }

      function groupTip (d) {
        return "College Info:<br/>"
            + headers[d.index] + " : " + Math.round(d.value) + "<br/>";
        }

      function mouseover(d, i) {
        d3.select("#tooltip")
          .style("visibility", "visible")
          .html(groupTip(d))
          .style("top", function () { return (d3.event.pageY - 80)+"px"})
          .style("left", function () { return (d3.event.pageX - 130)+"px";})

        chordPaths.classed("fade", function(p) {
          return p.source.index != i
              && p.target.index != i;
        });
  }

}



// Returns an event handler for fading a given chord group.
function fade(opacity) {
  return function(d, i) {
    svg.selectAll("path.chord")
        .filter(function(d) { return d.source.index != i && d.target.index != i; })
      .transition()
        .style("stroke-opacity", opacity)
        .style("fill-opacity", opacity);
  };
}

    </script>
  </body>
</html>

This works fine, and is totally functional. It shows student movement between colleges from Fall 2012-Fall 2013 (I plan to make the explanation on screen a bit better as I go). My next step is to make this drillable. So for instance, if you click a college I want to expand that college out into its departments and show the chords between various departments and "Other" which would be the combination of all other colleges. Further, you would be able to drill into department to get major-level detail in the same fashion. I have all the raw data I need to do this, and I am fairly confident I know how I can use it to create the new matrices on the fly when clicking on a group.

My question is, doing what I describe above is all fairly straight forward if I just dump the current viz for a new one with new data each time, but that's going to be a harsh transition. I'd prefer something with animation like this. However, that example only works if your matrices are the same size on each end of the transition. Because of how I'd like to drill into this data, that will not always be true for me. I could have fewer or more groups/chords than I did before. My question is, if I create a new chord layout with a bigger/smaller matrix than what is currently on screen, can I somehow smoothly animate the new/un-needed groups/chords in/away? If so, how would I go about this and are there any examples out there?

I'm still fairly new to D3 but I'm trying to learn quickly as there is a ton of demand here for these types of visualizations. If there is a tutorial on this feel free to simply link it, I've been researching this on and off for a few days now and haven't found anything satisfactory explaining how to smoothly add/remove elements in a d3 layout.

Upvotes: 3

Views: 4335

Answers (1)

Ian2400
Ian2400

Reputation: 51

In order to answer this, I used AmeliaBR's excellent answer to this other question: here

I made a few changes to this solution:

First, I updated the group exit to have a starting opacity. The way it was coded, anything that exited would pop out rather than fade out if I used attributes rather than styles, since transition requires a starting attribute value and can't grab the current value:

        groupG.exit()
        .attr("fill-opacity", 1)
        .attr("stroke-opacity", 1)
        .transition()
            .duration(300)
            //change fill and stroke opacity to avoid CSS conflicts
            .attr("fill-opacity", 0)
            .attr("stroke-opacity", 0)
            .remove(); //remove after transitions are complete

This does have the effect of causing faded chords to quickly fade up and down during the animation, but it looks smoother than pop-out so I kept it.

Also, I had issues in IE, specifically because our installation of IE on campus defaulted document mode to IE7, and you need at least IE9 for this to work. It just took one line of code to force this change:

<meta http-equiv="X-UA-Compatible" content="IE=edge" />

Also, pointer-events doesn't work in IE earlier than IE11 and I used a div tooltip instead of the tooltips in the example, so I needed to set the tooltip far enough away from the mouse that the user wouldn't touch it. This is a... less than elegant solution, and I would like to find a better one but it probably isn't worth the time.

My final solution can be found here. I plan on updating this with 1) unscrambling some of the un-necessary chord overlap and 2) making my data updating functions work with a template dataset, instead of the one specific dataset I created for this purpose.

Upvotes: 2

Related Questions