使用 D3 在和弦图中更改和转换数据集 [英] Change and transition dataset in chord diagram with D3

查看:32
本文介绍了使用 D3 在和弦图中更改和转换数据集的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用 D3 制作和弦图.

我试图做到这一点,当用户单击链接时,数据集将更改为另一个预定义的数据集.我已经看过 http://exposedata.com/tutorial/chord/latest.htmlhttp://fleetinbeing.net/d3e/chord.html,并尝试使用其中的一些元素来使其工作.

这是创建默认"图表的 JavaScript:

var dataset = "data/all_trips.json";无功宽度= 650,高度 = 600,外半径 = Math.min(width, height)/2 - 25,内半径 = 外半径 - 18;var formatPercent = d3.format("%");var arc = d3.svg.arc().innerRadius(内半径).outerRadius(outerRadius);var layout = d3.layout.chord().padding(.03).sortSubgroups(d3.descending).sortChords(d3.ascending);var 路径 = d3.svg.chord().radius(innerRadius);var svg = d3.select("#chart_placeholder").append("svg").attr("宽度", 宽度).attr("高度", 高度).append("g").attr("id", "圆圈").attr("transform", "translate(" + width/1.5 + "," + height/1.75 + ")");svg.append("圆圈").attr("r", 外半径);d3.csv("data/neighborhoods.csv", function(neighborhoods) {d3.json(数据集,函数(矩阵){//计算和弦布局.layout.matrix(矩阵);//每个街区添加一个组.var group = svg.selectAll(".group").data(layout.groups).enter().append("g").attr(班级",组").on("鼠标悬停",鼠标悬停);//添加鼠标悬停标题.group.append("title").text(function(d, i) {返回 numberWithCommas(d.value) + " 行程开始于 " +neighbors[i].name;});//添加组弧.var groupPath = group.append("path").attr("id", function(d, i) { return "group" + i; }).attr("d", 弧).style("fill", function(d, i) { return街区[i].color; });var rootGroup = d3.layout.chord().groups()[0];//从组向外辐射的文本标签.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(颜色",#fff").attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; }).attr(转换",函数(d){返回旋转("+(d.angle * 180/Math.PI - 90)+)"+" 翻译(" + (innerRadius + 26) + ")" +(d.angle > Math.PI ? "rotate(180)" : "");}).text(function(d, i) { 返回街区[i].name; });//添加和弦.var chord = svg.selectAll(".chord").data(layout.chords).enter().append("路径").attr(类",和弦").style("fill", function(d) { return街区[d.source.index].color; }).attr("d", 路径);//为每个和弦添加鼠标悬停.chord.append("title").text(function(d) {if (!(neighborhoods[d.target.index].name ===neighborhoods[d.source.index].name)) {return numberWithCommas(d.source.value) + " 从 " +neighbors[d.source.index].name + " 到 "+neighbors[d.target.index].name + "
" + 的行程numberWithCommas(d.target.value) + " 从" +街区[d.target.index].name + "到" +街区[d.source.index].name的行程;} 别的 {return numberWithCommas(d.source.value) + " 行程开始和结束于 " +街区[d.source.index].name;}});功能鼠标悬停(d,我){chord.classed(淡入淡出",函数(p){返回 p.source.index != i&&p.target.index != i;});var selectedOrigin = d.value;var selectedOriginName =neighbors[i].name;}});});

