Skip to content

Instantly share code, notes, and snippets.

@bmershon
Last active October 24, 2016 17:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmershon/f84b7b31bc2ac92f2e30 to your computer and use it in GitHub Desktop.
Save bmershon/f84b7b31bc2ac92f2e30 to your computer and use it in GitHub Desktop.
Context Sensitive Label Visibility

This was developed at the Washington Post as a prototype for an illustration about the devastating effects of U.S. nuclear testing carried out around the Marshall Islands. The purpose of this prototype was to demonstrate a means for showing geographic information what would otherise require many "small multiples" in a more dense, interactive, and perhaps intuitive way.

The use of context-senstive label placement that attempts to avoid cluttering the view with unecessary details was one of the most valuable design exercises involved in creating this graphic prototype.

SVG rendering is not performant enough for smooth mouse interaction of the 3D globe. The author's 2011 Mac Book Air, for example, manages a mere 11 frames per second as it performs the main update loop. Performance could be improved with more thoughtful managment of D3 4.0 selections as well as canvas rendering using the new D3 4.0 semantics for drawing paths.

N.B. Hex binning was performed using longitude and latitude: this produces a distortion in the binning of tests. See annulus sampling.

"use strict";
//d3.chart.min.js
(function(window){"use strict";var d3=window.d3;var hasOwnProp=Object.hasOwnProperty;var d3cAssert=function(test,message){if(test){return}throw new Error("[d3.chart] "+message)};d3cAssert(d3,"d3.js is required");d3cAssert(typeof d3.version==="string"&&d3.version.match(/^3/),"d3.js version 3 is required");"use strict";var lifecycleRe=/^(enter|update|merge|exit)(:transition)?$/;var Layer=function(base){d3cAssert(base,"Layers must be initialized with a base.");this._base=base;this._handlers={}};Layer.prototype.dataBind=function(){d3cAssert(false,"Layers must specify a `dataBind` method.")};Layer.prototype.insert=function(){d3cAssert(false,"Layers must specify an `insert` method.")};Layer.prototype.on=function(eventName,handler,options){options=options||{};d3cAssert(lifecycleRe.test(eventName),"Unrecognized lifecycle event name specified to `Layer#on`: '"+eventName+"'.");if(!(eventName in this._handlers)){this._handlers[eventName]=[]}this._handlers[eventName].push({callback:handler,chart:options.chart||null});return this._base};Layer.prototype.off=function(eventName,handler){var handlers=this._handlers[eventName];var idx;d3cAssert(lifecycleRe.test(eventName),"Unrecognized lifecycle event name specified to `Layer#off`: '"+eventName+"'.");if(!handlers){return this._base}if(arguments.length===1){handlers.length=0;return this._base}for(idx=handlers.length-1;idx>-1;--idx){if(handlers[idx].callback===handler){handlers.splice(idx,1)}}return this._base};Layer.prototype.draw=function(data){var bound,entering,events,selection,handlers,eventName,idx,len;bound=this.dataBind.call(this._base,data);d3cAssert(bound&&bound.call===d3.selection.prototype.call,"Invalid selection defined by `Layer#dataBind` method.");d3cAssert(bound.enter,"Layer selection not properly bound.");entering=bound.enter();entering._chart=this._base._chart;events=[{name:"update",selection:bound},{name:"enter",selection:this.insert.bind(entering)},{name:"merge",selection:bound},{name:"exit",selection:bound.exit.bind(bound)}];for(var i=0,l=events.length;i<l;++i){eventName=events[i].name;selection=events[i].selection;if(typeof selection==="function"){selection=selection()}if(selection.empty()){continue}d3cAssert(selection&&selection.call===d3.selection.prototype.call,"Invalid selection defined for '"+eventName+"' lifecycle event.");handlers=this._handlers[eventName];if(handlers){for(idx=0,len=handlers.length;idx<len;++idx){selection._chart=handlers[idx].chart||this._base._chart;selection.call(handlers[idx].callback)}}handlers=this._handlers[eventName+":transition"];if(handlers&&handlers.length){selection=selection.transition();for(idx=0,len=handlers.length;idx<len;++idx){selection._chart=handlers[idx].chart||this._base._chart;selection.call(handlers[idx].callback)}}}};"use strict";d3.selection.prototype.layer=function(options){var layer=new Layer(this);var eventName;layer.dataBind=options.dataBind;layer.insert=options.insert;if("events"in options){for(eventName in options.events){layer.on(eventName,options.events[eventName])}}this.on=function(){return layer.on.apply(layer,arguments)};this.off=function(){return layer.off.apply(layer,arguments)};this.draw=function(){return layer.draw.apply(layer,arguments)};return this};"use strict";function extend(object){var argsIndex,argsLength,iteratee,key;if(!object){return object}argsLength=arguments.length;for(argsIndex=1;argsIndex<argsLength;argsIndex++){iteratee=arguments[argsIndex];if(iteratee){for(key in iteratee){object[key]=iteratee[key]}}}return object}var initCascade=function(instance,args){var ctor=this.constructor;var sup=ctor.__super__;if(sup){initCascade.call(sup,instance,args)}if(hasOwnProp.call(ctor.prototype,"initialize")){this.initialize.apply(instance,args)}};var transformCascade=function(instance,data){var ctor=this.constructor;var sup=ctor.__super__;if(this===instance&&hasOwnProp.call(this,"transform")){data=this.transform(data)}if(hasOwnProp.call(ctor.prototype,"transform")){data=ctor.prototype.transform.call(instance,data)}if(sup){data=transformCascade.call(sup,instance,data)}return data};var Chart=function(selection,chartOptions){this.base=selection;this._layers={};this._attached={};this._events={};if(chartOptions&&chartOptions.transform){this.transform=chartOptions.transform}initCascade.call(this,this,[chartOptions])};Chart.prototype.initialize=function(){};Chart.prototype.unlayer=function(name){var layer=this.layer(name);delete this._layers[name];delete layer._chart;return layer};Chart.prototype.layer=function(name,selection,options){var layer;if(arguments.length===1){return this._layers[name]}if(arguments.length===2){if(typeof selection.draw==="function"){selection._chart=this;this._layers[name]=selection;return this._layers[name]}else{d3cAssert(false,"When reattaching a layer, the second argument "+"must be a d3.chart layer")}}layer=selection.layer(options);this._layers[name]=layer;selection._chart=this;return layer};Chart.prototype.attach=function(attachmentName,chart){if(arguments.length===1){return this._attached[attachmentName]}this._attached[attachmentName]=chart;return chart};Chart.prototype.draw=function(data){var layerName,attachmentName,attachmentData;data=transformCascade.call(this,this,data);for(layerName in this._layers){this._layers[layerName].draw(data)}for(attachmentName in this._attached){if(this.demux){attachmentData=this.demux(attachmentName,data)}else{attachmentData=data}this._attached[attachmentName].draw(attachmentData)}return this};Chart.prototype.on=function(name,callback,context){var events=this._events[name]||(this._events[name]=[]);events.push({callback:callback,context:context||this,_chart:this});return this};Chart.prototype.once=function(name,callback,context){var self=this;var once=function(){self.off(name,once);callback.apply(this,arguments)};return this.on(name,once,context)};Chart.prototype.off=function(name,callback,context){var names,n,events,event,i,j;if(arguments.length===0){for(name in this._events){this._events[name].length=0}return this}if(arguments.length===1){events=this._events[name];if(events){events.length=0}return this}names=name?[name]:Object.keys(this._events);for(i=0;i<names.length;i++){n=names[i];events=this._events[n];j=events.length;while(j--){event=events[j];if(callback&&callback===event.callback||context&&context===event.context){events.splice(j,1)}}}return this};Chart.prototype.trigger=function(name){var args=Array.prototype.slice.call(arguments,1);var events=this._events[name];var i,ev;if(events!==undefined){for(i=0;i<events.length;i++){ev=events[i];ev.callback.apply(ev.context,args)}}return this};Chart.extend=function(name,protoProps,staticProps){var parent=this;var child;if(protoProps&&hasOwnProp.call(protoProps,"constructor")){child=protoProps.constructor}else{child=function(){return parent.apply(this,arguments)}}extend(child,parent,staticProps);var Surrogate=function(){this.constructor=child};Surrogate.prototype=parent.prototype;child.prototype=new Surrogate;if(protoProps){extend(child.prototype,protoProps)}child.__super__=parent.prototype;Chart[name]=child;return child};"use strict";d3.chart=function(name){if(arguments.length===0){return Chart}else if(arguments.length===1){return Chart[name]}return Chart.extend.apply(Chart,arguments)};d3.selection.prototype.chart=function(chartName,options){if(arguments.length===0){return this._chart}var ChartCtor=Chart[chartName];d3cAssert(ChartCtor,"No chart registered with name '"+chartName+"'");return new ChartCtor(this,options)};d3.selection.enter.prototype.chart=function(){return this._chart};d3.transition.prototype.chart=d3.selection.enter.prototype.chart})(this);
//atlas.min.js
(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?factory():typeof define==="function"&&define.amd?define(factory):factory()})(this,function(){"use strict";var configuration={};function layer_exit(){var chart=this.chart();return this.remove()}function layer_merge(){var chart=this.chart();if(chart._projection){chart._projection.scale(chart._scale).rotate(chart._rotation).precision(chart._precision).translate(chart._translate)}chart._path.projection(chart._projection).pointRadius(chart._pointRadius);return this.attr("d",chart._path)}function chart_initialize(options){var depth=0;var chart=this;chart.options=options||{};chart._w=chart.base.attr("width")||960;chart._h=chart.base.attr("height")||500;chart.base=chart.base.append("g").attr("class","base");chart._projection=d3.geo.orthographic().clipAngle(90);chart._path=d3.geo.path();chart._graticule=null;chart._sphere=null;chart._precision=Math.sqrt(2);chart._scale=1;chart._translate=[0,0];chart._rotation=[0,0,0];var layerSphere=chart.base.append("g").attr("class","sphere").append("path");var layerGraticule=chart.base.append("g").attr("class","graticule").append("path");chart.options.layers.forEach(function(layer){var layerBase=chart.base.append("g").attr("class","layer-base-"+layer.object+"-"+depth);var layerConfig={dataBind:layer.databind||function(data){var chart=this.chart();var toBind=Array.isArray(data[layer.object])?data[layer.object]:[data[layer.object]];toBind=layer.filter?toBind.filter(layer.filter):toBind;return this.selectAll("."+layer.object).data(toBind)},insert:layer.insert||function(){var chart=this.chart();var selection=this.append("path").attr("class",layer.class||"").classed(layer.object,true).attr("id",layer.id||function(d,i){return i});if(layer.interactions){for(var e in layer.interactions){if(layer.interactions.hasOwnProperty(e)){selection.on(e,layer.interactions[e])}}}return selection},events:layer.events||{merge:layer_merge,exit:layer_exit}};chart.layer("layer-"+layer.object+"-"+depth,layerBase,layerConfig);depth++});chart.options.labels.forEach(function(layer){var labelBase=chart.base.append("g").attr("class","layer-labels-"+layer.object+"-"+depth);var labelConfig={dataBind:function(data){var chart=this.chart();var toBind=Array.isArray(data[layer.object])?data[layer.object]:[data[layer.object]];toBind=layer.filter?toBind.filter(layer.filter):toBind;toBind=toBind.filter(function(d){var c=chart._path.centroid(d);return!(isNaN(c[0])||isNaN(c[1]))});return this.selectAll("."+"label-"+layer.object).data(toBind)},insert:function(){var chart=this.chart();var selection=this.append("text").attr("class",layer.class||"").classed("label-"+layer.object,true);return selection},events:layer.events||{update:function(){this.attr("transform",function(d){return"translate("+chart._path.centroid(d)+")"}).attr("id",layer.id||"").text(layer.text);return this},exit:function(){this.remove();return this}}};chart.layer("labels-"+layer.object+"-"+depth,labelBase,labelConfig);depth++});chart.on("change:projection",function(){if(this.data){if(chart._projection){chart._projection.scale(chart._scale).rotate(chart._rotation).precision(chart._precision).translate(chart._translate);layerGraticule.datum(chart._graticule).attr("d",chart._path).attr("class","graticule");layerSphere.datum(chart._sphere).attr("d",chart._path).attr("class","sphere")}chart._path.projection(chart._projection).pointRadius(chart._pointRadius);this.draw(this.data)}})}configuration.initialize=chart_initialize;function chart_transform(data){var chart=this;if(!(data.type=="Topology"))return data;var t={};this.options.layers.forEach(function(layer){if(!data.objects.hasOwnProperty(layer.object)){return[]}if(data.objects[layer.object].type=="GeometryCollection"){t[layer.object]=topojson.feature(data,data.objects[layer.object]).features}if(data.objects[layer.object].type=="MultiPolygon"){t[layer.object]=topojson.feature(data,data.objects[layer.object])}});this.topology=data;this.data=t;chart.trigger("change:projection");return t}configuration.transform=chart_transform;function center(_){if(arguments.length===0){return this._scale}if(_)this._center=_;this.trigger("change:projection");return this}configuration.center=center;function graticule(_){if(arguments.length===0){return this._graticule}this._graticule=_;this.trigger("change:projection");return this}configuration.graticule=graticule;function height(_){if(arguments.length===0){return this._h}this._h=_;this.trigger("change:projection");return this}configuration.height=height;function path(_){if(arguments.length===0){return this._path}if(_)this._path=_;this.trigger("change:projection");return this}configuration.path=path;function projection(_){if(arguments.length===0){return this._projection}if(_||_===null)this._projection=_;this.trigger("change:projection");return this}configuration.projection=projection;function rotate(_){if(arguments.length===0){return this._rotation}if(_)this._rotation=_;this.trigger("change:projection");return this}configuration.rotate=rotate;function scale(_){if(arguments.length===0){return this._scale}if(_)this._center=_;this.trigger("change:projection");return this}configuration.scale=scale;function sphere(_){if(arguments.length===0){return this._sphere}this._sphere=_;this.trigger("change:projection");return this}configuration.sphere=sphere;function translate(_){if(arguments.length===0){return this._translate}if(_)this._translate=_;this.trigger("change:projection");return this}configuration.translate=translate;function precision(_){if(arguments.length===0){return this._precision}if(_)this._precision=_;this.trigger("change:projection");return this}configuration.precision=precision;function pointRadius(_){if(arguments.length===0){return this._pointRadius}if(_)this._pointRadius=_;this.trigger("change:projection");return this}configuration.pointRadius=pointRadius;function width(_){if(arguments.length===0){return this._w}this._w=_;this.trigger("change:projection");return this}configuration.width=width;function zoomToLayer(_,_filter){if(arguments.length===0){return this._projection}var f=typeof _filter==="undefined"?function(d){return true}:_filter;var chart=this;if(this.data){var layerObject=_;var collection={type:"FeatureCollection",features:this.data[layerObject].filter(f)};var b=collection.features.length==0?[[-1,-1],[1,1]]:chart._path.bounds(collection),s=.9/Math.max((b[1][0]-b[0][0])/chart._w,(b[1][1]-b[0][1])/chart._h),t=[(chart._w-s*(b[1][0]+b[0][0]))/2,(chart._h-s*(b[1][1]+b[0][1]))/2];chart._scale=s;chart._translate=t}chart.trigger("change:projection");return this}configuration.zoomToLayer=zoomToLayer;function rotateToLayer(_,_filter){if(arguments.length===0){return this._projection}var f=typeof _filter==="undefined"?function(d){return true}:_filter;var chart=this;if(this.data){var layerObject=_;var collection={type:"FeatureCollection",features:this.data[layerObject]};var filteredCollection={type:"FeatureCollection",features:this.data[layerObject].filter(f)};var b=[[-1,-1],[1,1]],c=d3.geo.centroid(filteredCollection),s=.9/Math.max((b[1][0]-b[0][0])/chart._w,(b[1][1]-b[0][1])/chart._h),t=[(chart._w-s*(b[1][0]+b[0][0]))/2,(chart._h-s*(b[1][1]+b[0][1]))/2];chart._scale=s;chart._translate=t;c=[-c[0],-c[1]];chart._rotation=c}chart.trigger("change:projection");return this}configuration.rotateToLayer=rotateToLayer;d3.chart("atlas",configuration)});
d3.select(self.frameElement).style("height", "960px");
(function() {
var margin = {top: 0, right: 0, bottom: 0, left: 0},
padding = {top: 0, right: 0, bottom: 0, left: 0},
outerWidth = 960,
outerHeight = 960,
innerWidth = outerWidth - margin.left - margin.right,
innerHeight = outerHeight - margin.top - margin.bottom,
width = innerWidth - padding.left - padding.right,
height = innerHeight - padding.top - padding.bottom;
var smallFontScale = d3.scale.linear().domain([height/2, 0]).range([8, 18]);
var bigFontScale = d3.scale.linear().domain([height/2, 0]).range([8, 22]);
var opacityScale = d3.scale.linear().domain([.8*height/2, 0]).range([0, 1]);
var tightOpacityScale = d3.scale.linear().domain([.6*height/2, 0]).range([0, .6]);
var hexagonOpacity = d3.scale.linear().domain([.85*height/2, 0]).range([0, .8]);
var hexagonStroke = d3.scale.sqrt().range([.5, 8]);
var z = d3.scale.linear()
.range([4, 250]);
var data;
var time0 = Date.now(),
time1;
var fps = d3.select("#fps span");
var p = [outerWidth/2, outerHeight/2], vx = -40, vy = 0;
// map screen distance to incremental change in globe rotation (per unit time)
// exponential function mimics a "gimbal-like" feel with softened control around center
var dλ = d3.scale.pow()
.domain([-outerWidth/2, outerWidth/2])
.range([15, -15])
.exponent(2.2);
var dφ = d3.scale.pow()
.domain([-outerHeight/2, outerHeight/2])
.range([-15, 15])
.exponent(2.2);
// clamp globe longitude rotation (if needed)
var λ = d3.scale.linear().clamp(true)
.domain([-180, 180])
.range([-180, 180]);
// clamp globe latitude rotation
var φ = d3.scale.linear().clamp(true)
.domain([-90, 90])
.range([-90, 90]);
var radius = d3.scale.linear()
.range([4, 25]);
var center = [165, 0];
var bins;
var hexbin = d3.hexbin()
.radius(1); //spherical coordinates, degrees
var background = d3.select("#nuclear-testing");
var svg = background.append("g")
.attr("transform", "translate(" + (margin.left + padding.left) + "," + (margin.top + padding.top) + ")")
.attr("id", "globe");
// cache a layer added to the map
var g_hexagons;
var powers = {
"FRA": "France",
"CHN": "China",
"PAK": "Pakistan",
"IND": "India",
"USA": "United States",
"RUS": "Russia",
"GBR": "UK",
"PRK": "North Korea",
"DZA": ""
}
var options = {};
options.layers = [];
options.labels = [];
// define a layer and labels
options.layers.push({
class: "land",
object: "land"
});
options.layers.push({
class: "country",
id: function(d) {return d.properties["adm0_a3"]},
object: "countries",
filter: function(d) {return powers.hasOwnProperty(d.properties["adm0_a3"])},
});
// country labels
options.labels.push({
id: function(d) {return d.properties["adm0_a3"]},
object: "countries",
class: "country",
text: function(d) {return powers[d.properties["adm0_a3"]]},
filter: function(d) {return powers.hasOwnProperty(d.properties["adm0_a3"])}
});
// placeholder layer, not tied to topojson object; used for drawing order
options.layers.push({
object: "hexagons"
});
// locators for important events
options.layers.push({
object: "events",
class: "event",
text: function(d) {return d.properties.blurb}
});
// labels for important events -- tied to "events" topology object
options.labels.push({
object: "events",
class: "blurb",
text: function(d) {return d.properties.blurb}
});
// d3.chart grafts itself onto and modifies the d3 selection.
// Sets up all of the layer groups before the data is bound
var globe = svg.chart("atlas", options)
.width(width)
.height(height)
.rotate(center)
.sphere({type: "Sphere"})
.precision(.3)
.graticule(d3.geo.graticule().step([20, 20]))
.projection(d3.geo.orthographic().clipAngle(90))
.pointRadius(function(d) {
if(d.properties) {
return (d.properties.blurb) ? 30 : 5;
}
})
/*
Define a callback to be called when "change:projection" events are triggered
within the chart.
Mutators like .rotate() and .zoomToLayer() trigger change:projection internally.
This approach allows the *this* context passed to the callback to refer to the chart instance.
This is useful when we want access things like the internal projection
and d3.geo.path instance for calculating the centroid of a feature.
*/
globe.on("change:projection", function() {
var chart = this;
var path = chart._path;
var projection = chart._projection;
if(!chart.data) return;
svg.selectAll(".label-countries")
.each(function(d) {
var c = path.centroid(d);
var dx = width/2 - c[0];
var dy = height/2 - c[1];
d.distance = Math.sqrt(dx * dx + dy * dy);
})
.style("fill-opacity", function(d) {return tightOpacityScale(d.distance)})
.style("font-size", function(d) {return smallFontScale(d.distance)})
svg.selectAll(".event")
.each(function(d) {
var c = path.centroid(d);
var dx = width/2 - c[0];
var dy = height/2 - c[1];
d.distance = Math.sqrt(dx * dx + dy * dy);
})
.style("stroke-opacity", function(d) {return opacityScale(d.distance)})
svg.selectAll(".blurb")
.attr("x", 30)
.attr("y", 60)
.each(function(d) {
var c = path.centroid(d);
var dx = width/2 - c[0];
var dy = height/2 - c[1];
d.distance = Math.sqrt(dx * dx + dy * dy);
})
.style("opacity", function(d) {return opacityScale(d.distance)})
.style("font-size", function(d) {return bigFontScale(d.distance)})
})
queue()
.defer(d3.json, "combined.json")
.await(ready);
// The default export, called when the required data (topojson file) is ready.
function ready(error, topology) {
data = topology;
var locations = topojson.feature(topology, topology.objects.nuclear).features;
locations.forEach(function(d) {
var p = d.geometry.coordinates;
d[0] = p[0], d[1] = p[1];
});
globe.draw(data)
.rotateToLayer("land");
var projection = globe.projection();
var path = globe.path();
bins = hexbin(locations).sort(function(a, b) { return b.length - a.length;});
// calculate total yield for each bin
bins.map(function(bin) {
var sum = 0;
var length = bin.length;
for(var i = 0; i < length; i++) {
sum += +(bin[i].properties.yield)
}
bin.totalyield = sum;
})
var yieldExtent = d3.extent(bins, function(bin) {
return bin.totalyield;
})
// set scale domains for nuclear test symbology
radius.domain(yieldExtent);
z.domain(yieldExtent);
hexagonStroke.domain([1, d3.max(bins, function(bin) {return bin.length})]);
// find dominate country in bin and use that for color coding
bins.map(function(d) {
var length = d.length;
for(var i = 0; i < length; i++) {
var country = d[i].properties.country;
}
d.properties = {};
d.properties.country = country;
})
g_hexagons = d3.select(".layer-base-hexagons-2");
updateHexagons(projection, path);
background.on("mousemove", function() {
p = d3.mouse(this);
vx = p[0] - outerWidth/2;
vy = p[1] - outerHeight/2;
});
d3.timer(function(){
center[0] = center[0] + dλ(vx);
center[1] = φ(center[1] + dφ(vy)); // clamped to avoid rotating past +/- 90 degrees
globe.rotate([center[0], center[1]]);
updateHexagons(projection, path);
time1 = Date.now();
fps.text(Math.round(1000 / (time1 - time0)));
time0 = time1;
})
// NORTH EAST HIGHLIGHT (from sun)
svg.append("circle")
.attr("cx", width / 2).attr("cy", height / 2)
.attr("r", globe.scale())
.attr("class","noclicks")
.style("fill", "url(#globe_highlight)");
// SOUTH WEST SHADING
svg.append("circle")
.attr("cx", width / 2).attr("cy", height / 2)
.attr("r", globe.scale())
.attr("class","noclicks")
.style("fill", "url(#globe_shading)");
}
// ENTER, UPDATE, EXIT for hexagons (bins generated from hexbining of nuclear tests)
function updateHexagons(projection, path) {
var hexagons = g_hexagons.selectAll(".hexagon")
.data(bins.filter(function(d) {return visible(d, path)}))
hexagons.enter().append("circle")
hexagons.attr("r", function(d) { return radius(d.totalyield); })
.each(function(d, i) {
var test = {
"type": "Point",
"coordinates": [d.x, d.y] // spherical coordinates
}
var centroid = path.centroid(test);
var dx = centroid[0] - width/2;
var dy = height/2 - centroid[1]; //dy is pos when centroid is above equator
// d.angle = Math.atan2(dx, dy); // angle of spike
// d.z = z(d.totalyield)
// d.tip = [d.z * Math.cos(d.angle), -1 * d.z * Math.sin(d.angle)]
d.distance = Math.sqrt(dx * dx + dy * dy);
})
.classed("hexagon", true)
.attr("id", function(d) {return d.properties.country})
.attr("transform", function(d) { return "translate(" + projection([d.x, d.y])[0] + "," + projection([d.x, d.y])[1] + ")"; })
.style("stroke-width", function(d) {return hexagonStroke(d.length)})
.style("fill-opacity", function(d) {return hexagonOpacity(d.distance)})
.style("stroke-opacity", function(d) {return hexagonOpacity(d.distance)})
hexagons.exit().remove();
}
// Run a point through the geometry pipeline to test for orthographic clipping
function visible(d, path) {
var test = {
"type": "Point",
"coordinates": [d.x, d.y] // spherical coordinates
}
var c = path.centroid(test);
return !(isNaN(c[0]) || isNaN(c[1]));
}
})();
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
(function() {
d3.hexbin = function() {
var width = 1,
height = 1,
r,
x = d3_hexbinX,
y = d3_hexbinY,
dx,
dy;
function hexbin(points) {
var binsById = {};
points.forEach(function(point, i) {
var py = y.call(hexbin, point, i) / dy, pj = Math.round(py),
px = x.call(hexbin, point, i) / dx - (pj & 1 ? .5 : 0), pi = Math.round(px),
py1 = py - pj;
if (Math.abs(py1) * 3 > 1) {
var px1 = px - pi,
pi2 = pi + (px < pi ? -1 : 1) / 2,
pj2 = pj + (py < pj ? -1 : 1),
px2 = px - pi2,
py2 = py - pj2;
if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) pi = pi2 + (pj & 1 ? 1 : -1) / 2, pj = pj2;
}
var id = pi + "-" + pj, bin = binsById[id];
if (bin) bin.push(point); else {
bin = binsById[id] = [point];
bin.i = pi;
bin.j = pj;
bin.x = (pi + (pj & 1 ? 1 / 2 : 0)) * dx;
bin.y = pj * dy;
}
});
return d3.values(binsById);
}
function hexagon(radius) {
var x0 = 0, y0 = 0;
return d3_hexbinAngles.map(function(angle) {
var x1 = Math.sin(angle) * radius,
y1 = -Math.cos(angle) * radius,
dx = x1 - x0,
dy = y1 - y0;
x0 = x1, y0 = y1;
return [dx, dy];
});
}
hexbin.x = function(_) {
if (!arguments.length) return x;
x = _;
return hexbin;
};
hexbin.y = function(_) {
if (!arguments.length) return y;
y = _;
return hexbin;
};
hexbin.hexagon = function(radius) {
if (arguments.length < 1) radius = r;
return "m" + hexagon(radius).join("l") + "z";
};
hexbin.centers = function() {
var centers = [];
for (var y = 0, odd = false, j = 0; y < height + r; y += dy, odd = !odd, ++j) {
for (var x = odd ? dx / 2 : 0, i = 0; x < width + dx / 2; x += dx, ++i) {
var center = [x, y];
center.i = i;
center.j = j;
centers.push(center);
}
}
return centers;
};
hexbin.mesh = function() {
var fragment = hexagon(r).slice(0, 4).join("l");
return hexbin.centers().map(function(p) { return "M" + p + "m" + fragment; }).join("");
};
hexbin.size = function(_) {
if (!arguments.length) return [width, height];
width = +_[0], height = +_[1];
return hexbin;
};
hexbin.radius = function(_) {
if (!arguments.length) return r;
r = +_;
dx = r * 2 * Math.sin(Math.PI / 3);
dy = r * 1.5;
return hexbin;
};
return hexbin.radius(1);
};
var d3_hexbinAngles = d3.range(0, 2 * Math.PI, Math.PI / 3),
d3_hexbinX = function(d) { return d[0]; },
d3_hexbinY = function(d) { return d[1]; };
})();
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
min-height: 960px;
}
#fps {
position: absolute;
margin: 20px
}
.noclicks {
pointer-events:none;
}
svg {
top: 0;
position: absolute;
cursor: move;
}
.spike {
fill: none;
stroke: #000;
stroke-width: 1px;
}
.sphere {
fill: #f3efe7;
fill-opacity: 1;
stroke: none;
stroke-width: 1px;
}
.graticule {
fill: none;
stroke: #000F24;
stroke-width: 0.2px;
stroke-dasharray: 2,2;
}
path.country {
fill: none;
stroke: #5e5e5e;
stroke-width: .3px;
fill-opacity: 0.8;
}
path.land {
fill: #dad3c7;
stroke-width: .2px;
stroke: #798d94;
}
path#CHN {
fill: #de8b57;
}
path#USA {
fill: #6b6483;
}
path#FRA {
fill: #ac769d;
}
path#PAK {
fill: #6c8969;
}
path#RUS {
fill: #c15e50;
}
path#IND {
fill: #fdc576;
}
path#GBR {
fill: #cad15f;
}
path#PRK {
fill: #31061e;
}
/* The countries who conducted tests*/
.label-countries {
font-family: FranklinITCProLight,FranklinITCStdLight,Helvetica,Arial,sans-serif;
fill: #000F24;
font-style: normal;
text-anchor: middle;
}
/* Blurbs about areas affected*/
.blurb {
pointer-events: none;
fill: #000F24;
text-anchor: middle;
font-style: italic;
font-family: FranklinITCProLight,FranklinITCStdLight,Helvetica,Arial,sans-serif;
}
/* Dots to represent tests */
.event {
pointer-events: none;
fill: none;
stroke: #000000;
stroke-width: 2px;
stroke-dasharray: 5, 5;
}
/* Dots to represent tests */
.hexagon {
stroke: #222222;
stroke-width: 1px;
}
/* Specific dot styling */
#China {
fill: #de8b57;
}
#US {
fill: #6b6483;
}
#France {
fill: #ac769d;
}
#Pakistan {
fill: #6c8969;
}
#USSR {
fill: #c15e50;
}
#India {
fill: #fdc576;
}
#UK {
fill: #7bd52c;
}
#PRK {
fill: #31061e;
}
</style>
<link rel="stylesheet" href="https://storage.googleapis.com/code.getmdl.io/1.0.0/material.indigo-pink.min.css">
<script src="https://storage.googleapis.com/code.getmdl.io/1.0.0/material.min.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<body>
<div id="fps">FPS: <span>?</span></div>
<div id="map">
<svg width=960 height=960 id="nuclear-testing">
<defs><radialGradient id="globe_highlight" cx="75%" cy="25%"><stop offset="5%" stop-color="#fff" stop-opacity="0.6"></stop><stop offset="100%" stop-color="#b5b5b5" stop-opacity="0.2"></stop></radialGradient></defs>
<radialGradient id="globe_shading" cx="55%" cy="45%"><stop offset="30%" stop-color="#fff" stop-opacity="0"></stop><stop offset="100%" stop-color="#505962" stop-opacity="0.3"></stop></radialGradient>
</svg>
</div>
</body>
<script src="https://d3js.org/queue.v1.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://d3js.org/d3.geo.projection.v0.min.js" charset="utf-8"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script>
<script src="hexbin.js"></script>
<script src="app.js"></script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment