Published
Edited
May 9, 2021
1 star
Insert cell
Insert cell
Insert cell
csv = `source,target,value
A,B,50
A,C,50
C,D,250
E,C,200`
Insert cell
svg = {
const svg = d3.select(DOM.svg(width, height));

const defs = svg.append("defs");

// Add definitions for all of the linear gradients.
const gradients = defs
.selectAll("linearGradient")
.data(graph.links)
.join("linearGradient")
.attr("id", d => d.gradient.id);
gradients
.append("stop")
.attr("offset", 0.0)
.attr("stop-color", d => d.source.color);
gradients
.append("stop")
.attr("offset", 1.0)
.attr("stop-color", d => d.target.color);

// Add a g.view for holding the sankey diagram.
const view = svg
.append("g")
.classed("view", true)
.attr("transform", `translate(${margin}, ${margin})`);

// Define the nodes.
const nodes = view
.selectAll("rect.node")
.data(graph.nodes)
.join("rect")
.classed("node", true)
.attr("id", d => `node-${d.index}`)
.attr("x", d => d.x0)
.attr("y", d => d.y0)
.attr("width", d => d.x1 - d.x0)
.attr("height", d => Math.max(1, d.y1 - d.y0))
.attr("fill", d => d.color)
.attr("opacity", 0.9);

// Add titles for node hover effects.
nodes.append("title").text(d => `${d.name}\n${format(d.value)}`);

// Add text labels.
view
.selectAll("text.node")
.data(graph.nodes)
.join("text")
.classed("node", true)
.attr("x", d => d.x1)
.attr("dx", 6)
.attr("y", d => (d.y1 + d.y0) / 2)
.attr("dy", "0.35em")
.attr("fill", "black")
.attr("text-anchor", "start")
.attr("font-size", 10)
.attr("font-family", "Arial, sans-serif")
.text(d => d.name)
.filter(d => d.x1 > width / 2)
.attr("x", d => d.x0)
.attr("dx", -6)
.attr("text-anchor", "end");

// Define the gray links.
const links = view
.selectAll("path.link")
.data(graph.links)
.join("path")
.classed("link", true)
.attr("d", sankey.sankeyLinkHorizontal())
.attr("stroke", "black")
.attr("stroke-opacity", 0.1)
.attr("stroke-width", d => Math.max(1, d.width))
.attr("fill", "none");

// Add <title> hover effect on links.
links
.append("title")
.text(
d => `${d.source.name} ${arrow} ${d.target.name}\n${format(d.value)}`
);

// Define the default dash behavior for colored gradients.
function setDash(link) {
let el = view.select(`#${link.path.id}`);
let length = el.node().getTotalLength();
el.attr("stroke-dasharray", `${length} ${length}`).attr(
"stroke-dashoffset",
length
);
}

const gradientLinks = view
.selectAll("path.gradient-link")
.data(graph.links)
.join("path")
.classed("gradient-link", true)
.attr("id", d => d.path.id)
.attr("d", sankey.sankeyLinkHorizontal())
.attr("stroke", d => d.gradient)
.attr("stroke-opacity", 0)
.attr("stroke-width", d => Math.max(1, d.width))
.attr("fill", "none")
.each(setDash);

function branchAnimate(node) {
let links = view.selectAll("path.gradient-link").filter(link => {
return node.sourceLinks.indexOf(link) !== -1;
});
let nextNodes = [];
links.each(link => {
nextNodes.push(link.target);
});
links
.attr("stroke-opacity", 0.5)
.transition()
.duration(duration)
.ease(d3.easeLinear)
.attr("stroke-dashoffset", 0)
.on("end", () => {
nextNodes.forEach(node => {
branchAnimate(node);
});
});
}

function branchClear() {
gradientLinks.transition();
gradientLinks.attr("stroke-opactiy", 0).each(setDash);
}

nodes.on("mouseover", branchAnimate).on("mouseout", branchClear);

return svg.node();
}
Insert cell
graph = {
const graph = layout(energy2);

graph.nodes.forEach(node => {
node.color = color(node.index / graph.nodes.length);
});

graph.links.forEach(link => {
link.gradient = DOM.uid("gradient");
link.path = DOM.uid("path");
});

return graph;
}
Insert cell
layout = sankey.sankey()
.size(size)
.nodePadding(nodePadding)
.nodeWidth(nodeWidth)
Insert cell
format = (value) => {
let f = d3.format(",.0f");
return f(value) + " TWh";
}
Insert cell
color = (value) => {
return d3.interpolateRainbow(value);
}
Insert cell
Insert cell
csvData = d3.csvParse(await csv, d3.autoType)
Insert cell
// Recreate same object as energy object used in the original notebook (ugly code sorry)
energy2 = {
var a = {};
for (var i = 0; i < csvData.length; i++) {
a[csvData[i].source] = 1;
a[csvData[i].target] = 1;
}
var nodes = [];
var listNodes = Object.keys(a);
for (var i = 0; i < listNodes.length; i++) {
nodes.push({ name: listNodes[i] });
}
var links = [];
for (var i = 0; i < csvData.length; i++) {
links.push({
source: listNodes.indexOf(csvData[i].source),
target: listNodes.indexOf(csvData[i].target),
value: csvData[i].value
});
}

return { nodes, links };
}
Insert cell
size = [width - 2 * margin, height - 2 * margin]
Insert cell
height = 600
Insert cell
margin = 10
Insert cell
nodeWidth = 20
Insert cell
nodePadding = 10
Insert cell
duration = 250
Insert cell
arrow = "\u2192"
Insert cell
d3 = require("d3@5.9")
Insert cell
sankey = require("d3-sankey@0.12")
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