问题描述
我正在使用D3的和弦图。
我试图让它,使得当用户点击链接时,数据集将更改为另一个预定义数据集。我查看了获取有关不同组之间的交互的数据,并创建一组包含原始数据但也分配了角度测量值的数据对象。这样,它类似于,但有
与其他d3布局工具一样,您可以通过调用函数(<$ c $)来创建和弦布局对象c> d3.layout.chord()),然后在布局对象上调用其他方法来更改默认设置。然而,与饼图布局工具和大多数其他布局不同,和弦布局对象不是将您的数据作为输入并输出计算的布局属性(角度)设置的数据对象数组的函数。
而是您的数据是布局的另一个设置,您可以使用方法,并存储在布局对象中。数据必须存储在对象内,因为存在具有布局属性的两个不同的数据对象阵列,一个用于和弦(不同组之间的连接),一个用于组本身。布局对象存储数据的事实在处理更新时很重要,因为如果您仍需要旧数据进行转换,则必须小心,不要使用新的数据覆盖旧数据。
var chordLayout = d3.layout.chord()//创建布局对象
.sortChords(d3.ascending)//设置属性
.padding(0.01); //属性设置方法可以链接
chordLayout.matrix(data); //设置数据矩阵
通过调用可访问组数据对象。 groups()
在设置数据矩阵之后的和弦布局。每个组等效于数据矩阵中的一行(即数组数组中的每个子数组)。组数据对象已经分配了表示圆的一部分的开始角度和结束角度值。这很像一个饼图,区别在于每个组(和整个圆)的值通过对整个行(子阵列)的值求和来计算。组数据对象,表示其在
和弦数据对象可通过调用 .chords ()
在设置数据矩阵之后的和弦布局。每个和弦在数据矩阵中表示两个值,相当于两个组之间的两个可能的关系。例如,在@ latortue09的例子中,关系是邻域之间的自行车旅行,因此表示邻域A和邻域B之间的旅程的和弦表示从A到B的旅程数目以及从B到A的数目。如果邻域A在您的数据矩阵的行 a
中,邻域B在行 b
中,则这些值应为 data [a] [b]
和 data [b] [a]
(当然,有时你绘制的关系不会有这种类型的方向,在这种情况下,你的数据矩阵应该是对称的,意味着这两个值应该是相等的) / p>
每个和弦数据对象都有两个属性, source
和 target
,每个都是自己的数据对象。源和目标数据对象关于从一个组到另一个组的单向关系的信息,包括组的原始索引和该关系的值,以及表示一个组的圆的段的部分的开始和结束角度。
源/目标命名有点混乱,因为如上所述,和弦对象代表两个两个关系的方向组。具有较大值的方向确定哪个组被称为 source
,并且被称为 target
。因此,如果从邻居A到邻居B有200次旅行,但是从B到A有500次旅行,则用于该和弦对象的源
将表示邻居B的段圆圈,目标
将表示邻域A的圆的一部分。对于组与其自身之间的关系(在此示例中,在同一邻域中开始和结束的行程),源和目标对象是相同的。
和弦数据对象数组的最后一个重要方面是它只包含两个组之间存在关系的对象。如果在任一方向上在邻域A和邻域B之间没有行程,则对于那些组将没有和弦数据对象。这在从一个数据集更新到另一个数据集时变得重要。
其次,数据可视化方面。 Chord Layout工具创建数据对象数组,将数据矩阵中的信息转换为圈。但它不绘制任何东西。要创建和弦图的标准SVG表示形式,您可以使用创建已加入的元素到布局数据对象的数组。因为和弦图中有两个不同的布局数据对象数组,一个用于和弦,一个用于组,所以有两个不同的d3选择。
case,这两个选择将包含< path>
元素(和两种类型的路径将通过类来区分)。连接到和弦图组的数据数组的< path>
成为围绕圆圈外围的弧,而 ;
<$ c>的形状
$ c>< path> 由其属性。 D3有各种 ,它们是接受数据对象并创建可用于路径的d
属性的字符串的函数。每个路径生成器通过调用d3方法创建,每个路径生成器都可以通过调用自己的方法来修改。 标准和弦图中的群组是使用。这个电弧发生器与饼图和圆环图所用的相同。毕竟,如果你从和弦图中删除和弦,你基本上只有一个由组弧组成的圆环图。默认弧生成器期望通过 startAngle
和 endAngle
属性传递数据对象;由和弦布局创建的组数据对象使用此默认值。电弧发生器还需要知道电弧的内部和外部半径。这些可以指定为数据的函数或常量;对于和弦图,它们将是常数,对于每个弧都是相同的。
var arcFunction = d3.svg.arc /创建弧路径生成器
//使用默认角度访问器
.innerRadius(radius)
.outerRadius(radius + bandWidth);
//设置常量半径值
var groupPaths = d3.selectAll(path.group)
.data(chordLayout.groups());
//将选择连接到适当的数据对象数组
//从和弦布局
groupPaths.enter()。append(path)//创建路径if这不是更新
.attr(class,group); //设置类
/ *还设置独立于数据的任何其他属性* /
groupPaths.attr(fill,groupColourFunction)
//设置属性它们是数据的函数
.attr(d,arcFunction); //创建形状
// d3将每个路径的数据对象传递给arcFunction
//这将为路径d属性创建字符串
和弦图中的和弦具有此类型图的唯一形状。他们的形状是使用。默认和弦发生器期望由和弦布局对象创建的表单的数据,需要指定的唯一的事情是圆的半径(通常与弧组的内半径相同)。
var chordFunction = d3.svg.chord()//创建和弦路径生成器
//默认访问器$ b $半径(半径); // set constant radius
var chordPaths = d3.selectAll(path.chord)
.data(chordLayout.chords());
//将选择连接到适当的数据对象数组
//从和弦布局
chordPaths.enter()。append(path)//创建路径if这不是更新
.attr(class,chord); //设置类
/ *还设置独立于数据的任何其他属性* /
chordPaths.attr(fill,chordColourFunction)
//设置属性它们是数据的函数
.attr(d,chordFunction); //创建形状
// d3将每个路径的数据对象传递给chordFunction
//这将为路径d属性创建字符串
这是一个简单的例子,只有< path>
如果您还想要将文本标签与您的群组或和弦相关联,那么您的数据将被加入< g>
元素,<路径>
元素和< text>
元素(以及任何其他元素,如头发颜色示例中的刻度线)是继承它的数据对象的子元素。更新图表时,您需要更新受数据影响的所有子组件。
更新和弦图
考虑到所有这些信息,应如何创建和弦图可以用新数据更新?
首先,为了尽量减少代码总数,我通常建议您将更新初始化方法。是的,你仍然需要一些初始化步骤,以便在更新中不会发生变化,但是实际上绘制基于数据的形状,你只需要一个函数,不管这是一个更新还是一个新的可视化。 / p> 对于此示例,初始化步骤将包括创建 我只是传递一个数据url到更新函数,这意味着第一行函数将是一个数据解析函数调用。结果数据矩阵用作新的
< svg>
和中心< g>
元素,以及读取关于不同邻域的信息数组。然后初始化方法将使用默认数据矩阵调用update方法。切换到不同数据矩阵的按钮将调用相同的方法。
/ ***初始化可视化*** /
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 +));
//整个图形将在此< g>元素,
//所有坐标都将相对于圆的中心
g.append(circle)
.attr(r,outerRadius);
d3.csv(data / neighborhoods.csv,function(error,neighborhoodData){
if(error){alert(Error reading file: .statusText); return;}
neighborhoods = neighborhoodData;
//存储在可被其他函数访问的变量中
updateChords(dataset);
//调用update方法与默认数据集url
}); // d3.csv函数的结束
/ *更新触发器的示例* /
d3.select(#MenOnlyButton)。on(click,function b $ b updateChords(/data/men_trips.json);
disableButton(this);
});
matrix
方法创建一个新的。)为了减少代码重复,我使用创建新布局对象并设置其所有选项的函数:
/ *从数据创建或更新和弦布局矩阵* /
function updateChords(datasetURL){
d3.json(datasetURL,function(error,matrix){
if(error){alert错误读取文件:,error.statusText); return;}
/ *计算和弦布局* /
layout = getDefaultLayout(); //创建一个新的布局对象
layout.matrix(matrix);
/ *更新方法的主要部分在这里* /
}); // end of d3.json
}
然后进入更新的主要部分 - 或者 - 创建绘图函数:您需要将所有,用于数据连接,进入,退出和更新。这样,您可以在更新期间处理创建新元素(例如,在先前数据集中没有关系的组的新和弦),其中使用与用于处理可视化的原始创建的代码相同的代码。
首先是数据连接链。一个用于组,一个用于和弦。
要保持通过转换 - 并减少您必须在更新上设置的图形属性的数量 - - 您将需要在数据连接中设置一个键函数。默认情况下,d3仅根据页面/数组中的顺序将数据匹配到选择内的元素。因为我们的和弦布局的 .chords()
数组不包括和弦,在这个数据集中有零关系,和弦的顺序可能在更新轮次之间不一致。 .groups()
数组也可以重新排序为与原始数据矩阵不匹配的顺序,因此我们还添加了一个关键函数,以确保安全。在这两种情况下,关键函数都基于 .index
属性,并且存储在数据对象中的和弦布局。
/ *创建/更新组元素* /
var groupG = g.selectAll(g.group)
.data(layout.groups ),function(d){
return d.index;
//如果
//组在更新之间有不同排序,使用键函数
});
/ *创建/更新和弦路径* /
var chordPaths = g.selectAll(path.chord)
.data(layout.chords(),chordKey) ;
//指定一个键函数来匹配和弦
//在更新之间
/ *其他地方,chordKey定义为:* /
function chordKey (data){
return(data.source.index< data.target.index)?
data.source.index + - + data.target.index:
data.target.index + - + data.source.index;
//创建一个代表这两个组之间的关系
//的键*不管*
//哪个组被称为'source',而'target'
}
请注意,和弦是< path& / code>元素,但是组是
< g>
元素,它将包含< path>
和< text>
。
此步骤中创建的变量是数据连接选择;它们将包含与选择器和匹配的所有现有元素(如果有的话),并且它们将包含与现有元素不匹配的任何数据值的空指针。它们还具有访问这些链的 .enter()
和 .exit()
方法。
其次,输入链。对于所有不匹配元素的数据对象(如果这是第一次可视化绘制),我们需要创建元素及其子组件。此时,您还要设置对所有元素(不考虑数据)为常量的任何属性,或者基于您在键函数中使用的数据值,因此在更新时不会更改。 / p>
var newGroups = groupG.enter()。append(g)
.attr(class组);
//输入选择存储在变量中,以便我们可以
//输入< path>,< text>和< title>元素以及
//为新组创建标题工具提示
newGroups.append(title);
//创建弧路径并设置常量属性
//(基于组索引,而不是值)
newGroups.append(path)
.attr(id,function(d){
returngroup+ d.index;
//使用d.index而不是i维持一致性
/ /
})
})
.style(fill,function(d){
return neighborhoods [d.index] .color;
//创建组标签
newGroups.append(svg:text)
.attr(dy,.35em)
.attr (color,#fff)
.text(function(d){
return neighborhoods [d.index] .name;
});
//创建新的和弦路径
var newChords = chordPaths.enter()
.append(path)
.attr类,和弦);
//为每个新和弦添加标题工具提示。
newChords.append(title);
请注意,组弧的填充颜色是在enter上设置的,和弦。这是因为和弦颜色将根据哪个组(两个和弦连接)被称为源,哪个是目标,即根据关系的哪个方向更强(具有更多的行程)而改变。
第三,更新链。当您将元素附加到 .enter()
选择,新元素将替换原始数据连接选择中的空占位符。之后,如果您操作原始选择,设置将应用于新的和更新的元素。因此,这里是您设置任何依赖于数据的属性。
//根据data
groupG.select(title)
.text(function(d,i){
return numberWithCommas(d.value)
+ $ b + neighborhoods [i] .name;
});
//更新路径以匹配布局
groupG.select(path)
.transition()
.duration(1500)
.attr(opacity,0.5)//可选,只是观察过渡
.attrTween(d,arcTween(last_layout))
.transition()。duration opacity,1)// reset opacity
;
//定位组标签以匹配布局
groupG.select(text)
.transition()
.duration(1500)
。 attr(transform,function(d){
d.angle =(d.startAngle + d.endAngle)/ 2;
//存储数据对象中的中点角度
returnrotate(+(d.angle * 180 / Math.PI - 90)+)+
translate(+(innerRadius + 26)+)+
(d.angle> Math.PI?rotate(180):rotate(0));
//包括旋转零以使变换可以被插值
}
.attr(text-anchor,function(d){
return d.angle> Math.PI?end:begin;
});
//更新所有和弦标题文本
chordPaths.select(title)
.text(function(d){
if(neighborhoods [d.target .index] .name!==
neighborhoods [d.source.index] .name){
return [numberWithCommas(d.source.value),
,
neighborhoods [d.source.index] .name,
to,
neighborhoods [d.target.index] .name,
\\\
numberWithCommas(d.target.value),
从跳过,
neighborhoods [d.target.index] .name,
to,
neighborhoods [d.source.index] .name
] .join();
//加入许多字符串的数组比
快//重复调用'+' ,
//并为更简洁的代码!
}
else {//源和目标是相同的
return numberWithCommas(d.source.value)
+ 旅行开始并结束于
+ neighborhoods [d.source.index] .name;
}
});
//更新路径形状
chordPaths.transition()
.duration(1500)
.attr(opacity,0.5)//可选,just观察过渡
.style(fill,function(d){
return neighborhoods [d.source.index] .color;
})
.attrTween d,chordTween(last_layout))
.transition()。duration(10).attr(opacity,1)// reset opacity
;
//将鼠标悬停/淡出行为添加到组
//每次更新时都会重置,因此将使用最新的
// chordPaths选项
groupG.on(mouseover,function(d){
chordPaths.classed(fade,function(p){
//返回true,如果* both *
//匹配已被鼠标悬停的组$ over
return((p.source.index!= d.index)&&(p.target.index!= d.index)) ;
});
});
//unfade用CSS处理:hover class on g#circle
//你也可以在g#circle上使用mouseout事件
更改是使用以创建从一个图到另一个图的平滑移位。对于路径形状的更改,自定义函数用于进行转换,同时保持整体形状。 More about those below.
Fourth, the exit() chain. 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 (e.g., no trips between those two neighbourhoods) in this data set -- then you have to remove that element from the visualization. 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.)
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, . 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 and , 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.
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 to create a function that will give you an object at each stage of the transition that with appropriately transitioned angle properties. 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. 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:
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 + "\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".
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. 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.
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 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. Then the initialization method will call the update method with a default data matrix. 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. 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:
/* 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 (e.g., 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. 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. 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. In both cases, the key functions are based on the .index
properties that the chord layout stored in the data objects.
/* 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>
.
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.
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', i.e., 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. 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. 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. 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 (e.g., no trips between those two neighbourhoods) in this data set -- then you have to remove that element from the visualization. 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.)
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.
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. 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. 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/
这篇关于使用D3在和弦图中更改和转换数据集的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!