Published
Edited
Jun 9, 2021
3 forks
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

const {nodes, links} = sankey(data);

svg.append("g")
.attr("stroke", "#000")
.selectAll("rect")
.data(nodes)
.join("rect")
.attr("x", d => d.x0)
.attr("y", d => d.y0)
.attr("height", d => d.y1 - d.y0)
.attr("width", d => d.x1 - d.x0)
.attr("fill", color)
.attr('opacity', d =>
// Opacity depends on input/output
highlightedOutput && highlightedInput ?
highlightedOutput == d.name && highlightedInput == d.category ? 1 : 0.3 :
highlightedOutput ? highlightedOutput == d.name ? 1 : 0.3 :
highlightedInput ? highlightedInput == d.category ? 1 : 0.3 : 1)
// On mouseover of category rect, highlight group
.on('mouseover', function(el, d) {
// If rect has no sourceLinks, it is the "final" rect
if (d.sourceLinks.length == 0) {
// If final rect, we want to highlight all sources to the final rect
set(viewof highlightedInput, null);
set(viewof highlightedOutput, d.name);
} else {
// Otherwise, we want to highlight all targets
set(viewof highlightedInput, d.category);
set(viewof highlightedOutput, null);
}
})
.on('mouseout', function(el, d) {
// On mouseout, unhighight all
set(viewof highlightedInput, null);
set(viewof highlightedOutput, null);
})
.append("title")
.text(d => `${d.name}\n${format(d.value)}`);

const link = svg.append("g")
.attr("fill", "none")
.attr("stroke-opacity", 0.5)
.selectAll("g")
.data(links)
.join("g")
.style("mix-blend-mode", "multiply");

if (edgeColor === "path") {
const gradient = link.append("linearGradient")
.attr("id", d => (d.uid = DOM.uid("link")).id)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", d => d.source.x1)
.attr("x2", d => d.target.x0);

gradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", d => color(d.source));

gradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", d => color(d.target));
}

link.append("path")
.attr("d", d3.sankeyLinkHorizontal())
.attr("stroke", d => edgeColor === "none" ? "#aaa"
: edgeColor === "path" ? d.uid
: edgeColor === "input" ? color(d.source)
: color(d.target))
// Change opacity based on highlighted
.attr('opacity', d => {
if (highlightedOutput && highlightedInput) {
// If BOTH highlighted (via path highlight)
// Highlight those that match source and target
if (highlightedOutput == d.target.name &&
highlightedInput == d.source.category) { return 1 } else { return 0.3 }
} else if (highlightedOutput) {
// Otherwise, if target matches, highlight
if (highlightedOutput == d.target.name) { return 1 } else { return 0.3 }
} else if (highlightedInput) {
// Otherwise, if source matches, highlight
if (highlightedInput == d.source.category) { return 1 } else { return 0.3 }
} else { return 1 }
})
.attr("stroke-width", d => Math.max(1, d.width))
.on('mouseover', function(el, d) {
// On mouseover of path, set highlights to equal source and target
set(viewof highlightedInput, d.source.category)
set(viewof highlightedOutput, d.target.name)
})
.on('mouseout', () => {
// On mouseout, unhighlight
set(viewof highlightedInput, null)
set(viewof highlightedOutput, null)
});

link.append("title")
.text(d => `${d.source.name} → ${d.target.name}\n${format(d.value)}`);

svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("pointer-events", "none") // Remove pointer events so path hovers work
.selectAll("text")
.data(nodes)
.join("text")
.attr("x", d => d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6)
.attr("y", d => (d.y1 + d.y0) / 2)
.attr("dy", "0.35em")
.attr("text-anchor", d => d.x0 < width / 2 ? "start" : "end")
.text(d => d.name);

return svg.node();
}
Insert cell
sankey = {
const sankey = d3.sankey()
.nodeId(d => d.name)
.nodeAlign(d3[`sankey${align[0].toUpperCase()}${align.slice(1)}`])
.nodeWidth(15)
.nodePadding(10)
.extent([[1, 5], [width - 1, height - 5]]);
return ({nodes, links}) => sankey({
nodes: nodes.map(d => Object.assign({}, d)),
links: links.map(d => Object.assign({}, d))
});
}
Insert cell
format = {
const format = d3.format(",.0f");
return data.units ? d => `${format(d)} ${data.units}` : format;
}
Insert cell
color = {
const color = d3.scaleOrdinal(d3.schemeCategory10);
return d => color(d.category === undefined ? d.name : d.category);
}
Insert cell
data = {
const links = await FileAttachment("energy.csv").csv({typed: true});
const nodes = Array.from(new Set(links.flatMap(l => [l.source, l.target])), name => ({name, category: name.replace(/ .*/, "")}));
return {nodes, links, units: "TWh"};
}
Insert cell
width = 954
Insert cell
height = 600
Insert cell
d3 = require("d3@6", "d3-sankey@0.12")
Insert cell
function set(input, value) {
input.value = value;
input.dispatchEvent(new Event("input"));
}
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more