Skip to content

Instantly share code, notes, and snippets.

@yellowcap
Last active December 18, 2018 16:15
Show Gist options
  • Save yellowcap/8723f81abc6357875ee83d208a3a2ac3 to your computer and use it in GitHub Desktop.
Save yellowcap/8723f81abc6357875ee83d208a3a2ac3 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<meta charset="utf-8">
<title>spatialsankey.js - sankey diagrams on a map</title>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
<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="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></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("https://gist.githubusercontent.com/yellowcap/03cd4a6c72f661377f7e/raw/c146f7d256a1d070d63e346d61164f284f8fa73a/nodes.geojson", function(nodes) {
d3.csv("https://gist.githubusercontent.com/yellowcap/03cd4a6c72f661377f7e/raw/c146f7d256a1d070d63e346d61164f284f8fa73a/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 node_data = spatialsankey.nodes();
var circs = nodelayer.selectAll("circle")
.data(node_data)
.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);
// Add labels
var y_shift = -30;
var x_shift = 0;
var text_size = 20;
var text_padding = 5;
var corner_rounding = 5;
var bg_color = 'white'
var bg_stroke = 'black'
var font_color = 'black'
var label_cx = function(d) { return node.cx(d) + x_shift; }
var label_cy = function(d) { return node.cy(d) + y_shift; }
var labels_bg_width = 100 + 2 * text_padding;
var labels_bg_height = text_size + 2 * text_padding;
var label_bg_cx = function(d) { return node.cx(d) + x_shift - labels_bg_width / 2; }
var label_bg_cy = function(d) { return node.cy(d) + y_shift - labels_bg_height / 2 - text_padding; }
// Background for labels.
var labels_bg = nodelayer.selectAll("rect")
.data(spatialsankey.nodes())
.enter()
.append("rect")
.attr("width", function (node) { return node.properties.aggregate_outflows == 0 ? 0 : labels_bg_width; }) // Only return if flow value is not zero.100)
.attr("height", function (node) { return node.properties.aggregate_outflows == 0 ? 0 : labels_bg_height; }) // Only return if flow value is not zero.100)
.attr("x", label_bg_cx)
.attr("y", label_bg_cy)
.attr("rx", corner_rounding)
.attr("ry", corner_rounding)
.style("fill", bg_color)
.style("stroke", bg_stroke);
// Labels texts showing total outflows.
var labels = nodelayer.selectAll(".text")
.data(node_data)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("x", label_cx)
.attr("y", label_cy)
.attr("font-family", "sans-serif")
.attr("font-size", text_size)
.attr("fill", font_color)
.text( function (node) { return node.properties.aggregate_outflows == 0 ? '' : node.properties.aggregate_outflows; }); // Only return if flow value is not zero.
// Adopt size of drawn objects after leaflet zoom reset
var zoomend = function(event){
// Log zoom (could be used for adaptive label sizing.)
console.log('Currently at zoom', event.target._zoom)
linklayer.selectAll("path").attr("d", spatialsankey.link());
// Update node location.
circs.attr("cx", node.cx)
.attr("cy", node.cy);
// Update label location.
labels.attr("x", label_cx)
.attr("y", label_cy);
// Update label bg location.
labels_bg.attr("x", label_bg_cx)
.attr("y", label_bg_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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment