/*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) { vis.params = params || {}; chart = d3.select(vis.params.chart || "#chart"); // placeholder div for svg width = vis.params.width || 600; height = vis.params.height || 600; 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, trafficdelays) { if (error) { console.error(error); } vis.firs = firs; vis.world = world; vis.wnames = wnames; vis.trafficdelays = trafficdelays; if (running > 0) { vis.start(); } else { vis.draw(vis.params); } } vis.loaddata = function (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()) : ""); var trafficdelays = (params.trafficdelays || "En-Route_Traffic_FAB_FIR.csv") + (params.refresh ? ("#" + Math.random()) : ""); queue() .defer(d3.json, topo) .defer(d3.json, world) .defer(d3.tsv, names) .defer(d3.csv, trafficdelays) .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 + ")"); // console.log("g.attr('transform') [zoomed]: " + g.attr("transform")); // console.log("center [zoomed]: " + path.projection().center()); // console.log("translate [zoomed]: " + path.projection().translate()); // console.log("rotate [zoomed]: " + path.projection().rotate()); // console.log("scale [zoomed]: " + path.projection().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); // console.log("center: " + path.projection().center()); } 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); // console.log("center [clicked]: " + path.projection().center()); } vis.draw = function (params) { // make stuff here! var pars = params || {}, scale = pars.scale || 600, cLon = pars.centerLon || 10, cLat = pars.centerLat || 51.5, 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").append("tspan"), 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 = [ {"id": "BALTICFAB", "name": "Baltic FAB"}, {"id": "BLUMEDFAB", "name": "BLUE MED FAB"}, {"id": "DANUBEFAB", "name": "DANUBE FAB"}, {"id": "DKSEFAB", "name": "DK-SE FAB"}, {"id": "FABCE", "name": "FAB CE (SES RP2)"}, {"id": "FABEC", "name": "FABEC"}, {"id": "NEFAB", "name": "NEFAB"}, {"id":"SWFAB", "name": "SW FAB"}, {"id":"UKIRELANDFAB", "name": "UK-Ireland FAB"} ]; function idxForName(target) { return function(idx, item, i) { return item.name === target ? i : idx; }; } var rateById = {}; vis.trafficdelays.forEach(function(d) { var obj_idx = fabs.reduce(idxForName(d.ENTITY_NAME), -1); if (obj_idx === -1) { console.log(d); console.log("FAB name NOT found!!!!"); } else { //console.log(d); if (d.YEAR === "2015") { fabs[obj_idx].rate = d.FLT_ERT_1 == 0 ? 0 : d.DLY_ERT_1 / d.FLT_ERT_1; fabs[obj_idx].year = +d.YEAR; rateById[fabs[obj_idx].id] = fabs[obj_idx].rate; } } }); console.log(fabs); console.log(rateById); var twodecimals = d3.format(".2n"); var rates = fabs.map(function(d) { return d.rate;}); console.log([d3.min(rates), d3.max(rates)]); var qClasses = 5; var quantize = d3.scale.quantize() .domain([d3.min(rates), d3.max(rates)]) .range(d3.range(qClasses).map(function(i) { return "q" + i + "-" + qClasses; })); // var color = d3.scale.threshold() // .domain([.02, .03, .03, .05, .65]) // .range(["#f2f0f7", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"]); 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; } }); }); // /* Initialize tooltip */ // var tip = d3.tip().attr('class', 'd3-tip').html(function(d) { return d; }); // tip.html(function(d) { // return d.name + "
" + d.name + // ": avg delay per flight: " + twodecimals(d.rate) + ""; // }); // // use tip.offset to follow an hidden circle that follows the mouse when over the FAB // /* Invoke the tip in the context of your visualization */ // svg.call(tip); 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); svg.append("g") .attr("class", "legendQuant") .attr("transform", "translate(20," + height/3 + ")"); var legend = d3.legend.color() .labelFormat(d3.format(".2f")) .useClass(true) .scale(quantize); svg.select(".legendQuant") .call(legend); 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 = "#000"; tooltip.classed("hidden", true); }) .on("mousedown.log", function (d) { console.log("id=" + d.id + "; name=" + d.name + "; centroid=[" + path.centroid(d) + "] px."); }); // 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.id; })); mmm.id = f.id; mmm.name = f.name; mmm.rate = f.rate; g.append("path") .datum(mmm) .attr("d", path) // .attr("class", "fab " + f.id) // .style("fill", function(d) { return color(d.rate); }) .attr("class", "fab " + f.id + " " + quantize(f.rate)) // .on("click", clicked) .on("mouseover", function (d) { d3.select(this).style("stroke", "red"); // tip.show(d); tooltip.classed("hidden", false); // countryname.text(d.name + ", avg delay per flight: " + twodecimals(d.rate)); countryname.html(d.name + "
avg delay per flight: " + twodecimals(d.rate)); }) .on("mouseleave", function (d) { d3.select(this).style("stroke", "yellow"); tooltip.classed("hidden", true); // tip.hide(d); }) ; }); }; }());