Skip to content

Instantly share code, notes, and snippets.

@tomshanley
Last active November 28, 2019 21:48
Show Gist options
  • Save tomshanley/f06d047300ee71bc2649fb133fd8098e to your computer and use it in GitHub Desktop.
Save tomshanley/f06d047300ee71bc2649fb133fd8098e to your computer and use it in GitHub Desktop.
1st sankey attempt
license: mit
{"nodes":[{"name":"Agricultural 'waste'"},{"name":"Bio-conversion"},{"name":"Liquid"},{"name":"Losses"},{"name":"Solid"},{"name":"Gas"},{"name":"Biofuel imports"},{"name":"Biomass imports"},{"name":"Coal imports"},{"name":"Coal"},{"name":"Coal reserves"},{"name":"District heating"},{"name":"Industry"},{"name":"Heating and cooling - commercial"},{"name":"Heating and cooling - homes"},{"name":"Electricity grid"},{"name":"Over generation / exports"},{"name":"H2 conversion"},{"name":"Road transport"},{"name":"Agriculture"},{"name":"Rail transport"},{"name":"Lighting & appliances - commercial"},{"name":"Lighting & appliances - homes"},{"name":"Gas imports"},{"name":"Ngas"},{"name":"Gas reserves"},{"name":"Thermal generation"},{"name":"Geothermal"},{"name":"H2"},{"name":"Hydro"},{"name":"International shipping"},{"name":"Domestic aviation"},{"name":"International aviation"},{"name":"National navigation"},{"name":"Marine algae"},{"name":"Nuclear"},{"name":"Oil imports"},{"name":"Oil"},{"name":"Oil reserves"},{"name":"Other waste"},{"name":"Pumped heat"},{"name":"Solar PV"},{"name":"Solar Thermal"},{"name":"Solar"},{"name":"Tidal"},{"name":"UK land based bioenergy"},{"name":"Wave"},{"name":"Wind"}],"links":[{"source":0,"target":1,"value":124.729},{"source":1,"target":2,"value":0.597},{"source":1,"target":3,"value":26.862},{"source":1,"target":4,"value":280.322},{"source":1,"target":5,"value":81.144},{"source":6,"target":2,"value":35},{"source":7,"target":4,"value":35},{"source":8,"target":9,"value":11.606},{"source":10,"target":9,"value":63.965},{"source":9,"target":4,"value":75.571},{"source":11,"target":12,"value":10.639},{"source":11,"target":13,"value":22.505},{"source":11,"target":14,"value":46.184},{"source":15,"target":16,"value":104.453},{"source":15,"target":14,"value":113.726},{"source":15,"target":17,"value":27.14},{"source":15,"target":12,"value":342.165},{"source":15,"target":18,"value":37.797},{"source":15,"target":19,"value":4.412},{"source":15,"target":13,"value":40.858},{"source":15,"target":3,"value":56.691},{"source":15,"target":20,"value":7.863},{"source":15,"target":21,"value":90.008},{"source":15,"target":22,"value":93.494},{"source":23,"target":24,"value":40.719},{"source":25,"target":24,"value":82.233},{"source":5,"target":13,"value":0.129},{"source":5,"target":3,"value":1.401},{"source":5,"target":26,"value":151.891},{"source":5,"target":19,"value":2.096},{"source":5,"target":12,"value":48.58},{"source":27,"target":15,"value":7.013},{"source":17,"target":28,"value":20.897},{"source":17,"target":3,"value":6.242},{"source":28,"target":18,"value":20.897},{"source":29,"target":15,"value":6.995},{"source":2,"target":12,"value":121.066},{"source":2,"target":30,"value":128.69},{"source":2,"target":18,"value":135.835},{"source":2,"target":31,"value":14.458},{"source":2,"target":32,"value":206.267},{"source":2,"target":19,"value":3.64},{"source":2,"target":33,"value":33.218},{"source":2,"target":20,"value":4.413},{"source":34,"target":1,"value":4.375},{"source":24,"target":5,"value":122.952},{"source":35,"target":26,"value":839.978},{"source":36,"target":37,"value":504.287},{"source":38,"target":37,"value":107.703},{"source":37,"target":2,"value":611.99},{"source":39,"target":4,"value":56.587},{"source":39,"target":1,"value":77.81},{"source":40,"target":14,"value":193.026},{"source":40,"target":13,"value":70.672},{"source":41,"target":15,"value":59.901},{"source":42,"target":14,"value":19.263},{"source":43,"target":42,"value":19.263},{"source":43,"target":41,"value":59.901},{"source":4,"target":19,"value":0.882},{"source":4,"target":26,"value":400.12},{"source":4,"target":12,"value":46.477},{"source":26,"target":15,"value":525.531},{"source":26,"target":3,"value":787.129},{"source":26,"target":11,"value":79.329},{"source":44,"target":15,"value":9.452},{"source":45,"target":1,"value":182.01},{"source":46,"target":15,"value":19.013},{"source":47,"target":15,"value":289.366}]}
<!DOCTYPE html>
<html>
<head>
<title>Sankey.js</title>
<meta charset="utf-8">
<meta name="description" content="">
<script src="https://d3js.org/d3.v5.js" charset="utf-8"></script>
<!-- using this link as its not needing a bundle package I think!?! -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-sankey/0.7.1/d3-sankey.js" charset="utf-8"></script>
<style>
.path:hover {stroke-opacity: .4;
}
</style>
</head>
<body>
<svg width = "960" height = "475"></svg>
<script>
//attempt at replicating the energy sankey by Mike Bostock, notes are for my benifit and may not be factually correct in terms of terminology used and explanation but its my understanding at this stage.
// select the svg defined above and pass its height and width into a constant obj
const svg = d3.select("svg");
const height = +svg.attr("height");
const width = +svg.attr("width");
const colour = d3.scaleOrdinal(d3.schemeCategory10);
// enter data using the v5 promise approach
d3.json("energy.json").then(function(energy){
// Define the sankey const, essentially saving as an object for use later. Also define the size of the sankey ("extent") to give it the parameters it must work within later on
const sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(10)
.extent([[1, 1], [width-10, height-10]]);;
// create a graph instance of sankey that takes in the "energy" data
const graph = sankey(energy);
// output the resultant sankey object to the console
console.clear();
//
// enter data phase
//
// links/paths between nodes are being generated using the sankeyLinkHorizontal due
// to using d3-sankey/0.7.1. The .link() method was changed after v0.5
//append a group to the svg object
const link = svg.append("g")
//set the group to no fill
.attr("fill","none")
//change the colour of the links to grey
.attr("stroke","#000")
//make the links see through
.attr("stroke-opacity",0.2)
//select all path items which creates a null set
.selectAll("path")
//select the data to put into the null objects
.data(graph.links)
//join the data could have writtn this ".enter().append("path")"
.join("path")
//give the path's a class of "path"
.attr("class","path")
//sankeyLinkHorizontal is creating all of the path information to feed the d attribute of the path svg using the data in our energy json the path appears to be situated at the mid point of the node link value minus the delta created by any other links into the same node.
.attr("d",d3.sankeyLinkHorizontal())
//setting the thickness of the paths, removing this attribute gives uniform lines
.attr("stroke-width", function(d) {
return Math.max(1, d.width);});
// console.log(graph.links.source.sourceLinks);
// sum the value of all nodes which are not the target of any link, therefore are at the start of the graph, and have no parents
let sumOfNodesWithoutTarget = d3.sum(graph.nodes, function(node){
return node.targetLinks.length == 0 ? 0 : node.value
})
console.log(sumOfNodesWithoutTarget)
link.append("title").text(function(d){
let proportionOfParentNode = Math.round((d.value / d.source.value) * 100) / 100
let proportionOfNodesWithoutParent = Math.round((d.value / sumOfNodesWithoutTarget) * 100) / 100
return d.source.name + " → " + d.target.name
+ " \nValue: " + d.value
+ " \nProportion of parent: " + proportionOfParentNode
+ " \nProportion of graph: " + proportionOfNodesWithoutParent
})
const rect = svg.append("g").selectAll("rect")
.data(graph.nodes)
.join("rect")
.attr("class","rect")
.attr("height", d => { return d.y1 - d.y0; })
.attr("width", d => { return d.x1 - d.x0; })
.attr("x", d =>{return d.x0})
.attr("y", d =>{return d.y0})
.attr("fill", (d,i)=>{return colour(i)})
.append("title")
.text(d => `${d.name}\n${d.value}`);
// the node text uses the same approach as the rectangles with some additional positional/sizing considerations to shift the labels on the left and right side of the svg to the opposite side of the node with spacing
const text = svg.append("g")
.selectAll("text")
.data(graph.nodes)
.enter()
.append("text")
.attr("class", "text")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("x", function(d) { return d.x0 - 6; })
.attr("y", function(d) { return (d.y1 + d.y0) / 2; })
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.text(function(d) { return d.name; })
.filter(function(d) { return d.x0 < width / 2; })
.attr("x", function(d) { return d.x1 + 6; })
.attr("text-anchor", "start");
});
</script>
</body>
.link {
fill: none;
stroke: #000;
stroke-opacity: .1;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment