Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
3.4k views
in Technique[技术] by (71.8m points)

javascript - Change and transition dataset in chord diagram with D3

I'm working on a chord diagram using D3.

I am trying to make it so that when a user clicks on a link the dataset will change to another predefined dataset. I've looked at both http://exposedata.com/tutorial/chord/latest.html and http://fleetinbeing.net/d3e/chord.html, and have tried to use some elements in there to get it to work.

Here is the JavaScript to create the "default" diagram:

var dataset = "data/all_trips.json";

var width = 650,
    height = 600,
    outerRadius = Math.min(width, height) / 2 - 25,
    innerRadius = outerRadius - 18;

var formatPercent = d3.format("%");

var arc = d3.svg.arc()
    .innerRadius(innerRadius)
    .outerRadius(outerRadius);

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

var path = d3.svg.chord()
    .radius(innerRadius);

var svg = d3.select("#chart_placeholder").append("svg")
    .attr("width", width)
    .attr("height", height)
  .append("g")
    .attr("id", "circle")
    .attr("transform", "translate(" + width / 1.5 + "," + height / 1.75 + ")");

svg.append("circle")
    .attr("r", outerRadius);

d3.csv("data/neighborhoods.csv", function(neighborhoods) {
  d3.json(dataset, function(matrix) {

    // Compute chord layout.
    layout.matrix(matrix);

    // Add a group per neighborhood.
    var group = svg.selectAll(".group")
        .data(layout.groups)
      .enter().append("g")
        .attr("class", "group")
        .on("mouseover", mouseover);

    // Add a mouseover title.
    group.append("title").text(function(d, i) {
      return numberWithCommas(d.value) + " trips started in " + neighborhoods[i].name;
    });

    // Add the group arc.
    var groupPath = group.append("path")
        .attr("id", function(d, i) { return "group" + i; })
        .attr("d", arc)
        .style("fill", function(d, i) { return neighborhoods[i].color; });

    var rootGroup = d3.layout.chord().groups()[0];

    // Text label radiating outward from the group.
    var groupText = group.append("text");

   group.append("svg:text")
        .each(function(d) { d.angle = (d.startAngle + d.endAngle) / 2; })
        .attr("xlink:href", function(d, i) { return "#group" + i; })
        .attr("dy", ".35em")
        .attr("color", "#fff")
        .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(" + (innerRadius + 26) + ")" +
            (d.angle > Math.PI ? "rotate(180)" : "");
        })
        .text(function(d, i) { return neighborhoods[i].name; });

    // Add the chords.
    var chord = svg.selectAll(".chord")
        .data(layout.chords)
      .enter().append("path")
        .attr("class", "chord")
        .style("fill", function(d) { return neighborhoods[d.source.index].color; })
        .attr("d", path);

    // Add mouseover for each chord.
    chord.append("title").text(function(d) {
      if (!(neighborhoods[d.target.index].name === neighborhoods[d.source.index].name)) {
      return numberWithCommas(d.source.value) + " trips from " + neighborhoods[d.source.index].name + " to " + neighborhoods[d.target.index].name + "
" +
        numberWithCommas(d.target.value) + " trips from " + neighborhoods[d.target.index].name + " to " + neighborhoods[d.source.index].name;
      } else {
        return numberWithCommas(d.source.value) + " trips started and ended in " + neighborhoods[d.source.index].name;
      }
    });

    function mouseover(d, i) {
      chord.classed("fade", function(p) {
        return p.source.index != i
            && p.target.index != i;
      });
      var selectedOrigin = d.value;
      var selectedOriginName = neighborhoods[i].name;
    }
  });
});

