D3.js 和弦图:避免相邻(非交叉)和弦重叠

D3.js Chord Diagram: Avoid overlapping of adjacent (non-crossing) chords

我正在开发一个 D3.js 和弦图来可视化机场不同区域之间的人流。单击区域的外拱时,仅显示该拱的弦运行。 我现在正在精打细算,但对如何解决一个设计问题缺乏任何线索。

这是我要用 Photoshop 制作的目标:

我确实找到了解决和弦相互交叉问题的方法(使用这个优秀的博客post https://www.visualcinnamon.com/2016/06/orientation-gradient-d3-chord-diagram),但我现在仍然遇到的问题是相邻的和弦仍然重叠:

这看起来真的很可怕,我非常想避免这种情况发生,但是我不知道如何实现这一点……d3.js 可以吗? (我目前使用的是 v3,因为 "avoid chords from crossing" hack 正在自定义 d3.js v3 源文件)。否则,我在哪里可以自定义 d3 来改变和弦的形状?

编辑:我想强调一下,我已经弄清楚了和弦排序的问题,这已经是正确的,和弦没有相互交叉,但仍然重叠。这是一个说明我的问题的插图:

jsfiddle:https://jsfiddle.net/t2g3je69/

var locations = [
  { id: 0, name: "Gate A", color: "#12B32D" },
  { id: 1, name: "Gate B", color: "#0D8020" },
  { id: 2, name: "Gate D", color: "#095916" },
  { id: 3, name: "Gate E", color: "#064010" },
  { id: 4, name: "Check-in 1", color: "#F4CF11" },
  { id: 5, name: "Check-in 2", color: "#B3970C" },
  { id: 6, name: "Check-in 3", color: "#665607" },
  { id: 7, name: "Airside Center", color: "#0D6180" },
  { id: 8, name: "Airport Shopping", color: "#16A2D5" },
  { id: 9, name: "P1", color: "#01FAF1" },
  { id: 10, name: "P2", color: "#14CCCC" },
  { id: 11, name: "P3", color: "#0F9999" },
  { id: 12, name: "P4", color: "#0C8080" },
  { id: 13, name: "P5", color: "#074D4D" },
  { id: 14, name: "Rail", color: "#F27900" },
  { id: 15, name: "Bus/Tram", color: "#EF4F00" }
];

var flows = [
  { from: 0, to: 0, quantity: 428 },
  { from: 0, to: 1, quantity: 5 },
  { from: 0, to: 2, quantity: 2 },
  { from: 0, to: 3, quantity: 10 },
  { from: 0, to: 4, quantity: 1 },
  { from: 0, to: 5, quantity: 8 },
  { from: 0, to: 6, quantity: 0 },
  { from: 0, to: 7, quantity: 86 },
  { from: 0, to: 8, quantity: 318 },
  { from: 0, to: 9, quantity: 30 },
  { from: 0, to: 10, quantity: 23 },
  { from: 0, to: 11, quantity: 67 },
  { from: 0, to: 12, quantity: 101 },
  { from: 0, to: 13, quantity: 10 },
  { from: 0, to: 14, quantity: 270 },
  { from: 0, to: 15, quantity: 120 },
  { from: 1, to: 0, quantity: 0 },
  { from: 1, to: 1, quantity: 128 },
  { from: 1, to: 2, quantity: 40 },
  { from: 1, to: 3, quantity: 10 },
  { from: 1, to: 4, quantity: 0 },
  { from: 1, to: 5, quantity: 30 },
  { from: 1, to: 6, quantity: 10 },
  { from: 1, to: 7, quantity: 78 },
  { from: 1, to: 8, quantity: 172 },
  { from: 1, to: 9, quantity: 90 },
  { from: 1, to: 10, quantity: 2 },
  { from: 1, to: 11, quantity: 10 },
  { from: 1, to: 12, quantity: 13 },
  { from: 1, to: 13, quantity: 56 },
  { from: 1, to: 14, quantity: 134 },
  { from: 1, to: 15, quantity: 87 },
  { from: 2, to: 0, quantity: 0 },
  { from: 2, to: 1, quantity: 3 },
  { from: 2, to: 2, quantity: 97 },
  { from: 2, to: 3, quantity: 7 },
  { from: 2, to: 4, quantity: 12 },
  { from: 2, to: 5, quantity: 9 },
  { from: 2, to: 6, quantity: 3 },
  { from: 2, to: 7, quantity: 11 },
  { from: 2, to: 8, quantity: 109 },
  { from: 2, to: 9, quantity: 2 },
  { from: 2, to: 10, quantity: 3 },
  { from: 2, to: 11, quantity: 12 },
  { from: 2, to: 12, quantity: 9 },
  { from: 2, to: 13, quantity: 0 },
  { from: 2, to: 14, quantity: 76 },
  { from: 2, to: 15, quantity: 26 },
  { from: 3, to: 0, quantity: 3 },
  { from: 3, to: 1, quantity: 10 },
  { from: 3, to: 2, quantity: 9 },
  { from: 3, to: 3, quantity: 390 },
  { from: 3, to: 4, quantity: 0 },
  { from: 3, to: 5, quantity: 0 },
  { from: 3, to: 6, quantity: 12 },
  { from: 3, to: 7, quantity: 43 },
  { from: 3, to: 8, quantity: 126 },
  { from: 3, to: 9, quantity: 207 },
  { from: 3, to: 10, quantity: 23 },
  { from: 3, to: 11, quantity: 10 },
  { from: 3, to: 12, quantity: 36 },
  { from: 3, to: 13, quantity: 78 },
  { from: 3, to: 14, quantity: 532 },
  { from: 3, to: 15, quantity: 265 },
  { from: 4, to: 0, quantity: 165 },
  { from: 4, to: 1, quantity: 277 },
  { from: 4, to: 2, quantity: 80 },
  { from: 4, to: 3, quantity: 109 },
  { from: 4, to: 4, quantity: 78 },
  { from: 4, to: 5, quantity: 34 },
  { from: 4, to: 6, quantity: 10 },
  { from: 4, to: 7, quantity: 23 },
  { from: 4, to: 8, quantity: 381 },
  { from: 4, to: 9, quantity: 40 },
  { from: 4, to: 10, quantity: 35 },
  { from: 4, to: 11, quantity: 21 },
  { from: 4, to: 12, quantity: 54 },
  { from: 4, to: 13, quantity: 3 },
  { from: 4, to: 14, quantity: 38 },
  { from: 4, to: 15, quantity: 38 },
  { from: 5, to: 0, quantity: 80 },
  { from: 5, to: 1, quantity: 12 },
  { from: 5, to: 2, quantity: 5 },
  { from: 5, to: 3, quantity: 254 },
  { from: 5, to: 4, quantity: 10 },
  { from: 5, to: 5, quantity: 97 },
  { from: 5, to: 6, quantity: 22 },
  { from: 5, to: 7, quantity: 35 },
  { from: 5, to: 8, quantity: 103 },
  { from: 5, to: 9, quantity: 67 },
  { from: 5, to: 10, quantity: 12 },
  { from: 5, to: 11, quantity: 0 },
  { from: 5, to: 12, quantity: 6 },
  { from: 5, to: 13, quantity: 2 },
  { from: 5, to: 14, quantity: 10 },
  { from: 5, to: 15, quantity: 8 },
  { from: 6, to: 0, quantity: 12 },
  { from: 6, to: 1, quantity: 220 },
  { from: 6, to: 2, quantity: 70 },
  { from: 6, to: 3, quantity: 0 },
  { from: 6, to: 4, quantity: 12 },
  { from: 6, to: 5, quantity: 8 },
  { from: 6, to: 6, quantity: 238 },
  { from: 6, to: 7, quantity: 12 },
  { from: 6, to: 8, quantity: 3 },
  { from: 6, to: 9, quantity: 30 },
  { from: 6, to: 10, quantity: 10 },
  { from: 6, to: 11, quantity: 38 },
  { from: 6, to: 12, quantity: 8 },
  { from: 6, to: 13, quantity: 12 },
  { from: 6, to: 14, quantity: 20 },
  { from: 6, to: 15, quantity: 7 },
  { from: 7, to: 0, quantity: 87 },
  { from: 7, to: 1, quantity: 20 },
  { from: 7, to: 2, quantity: 123 },
  { from: 7, to: 3, quantity: 143 },
  { from: 7, to: 4, quantity: 9 },
  { from: 7, to: 5, quantity: 2 },
  { from: 7, to: 6, quantity: 0 },
  { from: 7, to: 7, quantity: 457 },
  { from: 7, to: 8, quantity: 30 },
  { from: 7, to: 9, quantity: 10 },
  { from: 7, to: 10, quantity: 32 },
  { from: 7, to: 11, quantity: 19 },
  { from: 7, to: 12, quantity: 3 },
  { from: 7, to: 13, quantity: 4 },
  { from: 7, to: 14, quantity: 73 },
  { from: 7, to: 15, quantity: 25 },
  { from: 8, to: 0, quantity: 120 },
  { from: 8, to: 1, quantity: 38 },
  { from: 8, to: 2, quantity: 96 },
  { from: 8, to: 3, quantity: 167 },
  { from: 8, to: 4, quantity: 3 },
  { from: 8, to: 5, quantity: 23 },
  { from: 8, to: 6, quantity: 9 },
  { from: 8, to: 7, quantity: 47 },
  { from: 8, to: 8, quantity: 97 },
  { from: 8, to: 9, quantity: 123 },
  { from: 8, to: 10, quantity: 86 },
  { from: 8, to: 11, quantity: 90 },
  { from: 8, to: 12, quantity: 34 },
  { from: 8, to: 13, quantity: 12 },
  { from: 8, to: 14, quantity: 176 },
  { from: 8, to: 15, quantity: 192 },
  { from: 9, to: 0, quantity: 30 },
  { from: 9, to: 1, quantity: 87 },
  { from: 9, to: 2, quantity: 9 },
  { from: 9, to: 3, quantity: 123 },
  { from: 9, to: 4, quantity: 376 },
  { from: 9, to: 5, quantity: 233 },
  { from: 9, to: 6, quantity: 199 },
  { from: 9, to: 7, quantity: 43 },
  { from: 9, to: 8, quantity: 90 },
  { from: 9, to: 9, quantity: 0 },
  { from: 9, to: 10, quantity: 0 },
  { from: 9, to: 11, quantity: 0 },
  { from: 9, to: 12, quantity: 4 },
  { from: 9, to: 13, quantity: 0 },
  { from: 9, to: 14, quantity: 10 },
  { from: 9, to: 15, quantity: 2 },
  { from: 10, to: 0, quantity: 23 },
  { from: 10, to: 1, quantity: 1 },
  { from: 10, to: 2, quantity: 9 },
  { from: 10, to: 3, quantity: 6 },
  { from: 10, to: 4, quantity: 197 },
  { from: 10, to: 5, quantity: 201 },
  { from: 10, to: 6, quantity: 66 },
  { from: 10, to: 7, quantity: 7 },
  { from: 10, to: 8, quantity: 143 },
  { from: 10, to: 9, quantity: 2 },
  { from: 10, to: 10, quantity: 0 },
  { from: 10, to: 11, quantity: 0 },
  { from: 10, to: 12, quantity: 1 },
  { from: 10, to: 13, quantity: 0 },
  { from: 10, to: 14, quantity: 2 },
  { from: 10, to: 15, quantity: 18 },
  { from: 11, to: 0, quantity: 0 },
  { from: 11, to: 1, quantity: 2 },
  { from: 11, to: 2, quantity: 0 },
  { from: 11, to: 3, quantity: 4 },
  { from: 11, to: 4, quantity: 67 },
  { from: 11, to: 5, quantity: 23 },
  { from: 11, to: 6, quantity: 221 },
  { from: 11, to: 7, quantity: 12 },
  { from: 11, to: 8, quantity: 4 },
  { from: 11, to: 9, quantity: 10 },
  { from: 11, to: 10, quantity: 0 },
  { from: 11, to: 11, quantity: 0 },
  { from: 11, to: 12, quantity: 0 },
  { from: 11, to: 13, quantity: 0 },
  { from: 11, to: 14, quantity: 3 },
  { from: 11, to: 15, quantity: 0 },
  { from: 12, to: 0, quantity: 2 },
  { from: 12, to: 1, quantity: 16 },
  { from: 12, to: 2, quantity: 10 },
  { from: 12, to: 3, quantity: 8 },
  { from: 12, to: 4, quantity: 412 },
  { from: 12, to: 5, quantity: 321 },
  { from: 12, to: 6, quantity: 100 },
  { from: 12, to: 7, quantity: 54 },
  { from: 12, to: 8, quantity: 89 },
  { from: 12, to: 9, quantity: 0 },
  { from: 12, to: 10, quantity: 2 },
  { from: 12, to: 11, quantity: 4 },
  { from: 12, to: 12, quantity: 0 },
  { from: 12, to: 13, quantity: 0 },
  { from: 12, to: 14, quantity: 0 },
  { from: 12, to: 15, quantity: 0 },
  { from: 13, to: 0, quantity: 0 },
  { from: 13, to: 1, quantity: 3 },
  { from: 13, to: 2, quantity: 30 },
  { from: 13, to: 3, quantity: 2 },
  { from: 13, to: 4, quantity: 80 },
  { from: 13, to: 5, quantity: 83 },
  { from: 13, to: 6, quantity: 20 },
  { from: 13, to: 7, quantity: 10 },
  { from: 13, to: 8, quantity: 0 },
  { from: 13, to: 9, quantity: 0 },
  { from: 13, to: 10, quantity: 0 },
  { from: 13, to: 11, quantity: 0 },
  { from: 13, to: 12, quantity: 1 },
  { from: 13, to: 13, quantity: 4 },
  { from: 13, to: 14, quantity: 10 },
  { from: 13, to: 15, quantity: 32 },
  { from: 14, to: 0, quantity: 30 },
  { from: 14, to: 1, quantity: 45 },
  { from: 14, to: 2, quantity: 10 },
  { from: 14, to: 3, quantity: 2 },
  { from: 14, to: 4, quantity: 486 },
  { from: 14, to: 5, quantity: 512 },
  { from: 14, to: 6, quantity: 89 },
  { from: 14, to: 7, quantity: 10 },
  { from: 14, to: 8, quantity: 188 },
  { from: 14, to: 9, quantity: 12 },
  { from: 14, to: 10, quantity: 8 },
  { from: 14, to: 11, quantity: 0 },
  { from: 14, to: 12, quantity: 4 },
  { from: 14, to: 13, quantity: 22 },
  { from: 14, to: 14, quantity: 12 },
  { from: 14, to: 15, quantity: 287 },
  { from: 15, to: 0, quantity: 30 },
  { from: 15, to: 1, quantity: 2 },
  { from: 15, to: 2, quantity: 8 },
  { from: 15, to: 3, quantity: 0 },
  { from: 15, to: 4, quantity: 275 },
  { from: 15, to: 5, quantity: 100 },
  { from: 15, to: 6, quantity: 45 },
  { from: 15, to: 7, quantity: 8 },
  { from: 15, to: 8, quantity: 87 },
  { from: 15, to: 9, quantity: 2 },
  { from: 15, to: 10, quantity: 0 },
  { from: 15, to: 11, quantity: 0 },
  { from: 15, to: 12, quantity: 8 },
  { from: 15, to: 13, quantity: 2 },
  { from: 15, to: 14, quantity: 310 },
  { from: 15, to: 15, quantity: 54 }
];

var totalCount = 0;
var matrix = [];

//Map list of data to matrix
flows.forEach(function(flow) {
  if (!matrix[flow.from]) {
    matrix[flow.from] = [];
  }
  matrix[flow.from][flow.to] = flow.quantity;
  totalCount += flow.quantity;
});

/*//////////////////////////////////////////////////////////
/////////////// Initiate Chord Diagram /////////////////////
//////////////////////////////////////////////////////////*/
var size = 1000;
var margin = {
  top: 50,
  right: 50,
  bottom: 50,
  left: 50
};
var width = size - margin.left - margin.right;
var height = size - margin.top - margin.bottom;
var innerRadius = Math.min(width, height) * .39;
var outerRadius = innerRadius * 1.08;
var focusedChordGroupIndex = null;

/*Initiate the SVG*/
//D3.js v3!
var svg = d3.select("#chart").append("svg:svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("svg:g")
  .attr("transform", "translate(" + (margin.left + width / 2) + "," + (margin.top + height / 2) + ")");

var chord = customChordLayout() //Using custom chord layout to order chords by adjacency so that they don't cross.
  .padding(0.02)
  .sortChords(d3.ascending) /*which chord should be shown on top when chords cross. Now the biggest chord is at the top*/
  .matrix(matrix);

/*//////////////////////////////////////////////////////////
////////////////// Draw outer Arcs /////////////////////////
//////////////////////////////////////////////////////////*/
var arc = d3.svg.arc()
  .innerRadius(innerRadius)
  .outerRadius(outerRadius);

var g = svg.selectAll("g.group")
  .data(chord.groups)
  .enter().append("svg:g")
  .attr("class", function(d) {
    return "group " + locations[d.index].id;
  });

g.append("svg:path")
  .attr("class", "arc")
  .style("stroke", function(d) {
    return d3.rgb(locations[d.index].color).brighter();
  })
  .style("fill", function(d) {
    return locations[d.index].color;
  })
  .attr("d", arc)
  .on("click", function(d) {
    highlightChords(d.index)
  });

/*//////////////////////////////////////////////////////////
////////////////// Initiate Ticks //////////////////////////
//////////////////////////////////////////////////////////*/
var ticks = svg.selectAll("g.group").append("svg:g")
  .attr("class", function(d) {
    return "ticks " + locations[d.index].id;
  })
  .selectAll("g.ticks")
  .attr("class", "ticks")
  .data(groupTicks)
  .enter().append("svg:g")
  .attr("transform", function(d) {
    return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" +
      "translate(" + outerRadius + 40 + ",0)";
  });

/*Append the tick around the arcs*/
ticks.append("svg:line")
  .attr("x1", 1)
  .attr("y1", 0)
  .attr("x2", 8)
  .attr("y2", 0)
  .attr("class", "ticks")
  .style("stroke", "#FFF")
  .style("stroke-width", "1.5px");

/*Add the labels for the %'s*/
ticks.append("svg:text")
  .attr("x", 8)
  .attr("dy", ".35em")
  .attr("class", "tickLabels")
  .style("font-size", "10px")
  .style("font-family", "sans-serif")
  .attr("fill", "#FFF")
  .attr("transform", function(d) {
    return d.angle > Math.PI ? "rotate(180)translate(-16)" : null;
  })
  .style("text-anchor", function(d) {
    return d.angle > Math.PI ? "end" : null;
  })
  .text(function(d) {
    return d.label;
  });

/*//////////////////////////////////////////////////////////
////////////////// Initiate Names //////////////////////////
//////////////////////////////////////////////////////////*/
g.append("svg:text")
  .each(function(d) {
    d.angle = (d.startAngle + d.endAngle) / 2;
  })
  .attr("dy", ".35em")
  .attr("class", "titles")
  .style("font-size", "14px")
  .style("font-family", "sans-serif")
  .attr("fill", "#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 + 55) + ")" +
      (d.angle > Math.PI ? "rotate(180)" : "");
  })
  .text(function(d, i) {
    return locations[i].name;
  });

/*//////////////////////////////////////////////////////////
//////////////// Initiate inner chords /////////////////////
//////////////////////////////////////////////////////////*/
var chords = svg.selectAll("path.chord")
  .data(chord.chords)
  .enter().append("svg:path")
  .attr("class", "chord")
  .attr("class", function(d) {
    return "chord chord-source-" + d.source.index + " chord-target-" + d.target.index;
  })
  .style("fill-opacity", "0.7")
  .style("stroke-opacity", "1")
  //Change the fill to reference the unique gradient ID
  //of the source-target combination
  .style("fill", function(d) {
    return "url(#chordGradient-" + d.source.index + "-" + d.target.index + ")";
  })
  .style("stroke", function(d) {
    return "url(#chordGradient-" + d.source.index + "-" + d.target.index + ")";
  })
  //.style("stroke", function (d) { return d3.rgb(locations[d.source.index].color).brighter(); })
  //.style("fill", function (d) { return locations[d.source.index].color; })
  .attr("d", d3.svg.chord().radius(innerRadius))
  .on("click", function() {
    showAllChords()
  });

//Cf https://www.visualcinnamon.com/2016/06/orientation-gradient-d3-chord-diagram
//Create a gradient definition for each chord
var grads = svg.append("defs").selectAll("linearGradient")
  .data(chord.chords)
  .enter().append("linearGradient")
  //Create a unique gradient id per chord: e.g. "chordGradient-0-4"
  .attr("id", function(d) {
    return "chordGradient-" + d.source.index + "-" + d.target.index;
  })
  //Instead of the object bounding box, use the entire SVG for setting locations
  //in pixel locations instead of percentages (which is more typical)
  .attr("gradientUnits", "userSpaceOnUse")
  //The full mathematical formula to find the x and y locations
  .attr("x1", function(d, i) {
    return innerRadius * Math.cos((d.source.endAngle - d.source.startAngle) / 2 +
      d.source.startAngle - Math.PI / 2);
  })
  .attr("y1", function(d, i) {
    return innerRadius * Math.sin((d.source.endAngle - d.source.startAngle) / 2 +
      d.source.startAngle - Math.PI / 2);
  })
  .attr("x2", function(d, i) {
    return innerRadius * Math.cos((d.target.endAngle - d.target.startAngle) / 2 +
      d.target.startAngle - Math.PI / 2);
  })
  .attr("y2", function(d, i) {
    return innerRadius * Math.sin((d.target.endAngle - d.target.startAngle) / 2 +
      d.target.startAngle - Math.PI / 2);
  });

//Set the starting color (at 0%)
grads.append("stop")
  .attr("offset", "0%")
  .attr("stop-color", function(d) {
    return locations[d.source.index].color;
  });

//Set the ending color (at 100%)
grads.append("stop")
  .attr("offset", "100%")
  .attr("stop-color", function(d) {
    return locations[d.target.index].color;
  });

/*//////////////////////////////////////////////////////////
////////////////// Extra Functions /////////////////////////
//////////////////////////////////////////////////////////*/

/*Returns an array of tick angles and labels, given a group*/
function groupTicks(d) {
  var anglePerPerson = (d.endAngle - d.startAngle) / d.value;
  var personsPerPercent = totalCount / 100;
  return d3.range(0, d.value, personsPerPercent).map(function(v, i) {
    return {
      angle: v * anglePerPerson + d.startAngle,
      label: i % 5 ? null : v / personsPerPercent + "%"
    };
  });
};

//Hides all chords except the chords connecting to the subgroup / location of the given index.
function highlightChords(index) {
  //If this subgroup is already highlighted, toggle all chords back on.
  if (focusedChordGroupIndex === index) {
    showAllChords();
    return;
  }

  hideAllChords();

  //Show only the ones with source or target == index
  d3.selectAll(".chord-source-" + index + ", .chord-target-" + index)
    .style("fill-opacity", "0.7")
    .style("stroke-opacity", "1");

  focusedChordGroupIndex = index;
}

function showAllChords() {
  svg.selectAll("path.chord")
    .style("fill-opacity", "0.7")
    .style("stroke-opacity", "1");

  focusedChordGroupIndex = null;
}

function hideAllChords() {
  svg.selectAll("path.chord")
    .style("fill-opacity", "0")
    .style("stroke-opacity", "0");
}


////////////////////////////////////////////////////////////
//////////// Custom Chord Layout Function //////////////////
/////// Places the Chords in the visually best order ///////
///////////////// to reduce overlap ////////////////////////
////////////////////////////////////////////////////////////
//////// Slightly adjusted by Nadieh Bremer ////////////////
//////////////// VisualCinnamon.com ////////////////////////
////////////////////////////////////////////////////////////
////// Original from the d3.layout.chord() function ////////
///////////////// from the d3.js library ///////////////////
//////////////// Created by Mike Bostock ///////////////////
////////////////////////////////////////////////////////////
function customChordLayout() {
  var ε = 1e-6,
    ε2 = ε * ε,
    π = Math.PI,
    τ = 2 * π,
    τε = τ - ε,
    halfπ = π / 2,
    d3_radians = π / 180,
    d3_degrees = 180 / π;
  var chord = {},
    chords, groups, matrix, n, padding = 0,
    sortGroups, sortSubgroups, sortChords;

  function relayout() {
    var subgroups = {},
      groupSums = [],
      groupIndex = d3.range(n),
      subgroupIndex = [],
      k, x, x0, i, j;
    var numSeq;
    chords = [];
    groups = [];
    k = 0, i = -1;

    while (++i < n) {
      x = 0, j = -1, numSeq = [];
      while (++j < n) {
        x += matrix[i][j];
      }
      groupSums.push(x);
      //////////////////////////////////////
      ////////////// New part //////////////
      //////////////////////////////////////
      for (var m = 0; m < n; m++) {
        numSeq[m] = (n + (i - 1) - m) % n;
      }
      subgroupIndex.push(numSeq);
      //////////////////////////////////////
      //////////  End new part /////////////
      //////////////////////////////////////
      k += x;
    } //while

    k = (τ - padding * n) / k;
    x = 0, i = -1;
    while (++i < n) {
      x0 = x, j = -1;
      while (++j < n) {
        var di = groupIndex[i],
          dj = subgroupIndex[di][j],
          v = matrix[di][dj],
          a0 = x,
          a1 = x += v * k;
        subgroups[di + "-" + dj] = {
          index: di,
          subindex: dj,
          startAngle: a0,
          endAngle: a1,
          value: v
        };
      } //while

      groups[di] = {
        index: di,
        startAngle: x0,
        endAngle: x,
        value: (x - x0) / k
      };
      x += padding;
    } //while

    i = -1;
    while (++i < n) {
      j = i - 1;
      while (++j < n) {
        var source = subgroups[i + "-" + j],
          target = subgroups[j + "-" + i];
        if (source.value || target.value) {
          chords.push(source.value < target.value ? {
            source: target,
            target: source
          } : {
            source: source,
            target: target
          });
        } //if
      } //while
    } //while
    if (sortChords) resort();
  } //function relayout

  function resort() {
    chords.sort(function(a, b) {
      return sortChords((a.source.value + a.target.value) / 2, (b.source.value + b.target.value) / 2);
    });
  }
  chord.matrix = function(x) {
    if (!arguments.length) return matrix;
    n = (matrix = x) && matrix.length;
    chords = groups = null;
    return chord;
  };
  chord.padding = function(x) {
    if (!arguments.length) return padding;
    padding = x;
    chords = groups = null;
    return chord;
  };
  chord.sortGroups = function(x) {
    if (!arguments.length) return sortGroups;
    sortGroups = x;
    chords = groups = null;
    return chord;
  };
  chord.sortSubgroups = function(x) {
    if (!arguments.length) return sortSubgroups;
    sortSubgroups = x;
    chords = null;
    return chord;
  };
  chord.sortChords = function(x) {
    if (!arguments.length) return sortChords;
    sortChords = x;
    if (chords) resort();
    return chord;
  };
  chord.chords = function() {
    if (!chords) relayout();
    return chords;
  };
  chord.groups = function() {
    if (!groups) relayout();
    return groups;
  };
  return chord;
};
body {
  background-color: #111111;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<html>

  <body>
    <div id="body">
      <div id="chart">
        <!--D3.js diagram goes here-->
      </div>
    </div>
  </body>

</html>

非常感谢任何提示或想法!

好的,解决了我的问题。感谢 JohanC 对此 post 的评论:Change and transition dataset in chord diagram with D3,它引导我走向正确的方向。

我的问题完全在于 d3 的和弦路径生成器生成的曲线形状。因此,我去更改了 svg 路径生成器,以便它根据我的喜好塑造曲线。默认的 d3 v3 路径生成器对和弦使用二次贝塞尔曲线,其中中间控制点位于和弦图的中心。我将生成器函数改为使用三次贝塞尔曲线,其中中间控制点位于内环和中心之间。起点和终点之间的角度越大,控制点越接近图表的中心,在二次尺度上(如果有人想要插图或更详细的解释,请随时发表评论)

之前(使用 d3.svg.chord()):

之后(使用自定义生成器):

代码 重要提示:这可能只适用于 d3.v3!

////////////////////////////////////////////////////////////
//////////// Custom Chord Path Generator ///////////////////
///////// Uses cubic bezier curves with quadratic //////////
/////// spread of control points to minimise overlap ///////
////////////////// of adjacent chords. /////////////////////
////////////////////////////////////////////////////////////
//////// Slightly adjusted by Severin Zahler ///////////////
////////////////////////////////////////////////////////////
/////// Original from the d3.svg.chord() function //////////
///////////////// from the d3.js library ///////////////////
//////////////// Created by Mike Bostock ///////////////////
////////////////////////////////////////////////////////////
function customChordPathGenerator() {
    var source = function(d) { return d.source; };
    var target = function(d) { return d.target; };
    var radius = function(d) { return d.radius; };
    var startAngle = function(d) { return d.startAngle; };
    var endAngle = function(d) { return d.endAngle; };

    function chord(d, i) {
        var s = subgroup(this, source, d, i),
            t = subgroup(this, target, d, i);

        var path = "M" + s.p0
            + arc(s.r, s.p1, s.a1 - s.a0) + (equals(s, t)
            ? curve(s.r, s.p1, s.a1, s.r, s.p0, s.a0)
            : curve(s.r, s.p1, s.a1, t.r, t.p0, t.a0)
            + arc(t.r, t.p1, t.a1 - t.a0)
            + curve(t.r, t.p1, t.a1, s.r, s.p0, s.a0))
            + "Z";

        return path;
    }

    function subgroup(self, f, d, i) {
        var subgroup = f.call(self, d, i),
            r = radius.call(self, subgroup, i),
            a0 = startAngle.call(self, subgroup, i) - (Math.PI / 2),
            a1 = endAngle.call(self, subgroup, i) - (Math.PI / 2);

        return {
            r: r,
            a0: a0,
            a1: a1,
            p0: [r * Math.cos(a0), r * Math.sin(a0)],
            p1: [r * Math.cos(a1), r * Math.sin(a1)]
        };
    }

    function equals(a, b) {
        return a.a0 == b.a0 && a.a1 == b.a1;
    }

    function arc(r, p, a) {
        return "A" + r + "," + r + " 0 " + +(a > Math.PI) + ",1 " + p;
    }

    function curve(r0, p0, a0, r1, p1, a1) {
        //////////////////////////////////////
        ////////////// New part //////////////
        //////////////////////////////////////
        var deltaAngle = Math.abs(mod((a1 - a0 + Math.PI), (2 * Math.PI)) - Math.PI);
        var radialControlPointScale = Math.pow((Math.PI - deltaAngle) / Math.PI, 2) * 0.9;
        var controlPoint1 = [p0[0] * radialControlPointScale, p0[1] * radialControlPointScale];
        var controlPoint2 = [p1[0] * radialControlPointScale, p1[1] * radialControlPointScale];
        var cubicBezierSvg = "C " + controlPoint1[0] + " " + controlPoint1[1] + ", " + controlPoint2[0] + " " + controlPoint2[1] + ", " + p1[0] + " " + p1[1];
        return cubicBezierSvg;
        //////////////////////////////////////
        //////////  End new part /////////////
        //////////////////////////////////////
    }

    function mod(a, n) {
        return (a % n + n) % n;
    }

    chord.radius = function(v) {
        if (!arguments.length) return radius;
            radius = typeof v === "function" ? v : function() { return v; };
        return chord;
    };

    chord.source = function(v) {
        if (!arguments.length) return source;
            source = typeof v === "function" ? v : function() { return v; };
        return chord;
    };

    chord.target = function(v) {
        if (!arguments.length) return target;
            target = typeof v === "function" ? v : function() { return v; };
        return chord;
    };

    chord.startAngle = function(v) {
        if (!arguments.length) return startAngle;
            startAngle = typeof v === "function" ? v : function() { return v; };
        return chord;
    };

    chord.endAngle = function(v) {
        if (!arguments.length) return endAngle;
            endAngle = typeof v === "function" ? v : function() { return v; };
        return chord;
    };

    return chord;
}

完整图表:https://jsfiddle.net/nbhodfas/