简体   繁体   English

使用D3更改和转换和弦图中的数据集

[英]Change and transition dataset in chord diagram with D3

I'm working on a chord diagram using D3. 我正在使用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. 我已经查看了http://exposedata.com/tutorial/chord/latest.htmlhttp://fleetinbeing.net/d3e/chord.html ,并尝试使用其中的一些元素来使其工作。

Here is the JavaScript to create the "default" diagram: 以下是用于创建“默认”图表的JavaScript:

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 + "\n" +
        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". 这就是我要做的事情,让它用新数据重新渲染图表(有一个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));
    }
  }
}

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 . 使用d3创建和弦图有许多层,对应于d3将数据操作数据可视化仔细分离。 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 . d3 和弦布局工具会获取有关不同组之间交互的数据,并创建一组数据对象,这些对象包含原始数据,但也会分配角度测量值。 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. 与其他d3布局工具一样,您可以通过调用函数( d3.layout.chord() )来创建和弦布局对象,然后在布局对象上调用其他方法来更改默认设置。 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. 相反,您的数据是布局的另一个设置,您可以使用.matrix()方法定义该布局,并将其存储在布局对象中。 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. 在处理更新时,布局对象存储数据这一事实非常重要,因为如果仍需要旧数据进行转换,则必须注意不要用new覆盖旧数据。

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. 在设置数据矩阵后,通过在和弦布局上调用.groups()来访问组数据对象。 Each group is equivalent to a row in your data matrix (ie, 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. 在设置数据矩阵后,通过在和弦布局上调用.chords()来访问和弦数据对象。 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. 例如,在@ latortue09的示例中,关系是邻域之间的自行车旅行,因此表示邻居A和邻居B之间的旅程的和弦表示从A到B的旅行次数以及从B到A的旅行次数。如果邻居A是在排a数据矩阵的和居民区b是行b ,则这些值应该是在data[a][b]data[b][a]分别。 (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. 每个和弦数据对象都有两个属性, sourcetarget ,每个属性都是自己的数据对象。 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 . 具有较大值的方向确定哪个组称为source ,哪个组称为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. 因此,如果从邻居A到邻居B有200次旅行,但是从B到A的500次旅行,那么该和弦对象的source将代表邻居B的圆圈段的一部分,并且target将代表邻居A的一部分圈子。 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. 如果在任一方向上邻居A和邻居B之间没有旅行,那么这些组将没有和弦数据对象。 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. Chord Layout工具创建数据对象数组,将数据矩阵中的信息转换为圆的角度。 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. 要创建和弦图的标准SVG表示,可以使用d3选择来创建连接到布局数据对象数组的元素。 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. 因为和弦图中有两个不同的布局数据对象阵列,一个用于和弦,一个用于组,有两个不同的d3选择。

In the simplest case, both selections would contain <path> elements (and the two types of paths would be distinguished by class). 在最简单的情况下,两个选择都将包含<path>元素(并且两种类型的路径将按类区分)。 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. 连接到和弦图组的数据数组的<path>成为圆外部的圆弧,而连接到和弦数据的<path>成为圆上的圆弧。

The shape of a <path> is determined by its "d" (path data or directions) attribute. <path>的形状由其"d" (路径数据或方向)属性确定。 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. D3有各种路径数据生成器 ,它们是接收数据对象并创建可用于路径"d"属性的字符串的函数。 Each path generator is created by calling a d3 method, and each can be modified by calling it's own methods. 每个路径生成器都是通过调用d3方法创建的,每个路径生成器都可以通过调用它自己的方法来修改。

The groups in a standard chord diagram are drawn using the d3.svg.arc() path data generator . 使用d3.svg.arc()路径数据生成器绘制标准和弦图中的 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; 期望使用startAngleendAngle属性传递默认的arc生成器; 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 . 它们的形状使用d3.svg.chord()路径数据生成器定义 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. 这是一个简单的例子,只有<path>元素。 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. 如果您还希望文本标签与您的组或和弦相关联,那么您的数据将连接到<g>元素, <path>元素和标签的<text>元素(以及任何其他元素,如刻度线)头发颜色示例中的标记线是继承它的数据对象的子项。 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. 首先,为了最小化代码总量,我通常建议将update方法设置为初始化方法的两倍。 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 is an update or a new visualization. 是的,您仍然需要对更新中永远不会更改的内容执行一些初始化步骤,但是对于实际绘制基于数据的形状,您只需要一个函数,无论这是更新还是新的可视化。

For this example, the initialization steps will include creating the <svg> and the centered <g> element, as well as reading in the array of information about the different neighbourhoods. 对于此示例,初始化步骤将包括创建<svg>和居中的<g>元素,以及读取有关不同邻域的信息数组。 Then the initialization method will call the update method with a default data matrix. 然后初始化方法将使用默认数据矩阵调用update方法。 The buttons that switch to a different data matrix will call the same method. 切换到不同数据矩阵的按钮将调用相同的方法。

/*** Initialize the visualization ***/
var g = d3.select("#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);

d3.csv("data/neighborhoods.csv", function(error, neighborhoodData) {

    if (error) {alert("Error reading file: ", error.statusText); return; }

    neighborhoods = neighborhoodData; 
        //store in variable accessible by other functions
    updateChords(dataset); 
    //call the update method with the default dataset url

} ); //end of d3.csv function

/* example of an update trigger */
d3.select("#MenOnlyButton").on("click", function() {
    updateChords( "/data/men_trips.json" );
    disableButton(this);
});

I'm just passing a data url to the update function, which means that the first line of that function will be a data-parsing function call. 我只是将数据url传递给update函数,这意味着该函数的第一行将是一个数据解析函数调用。 The resulting data matrix is used as the matrix for a new data layout object. 结果数据矩阵用作数据布局对象的矩阵。 We need a new layout object in order to keep a copy of the old layout for the transition functions. 我们需要一个新的布局对象,以便为转换函数保留旧布局的副本。 (If you weren't going to transition the changes, you could just call the matrix method on the same layout to create the new one.) To minimize code duplication, I use a function to create the new layout object and to set all its options: (如果你不打算转换更改,你可以在同一个布局上调用matrix方法来创建新的。)为了最大限度地减少代码重复,我使用一个函数来创建新的布局对象并设置它的全部选项:

/* Create OR update a chord layout from a data matrix */
function updateChords( datasetURL ) {

  d3.json(datasetURL, function(error, matrix) {

    if (error) {alert("Error reading file: ", error.statusText); return; }

    /* Compute chord layout. */
    layout = getDefaultLayout(); //create a new layout object
    layout.matrix(matrix);

    /* main part of update method goes here */

  }); //end of d3.json
}

Then on to the main part of the update-or-create drawing funcion: you're going to need to break down all your method chains into four parts for data join, enter, exit and update. 然后是更新或创建绘图功能的主要部分:您需要将所有方法链分解为四个部分,用于数据连接,输入,退出和更新。 This way, you can handle the creation new elements during an update (eg, new chords for groups that didn't have a relationship in the previous data set) with the same code that you use to handle the original creation of the visualization. 这样,您可以在更新期间处理创建新元素(例如,在先前数据集中没有关系的组的新和弦),以及用于处理可视化的原始创建的相同代码。

First, the data join chain. 首先,数据连接链。 One for the groups and one for the chords. 一组用于组,一组用于和弦。
To maintain object constancy through transitions -- and to reduce the number of graphical properties you have to set on update -- you'll want to set a key function within your data join. 要通过转换维护对象的持久性 - 并减少必须在更新时设置的图形属性的数量 - 您需要在数据连接中设置关键功能。 By default, d3 matches data to elements within a selection based only on their order in the page/array. 默认情况下,d3仅根据页面/数组中的顺序将数据与选择中的元素进行匹配。 Because our chord layout's .chords() array doesn't include chords were there is zero relationship in this data set, the order of the chords can be inconsistent between update rounds. 因为我们的和弦布局的.chords()数组不包含和弦,所以此数据集中没有关系,和弦的顺序在更新轮次之间可能不一致。 The .groups() array could also be re-sorted into orders that don't match the original data matrix, so we also add a key function for that to be safe. .groups()数组也可以重新排序为与原始数据矩阵不匹配的顺序,因此我们还添加了一个关键函数,以确保安全。 In both cases, the key functions are based on the .index properties that the chord layout stored in the data objects. 在这两种情况下,关键函数都基于和弦布局存储在数据对象中的.index属性。

/* 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
    });

/* Create/update the chord paths */
var chordPaths = g.selectAll("path.chord")
    .data(layout.chords(), chordKey );
        //specify a key function to match chords
        //between updates

/* Elsewhere, chordKey is defined as: */

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'
}

Note that the chords are <path> elements, but the groups are <g> elements, which will contain both a <path> and a <text> . 请注意,和弦是<path>元素,但组是<g>元素,它们包含<path><text>

The variables created in this step are data-join selections; 在此步骤中创建的变量是数据连接选择; they will contain all the existing elements (if any) that matched the selector and matched a data value, and they will contain null pointers for any data values which did not match an existing element. 它们将包含与选择器匹配匹配数据值的所有现有元素(如果有),并且它们将包含与现有元素不匹配的任何数据值的空指针。 They also have the .enter() and .exit() methods to access those chains. 它们还具有.enter().exit()方法来访问这些链。

Second, the enter chain. 第二,进入链条。 For all the data objects which didn't match an element (which is all of them if this is the first time the visualization is drawn), we need to create the element and its child components. 对于与元素不匹配的所有数据对象(如果这是第一次绘制可视化对象,则全部是这些对象),我们需要创建元素及其子组件。 At this time, you want to also set any attributes that are constant for all elements (regardless of the data), or which are based on the data values that you use in the key function, and therefore won't change on update. 此时,您还要为所有元素设置任何常量属性(无论数据如何),或者哪些属性都基于您在键功能中使用的数据值,因此在更新时不会更改。

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");

//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 neighborhoods[d.index].color;
    });

