forked from mbostock's block: Chord Diagram
forked from kafunk's block: Chord Diagram d3.v5 with self-wrapping text
forked from kafunk's block: noun&noun chord diagram single city
forked from kafunk's block: noun&noun chord diagram multiple cities
xxxxxxxxxx
<head>
<meta charset="utf-8">
<style>
body { position:absolute; top:0; bottom:0; right:0; left:0; }
text { font-family:monospace; }
.chord { fill-opacity:0.4; }
#title { font-size:48px; opacity:0.6; }
#subtitle { font-size:18px; opacity:0.7; }
#subsubtitle { font-size:10px; opacity:0.8; }
/* tspan.name, tspan.location, tspan.categories { font-size:18px; } */
</style>
</head>
<body>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script>
// CITIES
let cities = ["minneapolis","slc","nyc","denver","austin","milwaukee","san fran","chicago","iowa city","nola","seattle"],
replaceTxt = cities[cities.length-1].replace(/^/,'& '),
cityStr = (cities.slice(0,cities.length-1).concat(replaceTxt)).join(", ");
// DIMENSIONS
let height = 960,
width = 960,
outerRadius = width / 2,
innerRadius = outerRadius - 148;
// SVG
let svg = d3.select("body").append("svg")
.attr("height", height)
.attr("width", width)
.append("g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")");
// WRAPPED TITLE
let titleGrp = svg.append("g");
let title = titleGrp.append("text")
.attr("id", "title")
.attr("x", -outerRadius + 36)
.attr("y", -outerRadius - 18),
subtitle = titleGrp.append("text")
.attr("id", "subtitle")
.attr("x", -outerRadius + 36)
.attr("y", -outerRadius + 52),
subsubtitle = titleGrp.append("text")
.attr("id", "subsubtitle")
.attr("x", -outerRadius + 36)
.attr("y", -outerRadius + 96);
wrap("noun & noun", (width/2), title);
wrap("business titles pulled from Yelp data", (width/4), subtitle);
wrap(`for ${cityStr}`, (width/6), subsubtitle);
// STATIONARY TOOLTIP
let featured = svg.append("rect")
.attr("x", width/4)
.attr("y", -outerRadius)
.attr("width", width/4)
.attr("height", height/4)
.attr("fill", "none")
// FUNCTION EXPRESSIONS
const fill = d3.scaleOrdinal(d3.schemeDark2);
const chord = d3.chord()
// .padAngle(.06)
.sortSubgroups(d3.descending)
.sortChords(d3.descending);
const ribbon = d3.ribbon()
.radius(innerRadius)
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(innerRadius + 20);
// FETCH DATA
let promises = [],
jsonFiles = [];
cities.forEach(d => {
let j = d.replace(/\s/g, '').concat("_nouns");
jsonFiles.push(j);
});
jsonFiles.forEach(d => { promises.push(d3.json(`${d}.json`))});
Promise.all(promises).then(draw,logErr);
// MAIN FUNCTION
function draw(data) {
// massage data
let allData = data[0];
for (var i=0; i < data.length; i++) {
allData = allData.concat(data[i]);
}
allData = allData.filter(onlyUnique);
let allNames = allData.map(d => d.name);
let firstNouns = allNames.map(d => ({noun: d.split(" ")[0], assoc: [d.split(" ")[2]]})),
connectors = allNames.map(d => ({connector: d.split(" ")[1], assoc: [d.split(" ")[0], d.split(" ")[2]]})),
secondNouns = allNames.map(d => ({noun: d.split(" ")[2], assoc: [d.split(" ")[0]]}));
let allNouns = firstNouns.concat(secondNouns);
// alphabetical sort
allNouns.sort(function(a,b) { return a.noun > b.noun });
// setup data matrix
let indexByName = d3.map(),
nameByIndex = d3.map(),
matrix = [],
n = 0;
// Compute a unique index for each noun
allNouns.forEach(function(d) {
if (!indexByName.has(d = d.noun)) {
nameByIndex.set(n, d);
indexByName.set(d, n++);
}
});
// Construct a square matrix counting noun associations
allNouns.forEach(function(d) {
var source = indexByName.get(d.noun),
row = matrix[source];
if (!row) {
row = matrix[source] = [];
for (var i = -1; ++i < n;) row[i] = 0;
}
d.assoc.forEach(d => { row[indexByName.get(d)]++; });
});
// CHORD GROUPS => ARCS
let groups = svg.selectAll("g.group")
.data(chord(matrix).groups)
.enter().append("g")
.classed("group", true);
// Arcs
groups.append("path")
.style("fill", d => { return d3.rgb(fill(d.index)).brighter(); })
.style("stroke", "dimgray")
.attr("d", arc)
.on("mouseenter", feature())
.on("mouseout", unfeature())
// Self-wrapping arc labels
groups.append("text")
.each(d => { d.angle = (d.startAngle + d.endAngle) / 2; })
.attr("dy", ".35em")
.attr("transform", function(d) {
return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" + "translate(" + (innerRadius + 26) + ")" + (d.angle > Math.PI ? "rotate(180)" : "");
})
.style("text-anchor", d => { return d.angle > Math.PI ? "end" : null; })
.style("font-size", d=> { return d.value*0.4 +"em" })
.each(d => { wrap(nameByIndex.get(d.index), innerRadius/3, d3.select(groups.selectAll("text").nodes()[d.index])); })
// .text(d => { return nameByIndex.get(d.index) })
// CHORDS => RIBBONS
let chords = svg.selectAll("path.chord")
.data(chord(matrix))
.enter().append("path")
.classed("chord", true)
.style("stroke", d => { return d3.rgb(fill(d.source.index)); })
.style("fill", d => { return d3.rgb(fill(d.source.index)).brighter(); })
.attr("d", ribbon);
// (INNER) EVENT HANDLERS
function feature() {
return function(e,i) {
svg.selectAll("g path.chord")
.filter(function(d) {
return d.source.index != i && d.target.index != i;
})
.transition()
.style("opacity", 0.1);
let noun = nameByIndex.get(e.index),
names = allNames.filter(function(d) {
return (d.split(" ")[0] === noun || d.split(" ")[2] === noun)
});
let all = [];
names.forEach(name => { all.push(allData.filter(d => d.name === name)) });
featured.selectAll("g.business")
.data(all)
.enter().append("g")
.classed("business",true)
.append("text")
.text(d => { return d.name})
// // COMBAK
// .append("tspan")
// .attr("class", "name")
// .text(d => { return d.name})
// .append("tspan")
// .attr("class", "location")
// .text(d => { return d.location.city + ", " + d.location.state })
// .append("tspan")
// .attr("class", "categories")
// .text(d => { return d.categories })
};
}
function unfeature() {
return function(g,i) {
svg.selectAll("g path.chord")
.filter(function(d) {
return d.source.index != i && d.target.index != i;
})
.transition()
.style("opacity", 1);
// let selection = featured.selectAll("g.business")
// .data(null)
// .enter().append("g")
// .classed("business",true)
// selection.exit().remove()
};
}
} // end draw()
function onlyUnique(value,index,self) {
return self.indexOf(value) === index;
}
// WRAP TEXT
function wrap(text, width, parent) {
let words = text.split(/\s/).reverse();
if (words.length > 1) {
let word,
line = [],
lineNumber = 0,
lineHeight = 1, // ems
x = getX(parent),
y = getY(parent),
dy = 1.6,
tspan = parent.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
console.log(tspan)
tspan = parent.append("tspan").attr("x", x).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
} else {
parent.text(text)
}
function getX(parent) {
if (parent.attr("x")) {
return parent.attr("x")
} else if (parent.attr("angle")) {
return innerRadius + 26
} else {
return 0
}
}
function getY(parent) {
if (parent.attr("y")) {
return parent.attr("y")
} else if (parent.attr("angle")) {
if (d.angle > Math.PI) {
return "rotate(180)"
} else {
return -26
}
} else {
return ""
}
}
}
function logErr(error) {
console.log(error)
}
d3.select(self.frameElement).style("height", outerRadius * 2 + "px");
</script>
</body>
https://d3js.org/d3.v5.min.js
https://d3js.org/d3-scale-chromatic.v1.min.js