|
/*jslint browser: true, devel: true */ |
|
var d3, queue, vis, params, topojson; |
|
(function () { |
|
"use strict"; |
|
vis = {}; |
|
var width, height, |
|
chart, svg, g, active, background, |
|
path, |
|
defs, style, |
|
slider, step, maxStep, running, sv, timer, |
|
button; |
|
|
|
|
|
|
|
// general design from |
|
// http://www.jeromecukier.net/blog/2013/11/20/getting-beyond-hello-world-with-d3/ |
|
vis.init = function (params) { |
|
console.log("in init, params: " + JSON.stringify(params)); |
|
vis.params = params || {}; |
|
|
|
chart = d3.select(vis.params.chart || "#chart"); // placeholder div for svg |
|
width = vis.params.width || 960; |
|
height = vis.params.height || 500; |
|
active = d3.select(null); |
|
|
|
svg = chart.selectAll("svg") |
|
.data([{width: width, height: height}]).enter() |
|
.append("svg"); |
|
svg.attr({ |
|
width: function (d) { return d.width; }, |
|
height: function (d) { return d.height; } |
|
}); |
|
|
|
background = svg.selectAll("rect.background") |
|
.data([{}]).enter() |
|
.append("rect") |
|
.classed("background", true); |
|
|
|
|
|
g = svg.selectAll("g.all") |
|
.data([{}]).enter() |
|
.append("g") |
|
.classed("all", true); |
|
|
|
// vis.init can be re-ran to pass different height/width values |
|
// to the svg. this doesn't create new svg elements. |
|
|
|
style = svg.selectAll("style") |
|
.data([{}]).enter() |
|
.append("style") |
|
.attr("type", "text/css"); |
|
// this is where we can insert style that will affect the svg directly. |
|
|
|
defs = svg.selectAll("defs").data([{}]).enter() |
|
.append("defs"); |
|
// this is used if it's necessary to define gradients, patterns etc. |
|
|
|
// the following will implement interaction around a slider and a |
|
// button. repeat/remove as needed. |
|
// note that this code won't cause errors if the corresponding elements |
|
// do not exist in the HTML. |
|
slider = d3.select(vis.params.slider || ".slider"); |
|
|
|
if (slider[0][0]) { |
|
maxStep = slider.property("max"); |
|
step = slider.property("value"); |
|
slider.on("change", function () { |
|
vis.stop(); |
|
step = this.value; |
|
vis.draw(vis.params); |
|
}); |
|
running = vis.params.running || 0; // autorunning off or manually set on |
|
} else { |
|
running = -1; // never attempt auto-running |
|
} |
|
button = d3.select(vis.params.button || ".button"); |
|
if (button[0][0] && running > -1) { |
|
button.on("click", function () { |
|
if (running) { |
|
vis.stop(); |
|
} else { |
|
vis.start(); |
|
} |
|
}); |
|
} |
|
|
|
vis.loaddata(vis.params); |
|
}; |
|
|
|
function ready(error, firs, world, wnames) { |
|
if (error) { |
|
console.error(error); |
|
} |
|
|
|
vis.firs = firs; |
|
vis.world = world; |
|
vis.wnames = wnames; |
|
if (running > 0) { |
|
vis.start(); |
|
} else { |
|
vis.draw(vis.params); |
|
} |
|
} |
|
|
|
vis.loaddata = function (params) { |
|
console.log("in loaddata, params: " + JSON.stringify(params)); |
|
if (!params) { params = {}; } |
|
|
|
// if `params.refresh` is set/true forces the browser to reload the file |
|
// and not use the cached version due to URL being different (but the filename is the same) |
|
var topo = (params.topo || "ectrl-firs.json") + (params.refresh ? ("#" + Math.random()) : ""); |
|
var world = (params.world || "world-50m.json") + (params.refresh ? ("#" + Math.random()) : ""); |
|
var names = (params.worldnames || "world-country-names.tsv") + (params.refresh ? ("#" + Math.random()) : ""); |
|
|
|
queue() |
|
.defer(d3.json, topo) |
|
.defer(d3.json, world) |
|
.defer(d3.tsv, names) |
|
.await(ready); |
|
}; |
|
|
|
vis.play = function () { |
|
if (i === maxStep && !running) { |
|
step = -1; |
|
vis.stop(); |
|
} |
|
|
|
if (i < maxStep) { |
|
step = step + 1; |
|
running = 1; |
|
d3.select(".stop").html("Pause").on("click", vis.stop(params)); |
|
slider.property("value", sv); |
|
vis.draw(params); |
|
} else { |
|
vis.stop(); |
|
} |
|
}; |
|
|
|
vis.start = function (params) { |
|
timer = setInterval(function () { vis.play(params); }, 50); |
|
}; |
|
|
|
vis.stop = function (params) { |
|
clearInterval(timer); |
|
running = 0; |
|
d3.select(".stop").html("Play").on("click", vis.start(params)); |
|
}; |
|
|
|
var zoom = d3.behavior.zoom() |
|
.scaleExtent([1, 8000]) |
|
.on("zoom", zoomed); |
|
|
|
function zoomed() { |
|
g.style("stroke-width", 1.5 / d3.event.scale + "px"); |
|
g.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 reset() { |
|
active.classed("active", false); |
|
active = d3.select(null); |
|
svg.transition() |
|
.duration(750) |
|
.call(zoom.translate([0, 0]).scale(1).event); |
|
} |
|
|
|
function clicked(d) { |
|
if (active.node() === this) { |
|
return reset(); |
|
} |
|
active.classed("active", false); |
|
active = d3.select(this).classed("active", true); |
|
|
|
var bounds = path.bounds(d), |
|
dx = bounds[1][0] - bounds[0][0], |
|
dy = bounds[1][1] - bounds[0][1], |
|
x = (bounds[0][0] + bounds[1][0]) / 2, |
|
y = (bounds[0][1] + bounds[1][1]) / 2, |
|
scale = .9 / Math.max(dx / width, dy / height), |
|
translate = [width / 2 - scale * x, height / 2 - scale * y]; |
|
|
|
svg.transition() |
|
.duration(750) |
|
.call(zoom.translate(translate).scale(scale).event); |
|
} |
|
|
|
vis.draw = function (params) { |
|
// make stuff here! |
|
console.log("in draw, params: " + JSON.stringify(params)); |
|
var pars = params || {}, |
|
scale = pars.scale || 600, |
|
cLon = pars.centerLon || 0, |
|
cLat = pars.centerLan || 55.4, |
|
projection = d3.geo.albers() |
|
.center([cLon, cLat]) |
|
.rotate([4.4, 0]) |
|
.parallels([50, 60]) |
|
.scale(scale) |
|
.translate([width / 2, height / 2]), |
|
firs = topojson.feature(vis.firs, vis.firs.objects.firs).features, |
|
|
|
tooltip = d3.select("#tooltip").classed("hidden", true), |
|
countryname = d3.select("#countryname"), |
|
graticule = d3.geo.graticule(), |
|
|
|
land = topojson.feature(vis.world, vis.world.objects.land), |
|
countries = topojson.feature(vis.world, vis.world.objects.countries).features, |
|
borders = topojson.mesh(vis.world, vis.world.objects.countries, function (a, b) { return a.id !== b.id; }), |
|
country, fir, |
|
fabs = ["BALTICFAB", |
|
"BLUMEDFAB", |
|
"DANUBEFAB", |
|
"DKSEFAB", |
|
"FABCE", |
|
"FABEC", |
|
"NEFAB", |
|
"SWFAB", |
|
"UKIRELANDFAB"]; |
|
|
|
path = d3.geo.path() |
|
.projection(projection); |
|
|
|
countries.forEach(function (d) { |
|
vis.wnames.some(function (n) { |
|
if (+d.id === +n.id) { |
|
d.name = n.name; |
|
return d.name; |
|
} |
|
}); |
|
}); |
|
|
|
|
|
svg.on("click", stopped, true); |
|
background.on("click", reset); |
|
g.style("stroke-width", "0.5px"); |
|
|
|
svg |
|
.call(zoom) // delete this line to disable free zooming |
|
.call(zoom.event); |
|
|
|
svg.on("mousemove", function () { |
|
// update tooltip position |
|
tooltip.style("top", (event.pageY + 16) + "px").style("left", (event.pageX + 10) + "px"); |
|
return true; |
|
}); |
|
|
|
|
|
svg.selectAll(".path.graticule") |
|
.data([graticule]).enter() |
|
.append("path") |
|
.classed("graticule", true) |
|
.attr("d", path); |
|
|
|
|
|
country = g.selectAll(".country") |
|
.data(countries) |
|
.enter().insert("path", ".graticule") |
|
.attr("class", function (d) {return "country country" + d.id; }) |
|
.attr("d", path) |
|
.text(function (d) { return d.id; }) |
|
.on("mouseover", function (d, i) { |
|
d3.select(this).style({'stroke-opacity': 1, 'stroke': '#F00'}); |
|
// http://stackoverflow.com/questions/17917072/#answer-17917341 |
|
// d3.select(this.parentNode.appendChild(this)).style({'stroke-opacity':1,'stroke':'#F00'}); |
|
if (d.id) { |
|
tooltip.classed("hidden", false); |
|
countryname.text(d.name); |
|
} |
|
}) |
|
.on("mouseout", function () { |
|
this.style.stroke = "none"; |
|
tooltip.classed("hidden", true); |
|
}) |
|
.on("mousedown.log", function (d) { |
|
console.log("id=" + d.id + "; name=" + d.name + "; centroid=[" + path.centroid(d) + "] px."); |
|
}); |
|
|
|
// FIRs |
|
fir = g.selectAll(".fir") |
|
.data(firs) |
|
.enter().insert("path", ".graticule") |
|
.attr("class", function (d) { return "fir " + d.id; }) |
|
.attr("d", path) |
|
.on("click", clicked) |
|
.on("mouseover", function (d) { |
|
d3.select(this).style("fill", "red"); |
|
tooltip.classed("hidden", false); |
|
countryname.text(d.id + "; " + d.properties.name); |
|
}) |
|
.on("mouseleave", function () { |
|
d3.select(this).style("fill", "yellow"); |
|
tooltip.classed("hidden", true); |
|
}); |
|
|
|
// intra FIR borders |
|
g.selectAll(".fir-boundary") |
|
.data([topojson.mesh(vis.firs, vis.firs.objects.firs, function (a, b) { |
|
return a !== b; |
|
})]) |
|
.enter().insert("path", ".graticule") |
|
.attr("d", path) |
|
.attr("class", "fir-boundary"); |
|
|
|
// // // external borders |
|
g.selectAll("fir-boundary ECTRL") |
|
.data([topojson.mesh(vis.firs, vis.firs.objects.firs, function (a, b) { |
|
return a === b; |
|
})]) |
|
.enter().insert("path", ".graticule") |
|
.attr("d", path) |
|
.attr("class", "fir-boundary ECTRL"); |
|
|
|
// TODO: this seems not too much a D3 idiom... |
|
fabs.forEach(function (f) { |
|
// from http://stackoverflow.com/a/16093597/963575 |
|
var mmm = topojson.merge( |
|
vis.firs, |
|
vis.firs.objects.firs.geometries.filter(function (d) { |
|
return d.properties.fab === f |
|
// return d.properties.fab === "UKIRELANDFAB" |
|
})) |
|
mmm.id = f |
|
g.append('path') |
|
.datum(mmm) |
|
.attr('class', 'fab ' + f) |
|
.attr('d', path) |
|
.on("click", clicked) |
|
.on("mouseover", function (d) { |
|
d3.select(this).style("fill", "red"); |
|
tooltip.classed("hidden", false); |
|
countryname.text(d.id); |
|
}) |
|
.on("mouseleave", function () { |
|
d3.select(this).style("fill", "blue"); |
|
tooltip.classed("hidden", true); |
|
}); |
|
}) |
|
|
|
}; |
|
|
|
}()); |