Published
Edited
Sep 20, 2020
Importers
Insert cell
Insert cell
{
const links = [
Object({ source: 'A', target: 'B', value: 10 }),
Object({ source: 'B', target: 'C', value: 2 }),
Object({ source: 'C', target: 'A', value: 3 }),
Object({ source: 'A', target: 'C', value: 3 }),
Object({ source: 'B', target: 'D', value: 2 })
];
return drawCDD(links);
}
Insert cell
md`### Appendix: Code`
Insert cell
Insert cell
Insert cell
import { addDefaults } from '@nuuuwan/option-utils'
Insert cell
import { drawText2 } from '@nuuuwan/svg-utils'
Insert cell
function drawCDD(links, options) {
options = addDefaults(
options,
Object({
width: 500,
height: 500,
caption: 'No Label'
})
);
const innerRadius = Math.min(options.width, options.height) * 0.5 - 90;
const outerRadius = innerRadius + 10;

const rename = name =>
name.substring(name.indexOf(".") + 1, name.lastIndexOf("."));
const arc = d3
.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
const chord = d3.chordDirected().padAngle(10 / innerRadius);

const ribbon = d3
.ribbonArrow()
.radius(innerRadius - 1)
.padAngle(1 / innerRadius);

const nameSet = links.reduce(function(nameSet, d) {
nameSet.add(d.source);
nameSet.add(d.target);
return nameSet;
}, new Set());

const names = Array.from(nameSet).sort(function(a, b) {
const latLngA = COUNTRY_INFO_INDEX[a] && COUNTRY_INFO_INDEX[a].latLng[1];
const latLngB = COUNTRY_INFO_INDEX[b] && COUNTRY_INFO_INDEX[b].latLng[1];
return latLngA - latLngB;
});

options = addDefaults(
options,
Object({
nameToColor: d3.scaleOrdinal(
names,
d3.quantize(d3.interpolateRainbow, names.length)
)
})
);

const index = new Map(names.map((name, i) => [name, i]));
const matrix = Array.from(index, () => new Array(names.length).fill(0));
for (const { source, target, value } of links)
matrix[index.get(source)][index.get(target)] += value;

const svg = d3
.create("svg")
.attr("viewBox", [
-options.width / 2,
-options.height / 2,
options.width,
options.height
]);

const chords = chord(matrix);

const group = svg
.append("g")
.attr("font-size", 10)
.attr("font-family", "sans-serif")
.selectAll("g")
.data(chords.groups)
.join("g");

group
.append("path")
.attr("fill", d => options.nameToColor(names[d.index]))
.attr("d", arc);

group
.append("text")
.each(d => (d.angle = (d.startAngle + d.endAngle) / 2))
.attr("dy", "0.35em")
.attr(
"transform",
d => `
rotate(${(d.angle * 180) / Math.PI - 90})
translate(${outerRadius + 5})
${d.angle > Math.PI ? "rotate(180)" : ""}
`
)
.attr("text-anchor", d => (d.angle > Math.PI ? "end" : null))
.text(d => names[d.index]);

group.append("title").text(
d => `${names[d.index]}
${d3.sum(chords, c => (c.source.index === d.index) * c.source.value)} outgoing →
${d3.sum(
chords,
c => (c.target.index === d.index) * c.source.value
)} incoming ←`
);

svg
.append("g")
.attr("fill-opacity", 0.75)
.selectAll("path")
.data(chords)
.join("path")
.style("mix-blend-mode", "multiply")
.attr("fill", d => options.nameToColor(names[d.source.index]))
.attr("d", ribbon)
.append("title")
.text(
d =>
`${names[d.source.index]} → ${names[d.target.index]} ${d.source.value}`
);

drawText2(svg, [0, options.height * 0.45], 'Figure: ' + options.caption);

return svg.node();
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more