简介
弦图用来展示一组实体之间的关系,通过在不同的弧线之间画出二次贝塞尔曲线,将实体之间的关系表示在一张弦图中。 如:展示五个城市人口互相之间的来源关系,比如北京有2015人来自上海,上海有2060人来自广州。
主程序
代码里面已经标注详细的步骤,这里就不再赘述了。
var data = Object.assign([
[11975, 5871, 8916, 2868],
[1951, 10048, 2060, 6171],
[8010, 16145, 8090, 8045],
[1013, 990, 940, 6907]
], {
names: ["black", "blond", "brown", "red"],
colors: ["#000000", "#ffdd89", "#957244", "#f26223"]
})
var width = 640;
var height = 640;
var dome = document.getElementById('svgAll');
var svg = d3.create("svg").attr("width", width).attr("height", height)
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;");
const outerRadius = 320 - 30;
const innerRadius = outerRadius - 20;
const { names, colors } = data;
const sum = d3.sum(data.flat());
const tickStep = d3.tickStep(0, sum, 100);
const tickStepMajor = d3.tickStep(0, sum, 20);
const formatValue = d3.formatPrefix(",.0", tickStep);
const chord = d3.chord()
.padAngle(20 / innerRadius)
.sortSubgroups(d3.descending);
//绘制器、圆弧生成器
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
//ribbon通过传入的数据source和target就可以帮我们生成一个path路径,有了路径就可以轻松的绘制弦图了
const ribbon = d3.ribbon().radius(innerRadius);
//用d3.chord处理数据
const chords = chord(data);
//添加分组
const group = svg.append("g")
.selectAll()
.data(chords.groups)
.join("g");
//外环形
group.append("path")
.attr("fill", d => colors[d.index])
.attr("d", arc)//绑定路径的属性值
.append("title")
.text(d => `${d.value.toLocaleString("en-US")} ${names[d.index]}`);
//环形刻度
const groupTick = group.append("g")
.selectAll()
.data(d => groupTicks(d, tickStep))
.join("g")
.attr("transform", d => `rotate(${d.angle * 180 / Math.PI - 90}) translate(${outerRadius},0)`);
//刻度横线
groupTick.append("line")
.attr("stroke", "currentColor")
.attr("x2", 6);
//刻度数值
groupTick
.filter(d => d.value % tickStepMajor === 0)
.append("text")
.attr("x", 8)
.attr("dy", ".35em")
.attr("transform", d => d.angle > Math.PI ? "rotate(180) translate(-16)" : null)
.attr("text-anchor", d => d.angle > Math.PI ? "end" : null)
.text(d => formatValue(d.value));
// 重点----
// 生成丝带图形
svg.append("g")
.attr("fill-opacity", 0.7)
.selectAll()
.data(chords)
.join("path")
.attr("d", ribbon)
.attr("fill", d => colors[d.target.index])
.attr("stroke", "white")
.append("title")
.text(d => `${d.source.value.toLocaleString("en-US")} ${names[d.source.index]} → ${names[d.target.index]}${d.source.index !== d.target.index ? `\n${d.target.value.toLocaleString("en-US")} ${names[d.target.index]} → ${names[d.source.index]}` : ``}`);
function groupTicks(d, step) {
const k = (d.endAngle - d.startAngle) / d.value;
return d3.range(0, d.value, step).map(value => {
return { value: value, angle: value * k + d.startAngle };
});
}
dome.appendChild(svg.node());
另外一个人口流动示例
const city_name = ["北京", "上海", "广州", "深圳", "香港"];
const population = [
[1000, 3045, 4567, 1234, 3714],
[3214, 2000, 2060, 124, 3234],
[8761, 6545, 3000, 8045, 647],
[3211, 1067, 3214, 4000, 1006],
[2146, 1034, 6745, 4764, 5000]
];
var dome = document.getElementById('svgAll');
var svg = d3.create("svg").attr("width", 600).attr("height", 600);
//创建分组
const rootG = svg
.append("g")
.attr("class", "rootG")
.attr("transform", "translate(300,300)");
const groupG = rootG.append("g").attr("class", "group");
const chordG = rootG.append("g").attr("class", "chords");
//处理数据得到适合绘图数据
const chords = d3
.chord()
.padAngle(0.1)
.sortSubgroups(d3.descending)
.sortChords(d3.descending)(population);
console.log(chords);
//构建颜色比例尺
const color = d3.scaleOrdinal(d3.schemeCategory10);
//创建绘制器
const arc = d3.arc().innerRadius(100).outerRadius(130);
console.log(chords.groups);
const ribbon = d3.ribbon().radius(100); //创建弦绘制器
//绘制
groupG
.selectAll("path")
.data(chords.groups)
.enter()
.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc);
groupG
.selectAll(".outerText")
.data(chords.groups)
.enter()
.append("text")
.each(function (d, i) {
d.angle = (d.startAngle + d.endAngle) / 2;
d.name = city_name[i];
})
.attr("class", "outerText")
.attr("dy", ".35em")
.attr("transform", function (d) {
console.log(d);
const jd = (d.angle * 180) / Math.PI;
// 字体本来是水平的,所以旋转角度需要 - 90
return (
"rotate(" +
(jd - 90) +
")" +
"translate(" +
(jd >= 180 ? 180 : 140) +
")" +
(d.angle > Math.PI ? "rotate(180)" : "")
);
})
.text(function (d) {
return d.name;
});
chordG
.selectAll("path")
.data(chords)
.enter()
.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", ribbon);
dome.appendChild(svg.node());