Published
Edited
May 27, 2021
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// process the CSV file into a more convenient data structure
editions = {
let data = Object.assign(d3.dsvFormat(";").parse(await FileAttachment("TOCs@2.csv").text(), d3.autoType), {y: "PageCount"});

let editions = [];
for (let d of data) {
let idx = d.edition-1;
if (!editions[idx]) {
editions[idx] = {
edition: idx+1,
pages: 0,
chapters: []
}
}
d.predecessors = [];
d.successors = [];
d.edition = editions[idx];
if (idx > 0 && d.predecessorIDs) {
let predIDs = "" + d.predecessorIDs;
predIDs = predIDs.split(",").map(x => x);
for (let predID of predIDs || []) {
console.log(predID);
let pred = editions[idx-1].chapters.find(x => x.chapterID == predID);
if (pred) {
d.predecessors.push(pred);
pred.successors.push(d);
}
}
}
editions[idx].chapters.push(d);

// keep track of max page
if (d.pageTo > editions[idx].pages) {
editions[idx].pages = d.pageTo;
}
}
return editions;
}
Insert cell
Insert cell
function chapterFlow() {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("font-size", 10)
.attr("dominant-baseline", "central")
.attr("style", "background-color: #ffffff")
;

// groups for chapters and flow inbetween (for correct z-ordering)
let flow = svg.append("g")
.attr("class", "flow")
.selectAll("g.edition")
.data(editions)
.join("g")
.attr("class", "edition")
.attr("transform", d => "translate(" + x(d.edition) + ",0)")
;
let chapters = svg.append("g")
.attr("class", "chapters")
.selectAll("g.edition")
.data(editions)
.join("g")
.attr("class", "edition")
.attr("transform", d => "translate(" + x(d.edition) + ",0)")
;

chapters.append("text")
.text(d => "Edition " + d.edition + " (" + editionYears[d.edition-1] + ", " + d.pages + " pages)")
.attr("y", d => y(d.pages) - 20)
.attr("font-weight", "bold")
.attr("font-size", "16")
;
let chapter = chapters.selectAll("g.chapter")
.data(d => d.chapters)
.join("g")
.attr("class", "chapter")
.attr("transform", d => "translate(0," + y(d.edition.pages - d.pageFrom) + ")")
;

// chapter rectangles
chapter.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", editionWidth)
.attr("height", d => y(d.pageFrom) - y(d.pageTo))
.attr("fill", "#ffffff")
.attr("stroke", "#000000")
.attr("stroke-width", "0.5")
.attr("rx", "5")
.attr("ry", "5")
;

// chapter title
// add a wrapper svg around text to establish clipping
chapter.append("svg")
.attr("width", editionWidth - 8)
.append("text")
.text(d => d.pageCount > 4 ? d.title : "")
.attr("y", d => (y(d.pageFrom) - y(d.pageTo)) / 2)
.attr("x", 8)
;

// circles for new chapters
chapter.each(function(d) {
if (d.edition.edition > 1 && d.predecessors.length == 0 ) {
d3.select(this)
.append("circle")
.attr("cx", -7)
.attr("cy", d => (y(d.pageFrom) - y(d.pageTo)) / 2)
.attr("r", 2.5)
.attr("fill","#ffffff")
.attr("stroke", "#bbbbbb")
.attr("stroke-width", 2)
}
});

// bars for chapters which are not continued in next edition
chapter.each(function(d) {
if (d.edition.edition < 3 && d.successors.length == 0 ) {
d3.select(this)
.append("rect")
.attr("x", editionWidth + 3)
.attr("y", 1)
.attr("width", 2.5)
.attr("height", d => Math.max(y(d.pageFrom) - y(d.pageTo) - 2, 2))
.attr("fill","#bbbbbb")
}
});

// "flow" of chapters between one edition an the next
let iGroups = flow.selectAll("path")
.data(d => {
let data = [];
for (let c of d.chapters) {
for (let p of c.predecessors) {
data.push({chapter: c, predecessor: p});
}
}
return data;
})
.join("path")
.attr("fill", "#dddddd")
.attr("stroke", d => d.chapter.pageCount > 0 ? "#ffffff" : "none")
.attr("stroke-width", 1)
.attr("d", flowPath) // see below for function that constructs the path geometry
.sort((a,b) => minPathWidth(b) - minPathWidth(a))
;

return svg.node();
}
Insert cell
Insert cell
function minPathWidth(d) {
return Math.min(
d.predecessor.pageCount / d.predecessor.successors.length,
d.chapter.pageCount / (d.chapter.predecessors.length || 1)
)
}
Insert cell
function flowPath(d) {
let chap = d.chapter;
let pred = d.predecessor;

let pages = Math.min(chap.pageCount/chap.predecessors.length, pred.pageCount/pred.successors.length);
let predx = -editionGap;
let cpx = editionGap * 0.5;
let dcpx = -1;
let overlap = 5;

let yOffset1 = chap.predecessors.findIndex(x => x === pred) * pages;
let dy1 = (chap.pageCount - pages * chap.predecessors.length) / 2;
let y1 = y(chap.edition.pages - chap.pageFrom - pages - yOffset1 - dy1) - 0.5;
let y2 = y(chap.edition.pages - chap.pageFrom - yOffset1 - dy1 ) - 0.5;
let yOffset2 = pred.successors.findIndex(x => x === chap) * pages;
let dy2 = (pred.pageCount - pages * pred.successors.length) / 2;
let predy1 = y(pred.edition.pages - pred.pageFrom - pages - yOffset2 - dy2);
let predy2 = y(pred.edition.pages - pred.pageFrom - yOffset2 - dy2);
let path = "M " + overlap + "," + y1;
path += " C -" + (cpx+dcpx) + "," + y1 + "," + (predx+cpx-dcpx) + "," + predy1 + "," + (predx-overlap) + "," + predy1;
path += " L " + (predx-overlap) + "," + predy2;
path += " C " + (predx+cpx+dcpx) + "," + predy2 + ",-" + (cpx-dcpx) + "," + y2 + "," + overlap + "," + y2;
path += " Z";
return path;
}
Insert cell
x = d3.scaleLinear()
.domain([1,editions.length])
.range([margin.left, width - margin.right - editionWidth])
Insert cell
y = d3.scaleLinear()
.domain([d3.min(editions, e => d3.min(e.chapters, c => c.pageTo)), maxPages])
.range([height - margin.bottom, margin.top])
Insert cell
Insert cell
d3 = require("d3@6")
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