//create the group labels
newGroups.append("svg:text")
    .attr("dy", ".35em")
    .attr("color", "#fff")
    .text(function (d) {
        return neighborhoods[d.index].name;
    });


//create the new chord paths
var newChords = chordPaths.enter()
    .append("path")
    .attr("class", "chord");

// Add title tooltip for each new chord.
newChords.append("title");

Note that the fill colours for the group arcs is set on enter, but not the fill colours for the chords. 请注意,组弧的填充颜色在输入时设置,但不在和弦的填充颜色上设置。 That's because the chord colour is going to change depending on which group (of the two the chord connects) is called 'source' and which is 'target', ie, depending on which direction of the relationship is stronger (has more trips). 那是因为和弦颜色将根据哪个组(两个和弦连接)被称为'源'并且是'目标'而改变,即,取决于关系的哪个方向更强(具有更多的行程)。

Third, the update chain. 三,更新链。 When you append an element to an .enter() selection, that new element replaces the null place holder in the original data-join selection. 将元素追加到.enter()选项时,该新元素将替换原始数据连接选择中的空占位符。 After that, if you manipulate the original selection, the settings get applied to both the new and the updating elements. 之后,如果您操作原始选择,则设置将应用于新元素和更新元素。 So this is where you set any properties that depend on the data. 因此,您可以在此处设置依赖于数据的任何属性。

//Update the (tooltip) title text based on the data
groupG.select("title")
    .text(function(d, i) {
        return numberWithCommas(d.value) 
            + " trips started in " 
            + neighborhoods[i].name;
    });

//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(10).attr("opacity", 1) //reset opacity
    ;

//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";
        });

// Update all chord title texts
chordPaths.select("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,
                    "\n",
                    numberWithCommas(d.target.value),
                    " trips from ",
                    neighborhoods[d.target.index].name,
                    " to ",
                    neighborhoods[d.source.index].name
                    ].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) 
                + " trips started and ended in " 
                + neighborhoods[d.source.index].name;
        }
    });

//update the path shape
chordPaths.transition()
    .duration(1500)
    .attr("opacity", 0.5) //optional, just to observe the transition
    .style("fill", function (d) {
        return neighborhoods[d.source.index].color;
    })
    .attrTween("d", chordTween(last_layout))
    .transition().duration(10).attr("opacity", 1) //reset opacity
;

//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 on the g#circle

The changes are done using d3 transitions to create a smooth shift from one diagram to another. 使用d3过渡完成更改,以创建从一个图到另一个图的平滑移位。 For the changes to the path shapes, custom functions are used to do the transition while maintaining the overall shape. 对于路径形状的更改,使用自定义函数进行转换,同时保持整体形状。 More about those below. 更多关于以下的内容。

Fourth, the exit() chain. 四,exit()链。 If any elements from the previous diagram no longer have a match in the new data -- for example, if a chord doesn't exist because there are no relationships between those two groups (eg, no trips between those two neighbourhoods) in this data set -- then you have to remove that element from the visualization. 如果上一个图中的任何元素在新数据中不再匹配 - 例如,如果一个和弦不存在,因为这两个组之间没有关系(例如,这两个邻域之间没有跳闸) set - 然后你必须从可视化中删除该元素。 You can either remove them immediately, so they disappear to make room for transitioning data, or you can use a transition them out and then remove. 您可以立即删除它们,以便它们消失以便为转换数据腾出空间,或者您可以使用它们转换它们然后删除。 (Calling .remove() on a transition-selection will remove the element when that transition completes.) (在转换选择上调用.remove()将在转换完成时删除元素。)

You could create a custom transition to make shapes shrink into nothing, but I just use a fade-out to zero opacity: 你可以创建一个自定义过渡,使形状缩小为空,但我只是使用淡出到零不透明度:

//handle exiting groups, if any, and all their sub-components:
groupG.exit()
    .transition()
        .duration(1500)
        .attr("opacity", 0)
        .remove(); //remove after transitions are complete


//handle exiting paths:
chordPaths.exit().transition()
    .duration(1500)
    .attr("opacity", 0)
    .remove();

About the custom tween functions: 关于自定义补间功能:

If you just used a default tween to switch from one path shape to another, the results can look kind of strange . 如果您只是使用默认补间从一个路径形状切换到另一个路径形状, 结果看起来有点奇怪 Try switching from "Men Only" to "Women Only" and you'll see that the chords get disconnected from the edge of the circle. 尝试从“仅限男性”切换到“仅限女性”,您将看到和弦与圆形边缘断开连接。 If the arc positions had changed more significantly, you would see them crossing the circle to reach their new position instead of sliding around the ring. 如果弧形位置发生了更大的变化,您会看到它们越过圆圈到达新位置而不是在环上滑动。

That's because the default transition from one path shape to another just matches up points on the path and transitions each point in a straight line from one to the other. 这是因为从一个路径形状到另一个路径形状的默认转换恰好匹配路径上的点,并将每个点从一个直线过渡到另一个。 It works for any type of shape without any extra code, but it doesn't necessarily maintain that shape throughout the transition. 它适用于任何类型的形状,无需任何额外的代码,但它不一定在整个过渡期间保持这种形状。

The custom tween function lets you define how the path should be shaped at every step of the transition. 自定义补间功能允许您定义在转换的每个步骤中路径的形状。 I've written up comments about tween functions here and here , so I'm not going to rehash it. 我已经在这里这里写了关于补间函数的评论,所以我不打算重复它。 But the short description is that the tween function you pass to .attrTween(attribute, tween) has to be a function that gets called once per element, and must itself return a function that will be called at every "tick" of the transition to return the attribute value at that point in the transition. 但简短的描述是你传递给.attrTween(attribute, tween)的补间函数必须是每个元素被调用一次的函数,并且必须自己返回一个函数,该函数将在转换的每个“tick”处被调用。返回转换中该点的属性值。

To get smooth transitions of path shapes, we use the two path data generator functions -- the arc generator and the chord generator -- to create the path data at each step of the transition. 为了获得路径形状的平滑过渡,我们使用两个路径数据生成器函数 - 弧生成器和和弦生成器 - 在过渡的每个步骤创建路径数据。 That way, the arcs will always look like arcs and the chords will always look like chords. 这样,弧线总是看起来像弧形,和弦总是看起来像和弦。 The part that is transitioning is the start and end angle values. 正在转换的部分是开始和结束角度值。 Given two different data objects that describe the same type of shape, but with different angle values, you can use d3.interpolateObject(a,b) to create a function that will give you an object at each stage of the transition that with appropriately transitioned angle properties. 给定两个描述相同类型形状但具有不同角度值的不同数据对象,您可以使用d3.interpolateObject(a,b)创建一个函数,该函数将在转换的每个阶段为您提供一个对象,并且已适当转换角度属性。 So if you have the data object from the old layout and the matching data object from the new layout, you can smoothly shift the arcs or chords from one position to the other. 因此,如果您拥有旧布局中的数据对象和新布局中的匹配数据对象,则可以将弧或弦平滑地从一个位置移动到另一个位置。

However, what should you do if you don't have an old data object? 但是,如果您没有旧数据对象,应该怎么做? Either because this chord didn't have a match in the old layout, or because this is the first time the visualization is drawn and there is no old layout. 或者是因为这个和弦并没有在老布局相匹配,或者是因为这是第一次可视化绘制, 没有旧的布局。 If you pass an empty object as the first parameter to d3.interpolateObject , the transitioned object will always be exactly the final value. 如果将空对象作为第一个参数传递给d3.interpolateObject ,则转换的对象将始终完全是最终值。 In combination with other transitions, such as opacity, this could be acceptable. 与其他过渡相结合,例如不透明度,这是可以接受的。 However, I decided to make the transition such that it starts with a zero-width shape -- that is, a shape where the start angles match the end angles -- and then expands to the final shape: 但是,我决定进行过渡,使其以零宽度形状开始 - 即,起始角度与结束角度匹配的形状 - 然后扩展到最终形状:

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
            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));
        };
    };
}

(Check the fiddle for the arc tween code, which is slightly simpler) (检查弧补间代码的小提琴,这稍微简单一些)

Live version altogether: http://jsfiddle.net/KjrGF/12/ 现场版: http //jsfiddle.net/KjrGF/12/

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM