英文:
D3.js Chord Diagram: Avoid overlapping of adjacent (non-crossing) chords
问题
我正在开发一个D3.js弦图,用于可视化机场不同区域之间的人流动态。当单击区域的外弧时,只显示通往该弧的弦。
我现在正在着手解决一项设计问题,但不知道如何解决。
我已经找到了解决弦交叉问题的解决方案(使用这篇博文:https://www.visualcinnamon.com/2016/06/orientation-gradient-d3-chord-diagram),但我现在遇到的问题是相邻的弦仍然重叠:
这看起来非常难看,我非常希望避免这种情况发生,但是我不知道如何实现...这是否可能使用d3.js?(我目前正在使用v3,因为“避免弦交叉”的方法是自定义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" },
// ... 其他位置信息
];
var flows = [
{ from: 0, to: 0, quantity: 428 },
{ from: 0, to: 1, quantity: 5 },
// ... 其他流量信息
];
// 其他代码...
这是一个D3.js弦图的初始化和数据准备的示例代码。如果你有任何关于解决重叠问题的具体问题,请告诉我,我会尽力帮助你。
英文:
I am developing a D3.js chord diagram to visualize people flows between different areas of an airport. When clicking an an area's outer arch, only the chords running to that arch are shown.
I am now getting to the fine touches of it and am lacking any clues on how to approach one design problem.
Here's the photoshop-made target I am aiming for:
I did find a solution for the problem of chords crossing each other (using this excellent blog post https://www.visualcinnamon.com/2016/06/orientation-gradient-d3-chord-diagram), but the problem I am now still having, is that adjacent chords still overlap:
This just looks really hideous and I would very much like to avoid this from happening, however I've got no clue on how to achieve this... Is this possible with d3.js? (I am currently using v3, as the "avoid chords from crossing" hack is customizing d3.js v3 source files). Otherwise, where might I be able to be customize d3 to change the shape of the chords?
Edit: I want to emphasize, that I have figured out the problem of chord ORDERING, this is already correct, the chords do not cross each other, but still overlap. Here is an illustration to clarify my problem:
jsfiddle: https://jsfiddle.net/t2g3je69/
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
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;
};
<!-- language: lang-css -->
body {
background-color: #111111;
}
<!-- language: lang-html -->
<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>
<!-- end snippet -->
Thanks a ton for any hints or ideas!
答案1
得分: 1
好的,以下是您要翻译的内容:
"Okay, solved my problem. Thanks to JohanC's comment of this post: https://stackoverflow.com/questions/21813723/change-and-transition-dataset-in-chord-diagram-with-d3/21923560#21923560, it lead me towards the right direction.
My problem was solely the shape of the curves which d3's chord path generator generated. Thus I went and changed the svg path generator so that it shapes the curves to my liking. The default d3 v3 path generator uses quadratic bezier curves for the chords, where the middle control point is put at the center of the chord diagram. I changed the generator function to use cubic bezier curves instead, where the middle control points are placed between the inner ring and the center. The greater the angle between start- and endpoint, the closer the control points are to the center of the diagram, on a quadratic scale (in case anyone wishes an illustration or more elaborate explanation, feel free to comment)
Before (using d3.svg.chord()):
After (using custom generator):
Code
Important: This probably only works with 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;
}
Complete diagram: https://jsfiddle.net/nbhodfas/
英文:
Okay, solved my problem. Thanks to JohanC's comment of this post: https://stackoverflow.com/questions/21813723/change-and-transition-dataset-in-chord-diagram-with-d3/21923560#21923560, it lead me towards the right direction.
My problem was solely the shape of the curves which d3's chord path generator generated. Thus I went and changed the svg path generator so that it shapes the curves to my liking. The default d3 v3 path generator uses quadratic bezier curves for the chords, where the middle control point is put at the center of the chord diagram. I changed the generator function to use cubic bezier curves instead, where the middle control points are placed between the inner ring and the center. The greater the angle between start- and endpoint, the closer the control points are to the center of the diagram, on a quadratic scale (in case anyone wishes an illustration or more elaborate explanation, feel free to comment)
Before (using d3.svg.chord()):
After (using custom generator):
Code
Important: This probably only works with 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;
}
Complete diagram: https://jsfiddle.net/nbhodfas/
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论