这就是我试图用新数据重新渲染图表的方法(有一个 id 为女性"的图像元素.

d3.select("#female").on("click", function () {var new_data = "data/women_trips.json";重新渲染(新数据);});功能重新渲染(数据){var layout = d3.layout.chord().padding(.03).sortSubgroups(d3.descending).矩阵(数据);//更新弧svg.selectAll(".group").data(layout.groups).过渡().duration(1500).attrTween("d", arcTween(last_chord));//更新和弦svg.select(".chord").selectAll("路径").data(layout.chords).过渡().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);函数arcTween(布局){返回函数(d,i){var i = d3.interpolate(layout.groups()[i], d);返回函数(t){返回弧(i(t));}}}功能和弦Tween(布局){返回函数(d,i){var i = d3.interpolate(layout.chords()[i], d);返回函数(t){返回 chordl(i(t));}}}

解决方案

创建和弦图

使用 d3 创建获取有关不同组之间交互的数据,并创建一组数据对象,其中包含原始数据但也分配了角度测量值.这样,它类似于饼图布局工具,但是有一些与弦布局复杂性增加相关的重要差异.

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

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

var chordLayout = d3.layout.chord()//创建布局对象.sortChords( d3.ascending )//设置一个属性.填充(0.01);//属性设置方法可以链接chordLayout.matrix(数据);//设置数据矩阵

在设置数据矩阵后,通过在和弦布局上调用 .groups() 来访问组数据对象.每个组相当于数据矩阵中的一行(即数组数组中的每个子数组).组数据对象已被分配代表圆的一部分的起始角度和结束角度值.这很像饼图,不同之处在于每个组(以及整个圆)的值是通过对整行(子数组)的值求和来计算的.组数据对象也有属性表示它们在原始索引中的索引矩阵(很重要,因为它们可能按不同的顺序排序)和它们的总值.

在设置数据矩阵后,通过在和弦布局上调用 .chords() 来访问和弦数据对象.每个和弦代表数据矩阵中的两个值,相当于两组之间的两种可能关系.例如,在@latortue09 的例子中,关系是街区之间的自行车行程,所以代表街区 A 和街区 B 之间行程的和弦代表从 A 到 B 的次数以及从 B 到 A 的次数.如果邻居 A位于数据矩阵的 a 行,而 Neighborhood B 位于 b 行,那么这些值应该位于 data[a][b]data[b][a],分别.(当然,有时您绘制的关系对它们没有这种类型的方向,在这种情况下,您的数据矩阵应该对称,这意味着这两个值应该相等.)

每个和弦数据对象都有两个属性,sourcetarget,每个属性都是它自己的数据对象.源和目标数据对象具有相同的结构和信息关于从一组到另一组的单向关系,包括组的原始索引和该关系的值,以及表示一组圆段的一部分的起点和终点角度.

源/目标命名有点混乱,因为正如我上面提到的,和弦对象代表两个组之间关系的两个方向.具有较大值的方向决定了哪个组称为source,哪个称为target.因此,如果从邻居 A 到邻居 B 有 200 次旅行,但从 B 到 A 有 500 次旅行,那么该和弦对象的 source 将代表邻居 B 的圆的一部分,并且 target 将代表邻居 A 的部分圆圈.对于组与其自身之间的关系(在此示例中,在同一街区开始和结束的行程),源对象和目标对象是相同的.

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

第二,数据可视化方面.和弦布局工具创建数据对象数组,将数据矩阵中的信息转换为圆的角度.但它没有绘制任何东西.要创建和弦图的标准 SVG 表示,您可以使用 d3 selections 创建连接到布局数据对象的数组.因为和弦图中有两种不同的布局数据对象数组,一种用于和弦,一种用于组,因此有两种不同的 d3 选择.

在最简单的情况下,两个选择都将包含 元素(并且这两种类型的路径将通过类来区分).连接到弦图组的数据数组的 成为围绕圆外的弧,而 则是与数据结合起来,和弦本身就成为整个圆圈的乐队.

的形状由它的 决定d"(路径数据或方向) 属性.D3有多种路径数据生成器,这些函数接受一个数据对象并创建一个可用于路径的 "d" 属性的字符串.每个路径生成器都是通过调用一个 d3 方法创建的,每个路径生成器都可以通过调用它自己的方法来修改.

标准和弦图中的组是使用绘制的d3.svg.arc() 路径数据生成器.这个弧生成器与饼图和圆环图使用的相同.毕竟,如果您从和弦图中删除和弦,您基本上只有一个由组弧组成的圆环图.默认的弧生成器期望传递带有 startAngleendAngle 属性的数据对象;和弦布局创建的组数据对象使用此默认设置.弧发生器还需要知道弧的内外半径.这些可以指定为数据的函数或常量;对于弦图,它们将是常数,每个弧都相同.

var arcFunction = d3.svg.arc()//创建弧路径生成器//使用默认角度访问器.innerRadius(半径).outerRadius(半径 + 带宽);//设置恒定半径值var groupPaths = d3.selectAll("path.group").data( chordLayout.groups() );//将选择加入适当的数据对象数组//从和弦布局groupPaths.enter().append("path")//如果这不是更新,则创建路径.attr("class", "group");//设置类/* 还设置与数据无关的任何其他属性 */groupPaths.attr("填充", groupColourFunction )//设置作为数据函数的属性.attr("d", arcFunction );//创建形状//d3 将每个路径的数据对象传递给 arcFunction//这将为路径d"属性创建字符串

和弦图中的和弦具有此类图独有的形状.它们的形状使用 d3.svg.chord()<定义/code> 路径数据生成器.默认的和弦生成器需要和弦布局对象创建的表单数据,唯一需要指定的是圆的半径(通常与圆弧组的内半径相同).

var chordFunction = d3.svg.chord()//创建和弦路径生成器//使用默认访问器.radius(半径);//设置恒定半径var chordPaths = d3.selectAll("path.chord").data( chordLayout.chords() );//将选择加入适当的数据对象数组//从和弦布局chordPaths.enter().append("path")//如果这不是更新,则创建路径.attr("class", "chord");//设置类/* 还设置与数据无关的任何其他属性 */chordPaths.attr("填充", chordColourFunction )//设置作为数据函数的属性.attr("d", chordFunction );//创建形状//d3 会将每个路径的数据对象传递给 chordFunction//这将为路径d"属性创建字符串

这是简单的情况,只有 元素.如果您还想拥有与您的组或和弦相关联的文本标签,那么您的数据将连接到 元素,以及 元素和标签的 元素(以及任何其他元素,如头发颜色示例中的刻度线)是继承其数据对象的子元素.更新图表时,您需要更新所有受数据影响的子组件.

更新和弦图

考虑到所有这些信息,您应该如何创建可以使用新数据更新的和弦图?

首先,为了尽量减少代码总量,我通常建议您将 update 方法作为 initialization 方法的两倍.是的,对于在更新中永远不会改变的事物,您仍然需要一些初始化步骤,但是对于实际绘制基于数据的形状,您应该只需要一个函数,无论这是更新还是新的可视化.

对于此示例,初始化步骤将包括创建 和居中的 元素,以及读取信息数组关于不同的街区.然后初始化方法将使用默认数据矩阵调用更新方法.切换到不同数据矩阵的按钮将调用相同的方法.

/*** 初始化可视化 ***/var g = d3.select("#chart_placeholder").append("svg").attr("宽度", 宽度).attr("高度", 高度).append("g").attr("id", "圆圈").attr("转换","translate(" + width/2 + "," + height/2 + ")");//整个图形将被绘制在这个<g>元素,//所以所有坐标都将相对于圆心g.append("圆圈").attr("r", 外半径);d3.csv(数据/neighborhoods.csv",函数(错误,neighborhoodData){if (error) {alert("读取文件出错:", error.statusText);返回;}邻域 = 邻域数据;//存储在其他函数可访问的变量中更新和弦(数据集);//使用默认数据集url调用更新方法});//d3.csv 函数结束/* 更新触发器示例 */d3.select("#MenOnlyButton").on("click", function() {updateChords("/data/men_trips.json");禁用按钮(这个);});

我只是将数据 url 传递给更新函数,这意味着该函数的第一行将是数据解析函数调用.结果数据矩阵用作数据布局对象的矩阵.我们需要一个新的布局对象,以便为转换功能保留旧布局的副本.(如果您不打算转换更改,您可以在同一布局上调用 matrix 方法来创建新的.)为了尽量减少代码重复,我使用一个函数来创建新的布局对象并设置其所有选项:

/* 从数据矩阵创建或更新和弦布局 */函数 updateChords( datasetURL ) {d3.json(数据集URL,函数(错误,矩阵){if (error) {alert("读取文件出错:", error.statusText);返回;}/* 计算和弦布局.*/布局 = getDefaultLayout();//创建一个新的布局对象layout.matrix(矩阵);/* 更新方法的主要部分放在这里 */});//d3.json 结束}

然后是更新或创建绘图功能的主要部分:您将需要分解所有方法链分为数据连接、进入、退出和更新四个部分.通过这种方式,您可以使用与处理可视化的原始创建相同的代码在更新期间处理创建新元素(例如,为在先前数据集中没有关系的组创建新和弦).

首先是数据连接链.一个用于组,一个用于和弦.
通过转换保持对象稳定性 -- 并减少必须设置的图形属性的数量更新时——您需要在数据连接中设置一个键函数.默认情况下,d3 仅根据它们在页面/数组中的顺序将数据与选择中的元素进行匹配.因为我们的和弦布局的 .chords() 数组不包括在此数据集中零关系的和弦,所以和弦的顺序在更新轮次之间可能不一致..groups() 数组也可以重新排序为与原始数据矩阵不匹配的顺序,因此我们还添加了一个关键函数以确保安全.在这两种情况下,关键功能都基于和弦布局存储在数据对象中的 .index 属性.

/* 创建/更新组"元素 */var groupG = g.selectAll("g.group").data(layout.groups(), 函数 (d) {返回 d.index;//使用一个键函数,以防万一//组在更新之间的排序方式不同});/* 创建/更新和弦路径 */var chordPaths = g.selectAll("path.chord").data(layout.chords(), chordKey);//指定一个键函数来匹配和弦//更新之间/* 在其他地方,chordKey 定义为:*/功能和弦键(数据){返回 (data.source.index < data.target.index) ?data.source.index + "-" + data.target.index:data.target.index + "-" + data.source.index;//创建一个代表关系的键//在这两组之间*不管*//哪个组被称为源",哪个被称为目标"}

请注意,和弦是 元素,但组是 元素,它们将包含 ;.

这一步创建的变量是数据连接选择;它们将包含与选择器匹配的所有现有元素(如果有),并且它们将包含与现有元素不匹配的任何数据值的空指针.它们还有 .enter().exit() 方法来访问这些链.

第二,回车链.对于所有不匹配元素的数据对象(如果这是第一次绘制可视化,则为所有数据对象),我们需要创建元素及其子组件.此时,您还想为所有元素(与数据无关)设置任何不变的属性,或者基于您在键函数中使用的数据值的属性,因此不会在更新时更改.

var newGroups = groupG.enter().append("g").attr("class", "group");//输入选择存储在一个变量中,所以我们可以//输入<path>、<text>和<title>元素也是//为新组创建标题工具提示newGroups.append("title");//创建圆弧路径并设置常量属性//(那些基于组索引,而不是值)newGroups.append("路径").attr("id", 函数 (d) {返回组"+ d.index;//使用 d.index 而不是 i 来保持一致性//即使组已排序}).style(填充",功能(d){返回街区[d.index].color;});//创建组标签newGroups.append("svg:text").attr("dy", ".35em").attr(颜色",#fff").text(功能(d){返回街区[d.index].name;});//创建新的和弦路径var newChords = chordPaths.enter().append("路径").attr("class", "chord");//为每个新和弦添加标题工具提示.newChords.append("title");

请注意,组弧的填充颜色是在输入时设置的,但不是和弦的填充颜色.这是因为和弦颜色将根据哪一组(和弦连接的两个组)称为源"和目标"而改变,即取决于关系的哪个方向更强(有更多行程).

第三,更新链.当您将元素附加到 .enter() 选择时,该新元素将替换原始数据连接中的空占位符选择.之后,如果您操作原始选择,设置将同时应用于新元素和更新元素.因此,您可以在此处设置依赖于数据的任何属性.

//根据数据更新(工具提示)标题文字groupG.select("标题").text(function(d, i) {返回 numberWithCommas(d.value)+ " 旅行开始于 "+ 街区[i].name;});//更新路径以匹配布局groupG.select("路径").过渡().duration(1500).attr("opacity", 0.5)//可选,只是为了观察过渡.attrTween("d", arcTween(last_layout)).transition().duration(10).attr("opacity", 1)//重置不透明度;//定位组标签以匹配布局groupG.select("文本").过渡().duration(1500).attr(转换",函数(d){d.angle = (d.startAngle + d.endAngle)/2;//将中点角度存储在数据对象中返回旋转("+(d.angle * 180/Math.PI - 90)+)"+" 翻译(" + (innerRadius + 26) + ")" +(d.angle > Math.PI ? "rotate(180)" : "rotate(0)");//包括旋转零,以便可以插入变换}).attr(文本锚",函数(d){返回 d.angle >数学.PI ?结束":开始";});//更新所有和弦标题文本chordPaths.select("title").text(function(d) {if (neighborhoods[d.target.index].name !==社区[d.source.index].name) {返回 [numberWithCommas(d.source.value),来自"的旅行,社区[d.source.index].name,"到 ",街区[d.target.index].name,"
",numberWithCommas(d.target.value),来自"的旅行,街区[d.target.index].name,"到 ",社区[d.source.index].name].加入("");//加入多个字符串的数组比//重复调用'+'运算符,//并使代码更简洁!}else {//源和目标是一样的返回 numberWithCommas(d.source.value)+ " 旅行开始和结束于 "+ 社区[d.source.index].name;}});//更新路径形状chordPaths.transition().duration(1500).attr("opacity", 0.5)//可选,只是为了观察过渡.style(填充",功能(d){返回街区[d.source.index].color;}).attrTween("d", chordTween(last_layout)).transition().duration(10).attr("opacity", 1)//重置不透明度;//将鼠标悬停/淡出行为添加到组//这在每次更新时都会重置,因此它将使用最新的//和弦路径选择groupG.on(鼠标悬停",功能(d){chordPaths.classed("fade", function (p) {//如果*既不是和弦的源也不是目标,则返回真//匹配鼠标悬停的组返回 ((p.source.index != d.index) && (p.target.index != d.index));});});//unfade"是用 CSS 处理的:g#circle 上的悬停类//您也可以使用 g#circle 上的 mouseout 事件来完成

使用 d3 transitions 完成更改以创建从一个图表到另一个图表的平滑转换.对于路径形状的更改,使用自定义函数进行过渡,同时保持整体形状.有关以下内容的更多信息.

第四,exit() 链.如果上图中的任何元素在新数据中不再匹配——例如,如果一个和弦不存在,因为没有在这个数据集中这两个组之间的关系(例如,这两个街区之间没有旅行)——那么你必须从可视化中删除该元素.您可以立即删除它们,以便它们消失以腾出空间来转换数据,或者您可以使用转换它们然后将其删除.(在转换选择上调用 .remove() 将在转换完成时删除元素.)

您可以创建自定义过渡使形状缩小为无,但我只是使用淡出到零不透明度:

//处理退出组(如果有)及其所有子组件:groupG.exit().过渡().duration(1500).attr("不透明度", 0).消除();//转换完成后移除//处理退出路径:chordPaths.exit().transition().duration(1500).attr("不透明度", 0).消除();

关于自定义补间函数:

如果您只是使用默认补间从一种路径形状切换到另一种路径形状,结果可能看起来有点奇怪.尝试从仅限男性"切换到仅限女性",您会看到和弦与圆的边缘断开连接.如果圆弧位置发生了更显着的变化,您会看到它们穿过圆圈到达新位置,而不是绕着圆环滑动.

这是因为从一种路径形状到另一种路径形状的默认过渡只是匹配路径上的点,并将每个点从一个直线过渡到另一个.它适用于任何类型的形状,无需任何额外代码,但不一定在整个过渡过程中保持该形状.

自定义补间函数可让您定义在过渡的每一步应如何塑造路径.我已经写了关于补间函数的评论这里这里,所以我不打算重复它.但简短的描述是,您传递给 .attrTween(attribute, tween) 的 tween 函数必须是每个元素调用一次的函数,并且本身必须返回一个将在每个元素调用的函数转换的滴答"以返回转换中该点的属性值.

为了获得路径形状的平滑过渡,我们使用两个路径数据生成器函数——弧发生器和弦发生器——在过渡的每一步创建路径数据.这样,弧线将始终看起来像弧线,而和弦将始终看起来像和弦.过渡的部分是开始和结束角度值.给定描述相同类型形状但具有不同角度值的两个不同数据对象,您可以使用 d3.interpolateObject(a,b) 创建一个函数,该函数将在过渡的每个阶段为您提供一个具有适当过渡角度属性的对象.因此,如果您拥有旧布局中的数据对象和新布局中匹配的数据对象,则可以将圆弧或和弦从一个位置平滑地移动到另一个位置.

但是,如果没有旧的数据对象该怎么办?要么是因为这个和弦在旧布局中没有匹配项,要么是因为这是第一次绘制可视化并且没有旧布局.如果您将一个空对象作为第一个参数传递给 d3.interpolateObject,则转换后的对象将始终是最终值.结合其他过渡,例如不透明度,这可能是可以接受的.但是,我决定进行过渡,使其从零宽度形状开始——即开始角度与结束角度匹配的形状——然后扩展到最终形状:

function chordTween(oldLayout) {//这个函数会在每个更新周期调用一次//创建旧布局的和弦数组的键:值版本//所以我们可以很容易地找到匹配的和弦//(可能没有匹配的索引)var oldChords = {};如果(旧版式){oldLayout.chords().forEach(function(chordData){oldChords[ chordKey(chordData) ] = chordData;});}返回函数 (d, i) {//每个活动和弦都会调用这个函数变量补间;var old = oldChords[chordKey(d)];如果(旧){//old 不是未定义的,即//有一个匹配的旧和弦值//检查源和目标是否已经切换:如果(d.source.index != old.source.index ){//交换源和目标以匹配新数据旧 = {来源:old.target,目标:old.source};}tween = d3.interpolate(old, d);}别的 {//创建一个零宽度和弦对象var emptyChord = {来源:{ startAngle:d.source.startAngle,结束角度:d.source.startAngle},目标:{ startAngle:d.target.startAngle,结束角度:d.target.startAngle}};tween = d3.interpolate(emptyChord, d);}返回函数 (t) {//这个函数计算中间形状返回路径(补间(t));};};}

(查看fiddle中的arc tween代码,稍微简单一些)

完整版: http://jsfiddle.net/KjrGF/12/

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

解决方案

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,
                    "
",
                    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 在和弦图中更改和转换数据集的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