Directed and divided edges, using linear gradients to show direction (copied from the Thanksgiving Upshot).
Shapefiles from Statcan website. Trade data...is made up. And of course d3.js and topojson.js.
Click to zoom to an ER, or scroll zoom. Click and drag to pan around the map.
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<link href='https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css' rel='stylesheet' type='text/css'>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.9.0/d3-legend.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script>
<style>
body {
background-color: #333;
}
.map {
position: relative;
display: block;
clear: both;
}
.topbar {
position: absolute;
display: block;
clear: both;
color: #eee;
top: 50px;
right: 50px;
background: none;
text-align: center;
}
form, select, span {
/* text-align: center;*/
display: inline-block;
float: left;
}
.background {
fill: #333;
pointer-events: all;
}
.ers {
opacity: 0.85;
/* fill: #444;*/
cursor: pointer;
}
.terr, .state {
opacity: 0.85;
fill: #333;
}
.ers:hover {
opacity: 1;
}
/* The hover and mouseover areas of .ers are different. Sometimes shows name/info but doesn't activate hover class */
.ers.active {
opacity: 1;
}
path {
stroke: #fff;
stroke-width: 0.05;
stroke-linejoin: round;
stroke-linecap: round;
}
.border {
fill: none;
stroke: #fff;
stroke-linejoin: round;
stroke-linecap: round;
}
rect {
max-width: 100%;
vector-effect: non-scaling-stroke;
}
text {
cursor: default;
fill: #111;
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 4px;
margin: 0px;
padding: 1px;
}
.text {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 4px;
}
.place-label {
font-size: 3px;
fill: #FFFFFF;
}
.container {
width: 100%;
height: 0;
padding-bottom: 100%;
position: relative;
}
svg {
position: absolute;
top: 0;
left: 0;
}
.legendLinear {
font-family: 'Helvetica Neue', sans-serif;
background-color: white;
stroke-width: 0.025px;
}
.legendTitle {
font-family: 'Helvetica Neue', sans-serif;
fill: #fff;
stroke: #fff;
font-size: 4px;
stroke-width: 0.025px;
}
.swatch {
fill: #fff;
stroke: #fff;
}
.label {
fill: #fff;
stroke: #fff;
stroke-width: 0.025px;
font-size: 3px;
}
</style>
</head>
<body>
<div class="map container">
</div>
<script>
var p = {
max_line_w: 13,
line_w_exp: 1.0296361807650962,
min_opacity: 0.8, //.1502965352200607,
min_line_w: .03001779211320364,
from_color: "#0571b0", //d3.hcl(240,50,50).toString(), //"#9999FF", //"#FF9d00", //"#ff6252",
mid_color: "#f7f7f7", //d3.hcl(200,50,100).toString(), //"#FFFFFF", //"rgba(191,202,210,1)",
to_color: "#ca0020" //d3.hcl(360,50,100).toString(), //"#FF9999" //"#64a2a2"
}
var features,
centered,
active = d3.select(null);
var svg = d3.select(".map")
.append("svg")
.on("click", stopped, true);
var width = 300, //svg.node().getBoundingClientRect().width;
height = 200; //svg.node().getBoundingClientRect().height;
svg.attr({
viewBox: 0 + " " + 0 + " " + width + " " + height
});
svg.attr("preserveAspectRatio", "xMinYMin meet");
var formatNumber = d3.format(",.1f");
var proj = d3.geo.albers()
.center([9.5,52.5])
.rotate([100,0])
.parallels([52,60])
.scale(350)
.translate([width/2, height/2]);
var path = d3.geo.path()
.projection(proj);
var zoom = d3.behavior.zoom()
.translate([0, 0])
.scale(1)
.scaleExtent([1, 16.5])
.center([width/2,height/2])
.size([width, height])
.on("zoom", zoomed);
var rect = svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.on("click", reset);
var R = svg.append("defs");
var g2=svg.append("g")
.attr("transform", "translate(" + width/2 + "," + height/2 + ")");
var g=g2.append("g"); // for da map.
svg
.call(zoom) // delete this line to disable free zooming
.call(zoom.event);
d3.json("us.json", function(error, can){
if (error) return console.error(error);
features=topojson.feature(can, can.objects.nation).features;
g.selectAll(".state")
.data(features) //topojson.feature(can, can.objects.ers).features)
.enter().append("path")
.attr("class", function(d) {
return "state";
})
.attr("d", path)
.style("fill", d3.hcl("hsl(0,0%,20%)"));
// g.append("path")
// .datum(topojson.mesh(can, can.objects.provs, function(a,b) { return a!==b || a===b; }))
// .attr("d",path)
// .attr("class","border");
terr();
});
function terr() {
d3.json("terr.json", function(error, terr){
if (error) return console.error(error);
features=topojson.feature(terr, terr.objects.nation).features;
g.selectAll(".terr")
.data(features) //topojson.feature(can, can.objects.ers).features)
.enter().append("path")
.attr("class", function(d) {
return "terr";
})
.attr("d", path)
.style("fill", d3.hcl("hsl(0,0%,20%)"));
// g.append("path")
// .datum(topojson.mesh(can, can.objects.provs, function(a,b) { return a!==b || a===b; }))
// .attr("d",path)
// .attr("class","border");
ers();
});
}
function ers() {
d3.csv("ger_centroid.csv", function(error, centroids) {
if (error) return console.error(error);
d3.json("can.json", function(error, can) {
if (error) return console.error(error);
jsonReady(can, centroids);
});
});
}
function jsonReady(can, centroids) {
features=topojson.feature(can, can.objects.ers).features;
// console.log(features);
g.selectAll(".ers")
.data(features) //topojson.feature(can, can.objects.ers).features)
.enter().append("path")
.attr("class", function(d) {
return "ers";
// return parseInt(d.id,10) < 6000 ? "ers" : "terr";
})
.attr("d", path)
.style("fill", d3.hcl("hsl(0,0%,27%)"))
// .on('mouseover', function(d) {
// if (parseInt(d.id,10)<6000) {
// return erInfo(d);
// }
// })
.on('click', clicked);
g.append("path")
.datum(topojson.mesh(can,can.objects.provs, function(a,b) { return a!==b || a===b; }))
.attr("d",path)
.attr("class","border");
var nnodes = [];
for (var f in features) {
nnodes.push({
'id': features[f].id,
'name': features[f].properties.name,
'x': proj([centroids[f].POINT_X,centroids[f].POINT_Y])[0], //path.centroid(features[f])[0],
'y': proj([centroids[f].POINT_X,centroids[f].POINT_Y])[1] //path.centroid(features[f])[1]
})
}
d3.csv("flows.csv", function(error, data) {
csvReady(can, data, nnodes);
var cities = [{name: "Toronto", coordinates: [-79.658322885399997,43.737463224300001]},
{name: "Vancouver", coordinates: [-123.142493118999990,49.447738007799998]},
{name: "Montreal", coordinates: [-73.646959531700006,45.607351254900003]},
{name: "Calgary", coordinates: [-114.117189348000000,51.244804740600003]},
{name: "Halifax", coordinates: [-63.736930048600001,44.797251121899997]},
{name: "Winnipeg", coordinates: [-97.185665760399999,49.864145789299997]},
{name: "Regina", coordinates: [-105.808425783000000,50.602568483799999]} //-104.6178, 50.4500]}
]
g.append("g")
.attr("class", "bubble")
.selectAll("circle")
.data(cities) //console.log(d); }) // try to filter here.
.enter()
.append("circle")
.attr("transform", function(d, i) {
// console.log(proj(d.coordinates));
return "translate(" + proj(d.coordinates) + ")";
})
.style("pointer-events", "none")
.style("fill", "#FFFFFF")
.attr("r", 0.5);
g.selectAll(".place-label")
.data(cities) //topojson.feature(uk, uk.objects.places).features)
.enter().append("text")
.attr("class", "place-label")
.attr("transform", function(d) { return "translate(" + proj(d.coordinates) + ")"; })
.style("pointer-events", "none")
.attr("dx", ".35em")
.attr("dy", ".35em")
.text(function(d) { return d.name; });
});
}
function csvReady(can, data, nnodes) {
var eedges = [];
var max=0,
min=Number.MAX_VALUE;
for (var i in data) {
var edge=data[i];
var flow=parseFloat(edge.value,10);
// console.log(edge);
if (flow > 0 && edge.o!==edge.d) {
eedges.push({
'source': edge.o-1, // id prob needs to be a number.
'target': edge.d-1,
'value': flow,
'line': linkArc(nnodes[edge.o-1], nnodes[edge.d-1]) // could try to transition this.
});
min = Math.min(min, flow);
max = Math.max(max, flow);
}
}
var new_scale_w = d3.scale.linear().domain([min, max]).range([0, 2.5]);
var new_scale_v = d3.scale.log().domain([min, max]).range([0, 1]);
// INSERT LEGEND CODE
var linear = d3.scale.linear()
.domain([0,1])
.range(["#0571b0", "#ca0020"]);
var gx = svg.append("g")
.attr("class", "legendLinear")
.attr("transform", "translate(10, 10)");
var legendLinear = d3.legend.color()
.shapeWidth(5)
.shapeHeight(5)
.cells([0, 1])
.labels(['Origin', 'Destination'])
.orient('vertical')
.scale(linear)
.title("Trade (Millions of $)");
svg.select(".legendLinear")
.call(legendLinear);
// gx.selectAll(".swatch")
// .attr("fill-opacity", function(d, i) {
// return i/5;
// });
// gx.selectAll(".swatch")
// .attr("fill", );
gx.selectAll(".label")
// .attr("transform", "translate(12,1)");
.attr("transform", "translate(7,4)");
gx.selectAll(".legendTitle")
.attr("transform", "translate(0,10)");
// END LEGEND CODE
var links = g.selectAll(".edge")
.data(eedges);
links
.enter()
.append("path")
.attr("d", function(d) {
return d.line;
})
.style("fill", "none")
.attr("class", function(d) {
return "edge divided source"+nnodes[d.source].id+" target"+nnodes[d.target].id;
})
.style("pointer-events", "none")
.style("stroke", function(d) {
return "url(#" + mt(null, nnodes[d.source], nnodes[d.target]) + ")";
})
.style("stroke-width", function(d) {
return new_scale_w(d.value);
})
.style('stroke-opacity', function(d) {
return new_scale_v(d.value);
});
// .style('display', 'none');
}
function linkArc(source, target) {
var dx = target.x - source.x,
dy = target.y - source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + source.x + "," + source.y + "A" + dr + "," + dr + " 0 0,1 " + target.x + "," + target.y;
}
function mt(e, t, n) { // e = d.line, d.line[0]? d.line[d.line.length-1] ? might be the arcline, really.
var r = 3,
i = [ p.from_color, p.mid_color, p.to_color ].map(function(e, t) {
var n = t / (r - 1) - .5,
i = Math.pow(Math.abs(n) * 2, .7) / 2 * (n < 0 ? -1 : 1) + .5;
return {
offset: Math.round(i * 100) + "%",
color: e
};
});
var s = "grad-" + Math.round(Math.random() * 1e7); //, o = u.extent(e, function(d) {return d.x;}), a = u.extent(e, function(d) { return d.y; }), f = o[1] - o[0], l = a[1] - a[0];
return R.append("linearGradient")
.attr("id", s)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", t.x)
.attr("x2", n.x)
.attr("y1", t.y)
.attr("y2", n.y)
.selectAll("stop")
.data(i)
.enter()
.append("stop")
.attr("offset", function(e) {
return e.offset;
}).attr("stop-color", function(e) {
return e.color;
}), s;
}
function clicked(d) {
if (active.node() === this) return reset();
active.classed("active", false);
active = d3.select(this).classed("active", true);
// probably need to check the state of choro vs edges. otherwise it turns on edges.
svg.selectAll(".edge")
.attr("opacity", 0);
svg.selectAll('.source' + d.id + ', .target' + d.id)
.attr("opacity", 1);
}
function reset() {
svg.selectAll(".edge")
.attr("opacity", 1);
active.classed("active", false);
active = d3.select(null);
}
function zoomed() {
g2.style("stroke-width", 1 / d3.event.scale + "px");
g2.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
// If the drag behavior prevents the default click,
// also stop propagation so we don’t click-to-zoom.
function stopped() {
if (d3.event.defaultPrevented) d3.event.stopPropagation();
}
/*
function coordinates(point) {
var scale = zoom.scale(),
translate = zoom.translate();
return [(point[0] - translate[0]) / scale, (point[1] - translate[1]) / scale];
}
function point(coordinates) {
var scale = zoom.scale(),
translate = zoom.translate();
return [coordinates[0] * scale + translate[0] , coordinates[1] * scale + translate[1]];
}
*/
</script>
</body>
</html>
https://d3js.org/d3.v3.min.js
https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.9.0/d3-legend.js
https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js