Skip to content

Instantly share code, notes, and snippets.

@yellowcap
Last active August 4, 2025 16:25
Show Gist options
  • Select an option

  • Save yellowcap/03cd4a6c72f661377f7e to your computer and use it in GitHub Desktop.

Select an option

Save yellowcap/03cd4a6c72f661377f7e to your computer and use it in GitHub Desktop.
spatialsankey.js - visualizing flows on a leaflet map
<!DOCTYPE html>
<meta charset="utf-8">
<title>spatialsankey.js - sankey diagrams on a map</title>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI=" crossorigin="" />
<style>
body {
position: absolute;
width:100%;
height: 100%;
margin: 0px;
font-family: sans-serif;
}
#map {
top:0px;
left:0px;
right:0px;
height: 100%;
}
path {
fill: none;
stroke: #4682B4;
stroke-opacity: 0.6;
stroke-linecap: round;
cursor: pointer;
}
path:hover {
stroke-opacity: 0.8;
stroke: #315B7E;
}
.curvesettings {
position: absolute;
right: 10px;
top:6px;
}
.box {
border: 1px solid #EEE;
margin: 3px;
padding: 5px;
background-color: white;
font-family: sans-serif;
font-size: 12px;
}
.title {
font-weight: 600;
}
.source {
position: absolute;
width: 50%;
top: 6px;
left: 50px;
}
</style>
<body>
<div id="map"></div>
<form class="curvesettings">
<div class="box">
<div class="title">Curve settings</div>
<div>Hover over nodes<br> to see links.</div>
<div>Change styles using<br> radio buttons.</div>
</div>
<div class="box">
<div class="title">Curve shape</div>
<input type="radio" name="use_arcs" value="0" checked>Beziers<br>
<input type="radio" name="use_arcs" value="1">Arcs<br>
</div>
<div class="box">
<div class="title">Flip at horizontal axis</div>
<input type="radio" name="flip" value="1" checked>Flip<br>
<input type="radio" name="flip" value="0">NoFlip<br>
</div>
<div class="box">
<div class="title">Set curve steepness X</div>
<div>(bezier control point)</div>
<input type="radio" name="xshift" value="0.1">xshift 0.1<br>
<input type="radio" name="xshift" value="0.4" checked>xshift 0.4<br>
<input type="radio" name="xshift" value="0.8">xshift 0.8<br>
<input type="radio" name="xshift" value="1.6">xshift 1.6<br>
</div>
<div class="box">
<div class="title">Set curve steepness Y</div>
<div>(bezier control point)</div>
<input type="radio" name="yshift" value="0.1" checked>yshift 0.1<br>
<input type="radio" name="yshift" value="0.4">yshift 0.4<br>
<input type="radio" name="yshift" value="0.8">yshift 0.8<br>
<input type="radio" name="yshift" value="1.6">yshift 1.6<br>
</div>
</form>
<div class="source box">
<div class="title">European energy imports</div>
This graph shows how much petroleum products in thousands of tonnes are imported by each EU28 country. The <a href="http://appsso.eurostat.ec.europa.eu/nui/show.do?dataset=nrg_123a&lang=en">datasource</a> is the Eurostat database table nrg123a and for the year 2012.
</div>
</body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js" integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM=" crossorigin=""></script>
<script src="http://rawgit.com/geodesign/spatialsankey/master/spatialsankey.js"></script>
<script type="text/javascript">
// Set leaflet map
var map = new L.map('map', {
center: new L.LatLng(50,15),
zoom: 4,
layers: [
new L.tileLayer('http://{s}tile.stamen.com/toner-lite/{z}/{x}/{y}.png', {
subdomains: ['','a.','b.','c.','d.'],
attribution: 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>'
})
]
});
// Initialize the SVG layer
map._initPathRoot()
// Setup svg element to work with
var svg = d3.select("#map").select("svg"),
linklayer = svg.append("g"),
nodelayer = svg.append("g");
// Load data asynchronosuly
d3.json("nodes.geojson", function(nodes) {
d3.csv("links.csv", function(links) {
// Setup spatialsankey object
var spatialsankey = d3.spatialsankey()
.lmap(map)
.nodes(nodes.features)
.links(links);
var mouseover = function(d){
// Get link data for this node
var nodelinks = spatialsankey.links().filter(function(link){
return link.source == d.id;
});
// Add data to link layer
var beziers = linklayer.selectAll("path").data(nodelinks);
link = spatialsankey.link(options);
// Draw new links
beziers.enter()
.append("path")
.attr("d", link)
.attr('id', function(d){return d.id})
.style("stroke-width", spatialsankey.link().width());
// Remove old links
beziers.exit().remove();
// Hide inactive nodes
var circleUnderMouse = this;
circs.transition().style('opacity',function () {
return (this === circleUnderMouse) ? 0.7 : 0;
});
};
var mouseout = function(d) {
// Remove links
linklayer.selectAll("path").remove();
// Show all nodes
circs.transition().style('opacity', 0.7);
};
// Draw nodes
var node = spatialsankey.node()
var circs = nodelayer.selectAll("circle")
.data(spatialsankey.nodes())
.enter()
.append("circle")
.attr("cx", node.cx)
.attr("cy", node.cy)
.attr("r", node.r)
.style("fill", node.fill)
.attr("opacity", 0.7)
.on('mouseover', mouseover)
.on('mouseout', mouseout);
// Adopt size of drawn objects after leaflet zoom reset
var zoomend = function(){
linklayer.selectAll("path").attr("d", spatialsankey.link());
circs.attr("cx", node.cx)
.attr("cy", node.cy);
};
map.on("zoomend", zoomend);
});
});
var options = {'use_arcs': false, 'flip': false};
d3.selectAll("input").forEach(function(x){
options[x.name] = parseFloat(x.value);
})
d3.selectAll("input").on("click", function(){
options[this.name] = parseFloat(this.value);
});
</script>
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@yellowcap
Copy link
Author

I did not have time to update this project, I suspect that there were many breaking changes on D3 and friends since I built this originally. I do not have time to digg into it again at the moment, so not much I can advise to you unfortunately @gplngr

@gplngr
Copy link

gplngr commented Aug 4, 2025

No problem @yellowcap - thanks for sharing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment