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

Javascript 使用D3更改和转换弦图中的数据集,javascript,d3.js,transition,chord-diagram,Javascript,D3.js,Transition,Chord Diagram,我正在用D3绘制和弦图 我试图这样做,当用户单击链接时,数据集将更改为另一个预定义的数据集。我已经研究了和,并尝试使用其中的一些元素使其工作 以下是创建“默认”图表的JavaScript: 下面是我试图用新数据重新呈现图表的步骤(有一个图像元素,其id为id“女性”) 创建和弦图 使用d3创建一个和弦图有很多层,这与d3小心地将数据操作与数据可视化分离相对应。如果您不仅要创建和弦图,还要顺利地更新它,那么您需要清楚地了解程序的每一部分是做什么的,以及它们是如何交互的 首先是数据操作方面。d3获

我正在用D3绘制和弦图

我试图这样做,当用户单击链接时,数据集将更改为另一个预定义的数据集。我已经研究了和,并尝试使用其中的一些元素使其工作

以下是创建“默认”图表的JavaScript:

下面是我试图用新数据重新呈现图表的步骤(有一个图像元素,其id为
id
“女性”)

创建和弦图 使用d3创建一个和弦图有很多层,这与d3小心地将数据操作与数据可视化分离相对应。如果您不仅要创建和弦图,还要顺利地更新它,那么您需要清楚地了解程序的每一部分是做什么的,以及它们是如何交互的

首先是数据操作方面。d3获取关于不同组之间交互的数据,并创建一组数据对象,其中包含原始数据,但也指定了角度测量值。这样,它类似于,但与增加的复杂度相关,存在一些重要的差异和弦布局的连续性

与其他d3布局工具一样,您可以通过调用函数(
d3.layout.chord()
)来创建一个chord布局对象,然后调用布局对象上的其他方法以更改默认设置。但是,与饼图布局工具和大多数其他布局不同,chord布局对象不是将数据作为输入并输出设置了布局属性(角度)的计算数据对象数组的函数

相反,您的数据是布局的另一个设置,由您使用方法定义,并存储在布局对象中。数据必须存储在对象中,因为有两个具有布局属性的不同数据对象数组,一个用于和弦(不同组之间的连接),一个用于组本身。在处理更新时,布局对象存储数据这一事实很重要,因为如果仍然需要旧数据进行转换,则必须小心不要用新数据过度写入旧数据

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
设置数据矩阵后,通过调用chord布局上的
.groups()
来访问组数据对象。每个组相当于数据矩阵中的一行(即数组中的每个子数组)。已为组数据对象指定了表示圆的一部分的开始角度和结束角度值。这与饼图非常相似,不同之处在于,每个组(以及整个圆)的值是通过对整行(子阵列)的值求和来计算的。表示原始矩阵中索引的组数据对象(很重要,因为它们可能按不同顺序排序)及其总值

通过调用
.chords()来访问chord数据对象
在设置数据矩阵后在和弦布局上。每个和弦表示数据矩阵中的两个值,相当于两个组之间的两个可能关系。例如,在@latortue09的示例中,这些关系是邻里之间的自行车行程,因此表示邻里A和Nei之间行程的和弦ghbourhood B表示从A到B的行程数以及从B到A的行程数。如果邻居A位于数据矩阵的
A
行,而邻居B位于
B
行,则这些值应分别位于
data[A][B]
data[B][A]
。(当然,有时您正在绘制的关系不会有这种方向,在这种情况下,您的数据矩阵应该是对称的,这意味着这两个值应该相等。)

每个chord数据对象都有两个属性,
source
target
,每个属性都是自己的数据对象。源数据对象和目标数据对象都包含关于从一个组到另一个组的单向关系的信息,包括组的原始索引和该关系的值,以及开始和结束日期les表示一组圆的一段

源/目标命名有点混乱,因为正如我前面提到的,chord对象表示两个组之间关系的两个方向。值较大的方向决定了哪个组称为
source
,哪个组称为
target
。因此,如果从邻居A到N有200次行程八个B,但从B到A的行程为500次,则该弦对象的
将表示邻域B的圆段的一部分,而
目标
将表示邻域A的圆段的一部分。对于组与自身之间的关系(在本例中,起始和结束于同一邻域的行程)源对象和目标对象是相同的

chord数据对象数组的最后一个重要方面是,它只包含两个组之间存在关系的对象。如果邻域A和邻域B在任一方向上都没有行程,那么这些组将没有chord数据对象。当从一个数据集更新到另一个数据集时,这一点变得非常重要呃

第二,数据可视化方面。和弦布局工具创建数据对象数组,将数据矩阵中的信息转换为圆的角度。但它不会绘制任何东西。要创建和弦图的标准SVG表示形式,可以使用创建与布局数据对象数组关联的元素。因为弦图中有两个不同的布局数据对象数组,一个用于弦,一个用于组,所以有两个不同的d3选择

在最简单的情况下,两种选择都是
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));
    }
  }
}
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
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
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
/*** 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);
});
/* 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
}
/* 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'
}
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");
//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
//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();
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));
        };
    };
}