chart2 = {
const {
sankey,
sankeyLinkHorizontal,
create,
rollup,
InternMap,
scaleOrdinal
} = d3;
const width = 928;
const height = 720;
const classes = ["1", "2", "3"];
const totalPeople = tested1.length;
const counts = new Map(classes.map(c => [c, 0]));
const fares = new Map(classes.map(c => [c, 0]));
tested1.forEach(d => {
const cls = String(d.Pclass);
counts.set(cls, counts.get(cls) + 1);
fares .set(cls, fares .get(cls) + (d.Fare || 0));
});
const nodes = [];
const indexByName = new InternMap([], JSON.stringify);
function addNode(name) {
indexByName.set(name, nodes.length);
nodes.push({ name });
}
addNode("All Passengers");
classes.forEach(c => addNode(`Class ${c}`));
classes.forEach(c => addNode(`Fare: Class ${c}`));
// Now connect them: first link everyone to their class,
// then link each class to its total fare bucket
const links = [];
classes.forEach(c => {
links.push({
source: indexByName.get("All Passengers"),
target: indexByName.get(`Class ${c}`),
value: counts.get(c)
});
});
classes.forEach(c => {
links.push({
source: indexByName.get(`Class ${c}`),
target: indexByName.get(`Fare: Class ${c}`),
value: fares.get(c)
});
});
// Run the Sankey layout—this calculates x0/x1/y0/y1 for nodes & links
const sankeyGen = sankey()
.nodeWidth(10) // how thick each node rectangle is
.nodePadding(20) // vertical spacing between nodes
.extent([[1,1], [width-1, height-1]]);
const { nodes: layoutNodes, links: layoutLinks } = sankeyGen({
nodes: nodes.map(d => ({ ...d })),
links: links.map(d => ({ ...d }))
});
// Pick some colors just for our three classes
const classColor = scaleOrdinal()
.domain(classes.map(c => `Class ${c}`))
.range(["steelblue", "indianred", "seagreen"]);
// Decide node fill: gray for the “All Passengers” root,
// colored otherwise by matching class number
function nodeColor(d) {
if (d.name === "All Passengers") return "#ccc";
const m = d.name.match(/\d/)[0];
return classColor(`Class ${m}`);
}
// Color each link by whichever end is “thicker”
function linkColor(d) {
const cls = d.source.name === "All Passengers"
? d.target.name.match(/\d/)[0]
: d.source.name.match(/\d/)[0];
return classColor(`Class ${cls}`);
}
// Create our SVG element and set a basic font
const svg = create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.style("font", "10px sans-serif");
// Draw the flow paths first (so they sit behind the nodes)
svg.append("g")
.attr("fill", "none")
.selectAll("path")
.data(layoutLinks)
.join("path")
.attr("d", sankeyLinkHorizontal())
.attr("stroke", linkColor)
.attr("stroke-width", d => d.width)
.style("mix-blend-mode", "multiply")
.append("title")
.text(d => `${d.source.name} → ${d.target.name}\n${d.value.toLocaleString()}`);
// Now draw the node rectangles on top
svg.append("g")
.selectAll("rect")
.data(layoutNodes)
.join("rect")
.attr("x", d => d.x0)
.attr("y", d => d.y0)
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0)
.attr("fill", nodeColor)
.attr("stroke", "#000")
.append("title")
.text(d => `${d.name}\n${d.value.toLocaleString()}`);
// Finally, label everything with counts or fare sums
svg.append("g")
.selectAll("text")
.data(layoutNodes)
.join("text")
.attr("x", d => d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6)
.attr("y", d => (d.y0 + d.y1) / 2)
.attr("dy", "0.35em")
.attr("text-anchor", d => d.x0 < width / 2 ? "start" : "end")
.text(d => {
if (d.name === "All Passengers") {
return `Total People: ${totalPeople}`;
}
if (/^Class \d$/.test(d.name)) {
const cls = d.name.match(/\d/)[0];
return `Class ${cls}: ${counts.get(cls)}`;
}
const cls = d.name.match(/\d/)[0];
return `Fare: Class ${cls}: $${fares.get(cls).toLocaleString(undefined,{minimumFractionDigits:2})}`;
});
// Return our finished SVG so it can be attached to the page
return svg.node();
}