And here's what I'm trying to do to make it re-render the chart with the new data (there is an image element with the id "female".

d3.select("#female").on("click", function () {
  var new_data = "data/women_trips.json";
  reRender(new_data);
});

function reRender(data) {
  var layout = d3.layout.chord()
  .padding(.03)
  .sortSubgroups(d3.descending)
  .matrix(data);

  // Update arcs

  svg.selectAll(".group")
  .data(layout.groups)
  .transition()
  .duration(1500)
  .attrTween("d", arcTween(last_chord));

  // Update chords

  svg.select(".chord")
     .selectAll("path")
     .data(layout.chords)
     .transition()
     .duration(1500)
     .attrTween("d", chordTween(last_chord))

};

var arc =  d3.svg.arc()
      .startAngle(function(d) { return d.startAngle })
      .endAngle(function(d) { return d.endAngle })
      .innerRadius(r0)
      .outerRadius(r1);

var chordl = d3.svg.chord().radius(r0);

function arcTween(layout) {
  return function(d,i) {
    var i = d3.interpolate(layout.groups()[i], d);

    return function(t) {
      return arc(i(t));
    }
  }
}

function chordTween(layout) {
  return function(d,i) {
    var i = d3.interpolate(layout.chords()[i], d);

    return function(t) {
      return chordl(i(t));
    }
  }
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Creating a chord diagram

There are a number of layers to creating a chord diagram with d3, corresponding to d3's careful separation of data manipulation from data visualization. If you're going to not only create a chord diagram, but also update it smoothly, you'll need to clearly understand what each piece of the program does and how they interact.

Sample Chord Diagram, from the example linked above

First, the data-manipulation aspect. The d3 Chord Layout tool takes your data about the interactions between different groups and creates a set of data objects which contain the original data but are also assigned angle measurements . In this way, it is similar to the pie layout tool, but there are some important differences related to the increased complexity of the chord layout.

Like the other d3 layout tools, you create a chord layout object by calling a function (d3.layout.chord()), and then you call additional methods on the layout object to change the default settings. Unlike the pie layout tool and most of the other layouts, however, the chord layout object isn't a function that takes your data as input and outputs the calculated array of data objects with layout attributes (angles) set.

Instead, your data is another setting for the layout, which you define with the .matrix() method, and which is stored within the layout object. The data has to be stored within the object because there are two different arrays of data objects with layout attributes, one for the chords (connections between different groups), and one for the groups themselves. The fact that the layout object stores the data is important when dealing with updates, as you have to be careful not to over-write old data with new if you still need the old data for transitions.

var chordLayout = d3.layout.chord() //create layout object
                  .sortChords( d3.ascending ) //set a property
                  .padding( 0.01 ); //property-setting methods can be chained

chordLayout.matrix( data );  //set the data matrix

The group data objects are accessed by calling .groups() on the chord layout after the data matrix has been set. Each group is equivalent to a row in your data matrix (i.e., each subarray in an array of arrays). The group data objects have been assigned start angle and end angle values representing a section of the circle. This much is just like a pie graph, with the difference being that the values for each group (and for the circle as a whole) are calculated by summing up values for the entire row (subarray). The group data objects also have properties representing their index in the original matrix (important because they might be sorted into a different order) and their total value.

The chord data objects are accessed by calling .chords() on the chord layout after the data matrix has been set. Each chord represents two values in the data matrix, equivalent to the two possible relationships between two groups. For example, in @latortue09's example, the relationships are bicycle trips between neighbourhoods, so the chord that represents trips between Neighbourhood A and Neighbourhood B represents the number of trips from A to B as well as the number from B to A. If Neighbourhood A is in row a of your data matrix and Neighbourhood B is in row b, then these values should be at data[a][b] and data[b][a], respectively. (Of course, sometimes the relationships you're drawing won't have this type of direction to them, in which case your data matrix should be symmetric, meaning that those two values should be equal.)

Each chord data object has two properties, source and target, each of which is its own data object. Both the source and target data object have the same structure with information about the one-way relationship from one group to the other, including the original indexes of the groups and the value of that relationship, and start and end angles representing a section of one group's segment of the circle.

The source/target naming is kind of confusing, since as I mentioned above, the chord object represents both directions of the relationship between two groups. The direction that has the larger value determines which group is called source and which is called target. So if there are 200 trips from Neighbourhood A to Neighbourhood B, but 500 trips from B to A, then the source for that chord object will represent a section of Neighbourhood B's segment of the circle, and the target will represent part of Neighbourhood A's segment of the circle. For the relationship between a group and itself (in this example, trips that start and end in the same neighbourhood), the source and target objects are the same.

One final important aspect of the chord data object array is that it only contains objects where relationships between two groups exist. If there are no trips between Neighbourhood A and Neighbourhood B in either direction, then there will be no chord data object for those groups. This becomes important when updating from one dataset to another.

Second, the data-visualization aspect. The Chord Layout tool creates arrays of data objects, converting information from the data matrix into angles of a circle. But it doesn't draw anything. To create the standard SVG representation of a chord diagram, you use d3 selections to create elements joined to an array of layout data objects. Because there are two different arrays of layout data objects in the chord diagram, one for the chords and one for the groups, there are two different d3 selections.

In the simplest case, both selections would contain <path> elements (and the two types of paths would be distinguished by class). The <path>s that are joined to the data array for the chord diagram groups become the arcs around the outside of the circle, while the <path>s that are joined to the data for the chords themselves become the bands across the circle.

The shape of a <path> is determined by its "d" (path data or directions) attribute. D3 has a variety of path data generators, which are functions that take a data object and create a string that can be used for a path's "d" attribute. Each path generator is created by calling a d3 method, and each can be modified by calling it's own methods.

The groups in a standard chord diagram are drawn using the d3.svg.arc() path data generator. This arc generator is the same one used by pie and donut graphs. After all, if you remove the chords from a chord diagram, you essentially just have a donut diagram made up of the group arcs. The default arc generator expects to be passed data objects with startAngle and endAngle properties; the group data objects created by the chord layout works with this default. The arc generator also needs to know the inside and outside radius for the arc. These can be specified as functions of the data or as constants; for the chord diagram they will be constants, the same for every arc.

var arcFunction = d3.svg.arc() //create the arc path generator
                               //with default angle accessors
                  .innerRadius( radius )
                  .outerRadius( radius + bandWidth); 
                               //set constant radius values

var groupPaths = d3.selectAll("path.group")
                 .data( chordLayout.groups() ); 
    //join the selection to the appropriate data object array 
    //from the chord layout 

groupPaths.enter().append("path") //create paths if this isn't an update
          .attr("class", "group"); //set the class
          /* also set any other attributes that are independent of the data */

groupPaths.attr("fill", groupColourFunction )
          //set attributes that are functions of the data
          .attr("d", arcFunction ); //create the shape
   //d3 will pass the data object for each path to the arcFunction
   //which will create the string for the path "d" attribute

The chords in a chord diagram have a shape unique to this type of diagram. Their shapes are defined using the d3.svg.chord() path data generator. The default chord generator expects data of the form created by the chord layout object, the only thing that needs to be specified is the radius of the circle (which will usually be the same as the inner radius of the arc groups).

var chordFunction = d3.svg.chord() //create the chord path generator
                                   //with default accessors
                    .radius( radius );  //set constant radius

var chordPaths = d3.selectAll("path.chord")
                 .data( chordLayout.chords() ); 
    //join the selection to the appropriate data object array 
    //from the chord layout 

chordPaths.enter().append("path") //create paths if this isn't an update
          .attr("class", "chord"); //set the class
          /* also set any other attributes that are independent of the data */

chordPaths.attr("fill", chordColourFunction )
          //set attributes that are functions of the data
          .attr("d", chordFunction ); //create the shape
   //d3 will pass the data object for each path to the chordFunction
   //which will create the string for the path "d" attribute

That's the simple case, with <path> elements only. If you want to also have text labels associated with your groups or chords, then your data is joined to <g> elements, and the <path> elements and the <text> elements for the labels (and any other elements, like the tick mark lines in the hair-colour example) are children of the that inherit it's data object. When you update the graph, you'll need to update all the sub-components that are affected by the data.

Updating a chord diagram

With all that information in mind, how should you approach creating a chord diagram that can be updated with new data?

First, to minimize the total amount of code, I usually recommend making your update method double as your initialization method. Yes, you'll still need some initialization steps for things that never change in the update, but for actually drawing the shapes that are based on the data you should only need one function regardless of whether this


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...