|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
svg { stroke-linejoin: round; } |
|
|
|
.shell { |
|
width:960px; |
|
position:relative; |
|
} |
|
.tally { |
|
width:100%; |
|
margin: 0 auto; |
|
text-align:center; |
|
font-family: sans-serif; |
|
color:#bbb; |
|
position: absolute; |
|
top: 60px; |
|
|
|
} |
|
|
|
/*map*/ |
|
.horizon { |
|
fill: none; |
|
stroke: #eee; |
|
opacity: 0.5; |
|
stroke-width: 2px; |
|
} |
|
|
|
.ocean { |
|
fill: url(#oceanGradient); |
|
} |
|
|
|
.graticule { |
|
fill: none; |
|
stroke: #aaa; |
|
stroke-width: .5px; |
|
stroke-opacity: .1; |
|
} |
|
|
|
|
|
.land { |
|
stroke-opacity:0; |
|
stroke: #2e414c; |
|
stroke-width: .5px; |
|
|
|
fill: #fcfcff; |
|
fill-opacity:1; |
|
} |
|
|
|
.land.thickhalo { |
|
opacity: 1; |
|
fill:none; |
|
|
|
stroke-opacity:0.04; |
|
stroke: #2e414c; |
|
stroke-width: 8px; |
|
} |
|
|
|
.land.thinhalo { |
|
opacity: 1; |
|
fill:none; |
|
|
|
stroke-opacity:0.1; |
|
stroke: #2e414c; |
|
stroke-width: 2.5px; |
|
} |
|
|
|
.night { |
|
stroke: none; |
|
stroke-width: 3px; |
|
stroke-opacity: 0.5; |
|
|
|
fill: #1f2a33; |
|
fill: url(#nightGradient); |
|
} |
|
|
|
.sun { |
|
fill: url(#sunGradient); |
|
stroke: #ffc800; |
|
stroke-opacity:0.2; |
|
transition:fill .3s, stroke .3s; |
|
} |
|
|
|
.sun { |
|
mask: url(#globeMask); |
|
} |
|
|
|
/* */ |
|
|
|
#globeMask rect { fill: #fff; } |
|
|
|
/* |
|
the opacity of the mask's circle |
|
determines whether the shape of the globe |
|
shows up in the opacity mask or not |
|
|
|
so, by 'toggling' the circle as part of the mask |
|
we can choose when to occlude an element using |
|
the globe's shape, creating a false 3d effect |
|
when an element passes 'behind' the globe. |
|
|
|
the globeMaskCircle's fill color determines the extent to which |
|
it masks out the things 'behind' it. #000 = fully hidden |
|
*/ |
|
#globeMaskCircle { |
|
fill: #000; |
|
opacity: 0; |
|
} |
|
|
|
#globeMaskCircle.behind { |
|
opacity: 1; |
|
} |
|
</style> |
|
|
|
<script src="http://d3js.org/d3.v3.min.js"></script> |
|
<script src="http://d3js.org/queue.v1.min.js"></script> |
|
<script src="http://d3js.org/topojson.v1.min.js"></script> |
|
|
|
<body> |
|
|
|
<div class="shell"> |
|
<div class="tally"></div> |
|
<svg width="960" height="500" id="globe"></svg> |
|
</div> |
|
|
|
<script> |
|
// begin code for your graphic here: |
|
|
|
var mapW = 960, |
|
mapH = 500; |
|
|
|
// utility |
|
var π = Math.PI, |
|
radians = π / 180, |
|
degrees = 180 / π; |
|
|
|
var parseDate = d3.time.format("%Y-%m-%d-%H-%Z").parse, |
|
logDate = d3.time.format("%B %e"); |
|
|
|
// map setup |
|
|
|
var earth = d3.geo.orthographic() |
|
.scale(0.25 *mapH) |
|
.translate([mapW /2, mapH /2]) |
|
.clipAngle(90) |
|
// .rotate([-8, -61, 0]) |
|
.rotate([10, 150, 23.4]) |
|
.precision(.1); |
|
|
|
var sky = d3.geo.orthographic() |
|
.scale(0.49 *mapH) |
|
.translate(earth.translate()) |
|
.clipAngle(earth.clipAngle()) |
|
.rotate(earth.rotate()) |
|
.precision(earth.precision()); |
|
|
|
var circle = d3.geo.circle() |
|
.angle(90); |
|
|
|
var path = d3.geo.path() |
|
.projection(earth); |
|
|
|
var skypath = d3.geo.path() |
|
.projection(sky); |
|
|
|
var graticule = d3.geo.graticule(); |
|
|
|
var svg = d3.select("svg") |
|
.attr('class','map') |
|
.attr("width", mapW) |
|
.attr("height", mapH) |
|
.attr('viewBox', '' + 0 + ' ' + 0 + ' ' + mapW + ' ' + mapH ); |
|
|
|
var defs = svg.append("defs"); |
|
|
|
var sunScale = d3.scale.linear().domain([0,1]).range([5,25]); |
|
|
|
var jun6 = parseDate('2014-06-01-00--0000'); |
|
|
|
|
|
// read data files |
|
queue() |
|
.defer(d3.json, "/d/4090846/world-110m.json") |
|
.await(ready); |
|
|
|
function ready(error, world) { |
|
|
|
var num = 0; |
|
var start = earth.rotate(); |
|
|
|
d3.timer(function() { |
|
var dt = addMinutes(jun6,num); |
|
|
|
d3.select('.tally').text(logDate(dt)) |
|
|
|
var newRot = [ |
|
(num/1440/0.8)+start[0], |
|
start[1], |
|
start[2] |
|
]; |
|
|
|
earth.rotate(newRot); |
|
sky.rotate(newRot) |
|
|
|
redraw(dt) |
|
|
|
num+=1440; // increment time in minutes (1440 = 1 day) |
|
}); |
|
|
|
buildDefs(); |
|
buildGlobe(); |
|
|
|
function buildDefs() { |
|
// draw map |
|
defs.append("path") |
|
.datum({type: "Sphere"}) |
|
.attr("id", "sphere") |
|
.attr("d", path); |
|
|
|
defs.append("clipPath") |
|
.attr("id", "clip") |
|
.append("use") |
|
.attr("xlink:href", "#sphere"); |
|
|
|
var globeMask = defs.append('mask') |
|
.attr('id','globeMask') |
|
.attr('x',0).attr('y',0) |
|
.attr('width',mapW) |
|
.attr('height',mapH) |
|
|
|
globeMask.append('rect') |
|
.attr('x',0).attr('y',0) |
|
.attr('width',mapW) |
|
.attr('height',mapH) |
|
|
|
globeMask.append('use') |
|
.attr('id','globeMaskCircle') |
|
.attr("xlink:href", "#sphere"); |
|
|
|
var sunGradient = defs.append("radialGradient") |
|
.attr("id", "sunGradient") |
|
.attr("cx", "75%") |
|
.attr("cy", "25%"); |
|
|
|
sunGradient.append("stop").attr("offset", "5%" ).attr("stop-color", "#fffffa"); |
|
sunGradient.append("stop").attr("offset", "150%").attr("stop-color", "#fdf2ce"); |
|
|
|
var oceanGradient = defs.append("radialGradient") |
|
.attr("id", "oceanGradient") |
|
.attr("cx", "75%") |
|
.attr("cy", "25%"); |
|
|
|
oceanGradient.append("stop").attr("offset", "5%" ).attr("stop-color", "#fff"); |
|
oceanGradient.append("stop").attr("offset", "150%").attr("stop-color", "#d4dfe8"); |
|
|
|
var nightGradient = defs.append("radialGradient") |
|
.attr("id", "nightGradient") |
|
.attr("r", "110%") |
|
.attr("cx", "75%") |
|
.attr("cy", "25%"); |
|
|
|
|
|
nightGradient.append("stop").attr("offset", "0%" ).attr("stop-color", "rgba(31, 42, 51,0.3)"); |
|
nightGradient.append("stop").attr("offset", "100%").attr("stop-color", "rgba(31, 42, 51,0.0)"); |
|
|
|
} |
|
|
|
function buildGlobe() { |
|
var landMasses = topojson.feature(world, world.objects.land); |
|
|
|
svg.append("use") |
|
.attr("class", "horizon") |
|
.attr("xlink:href", "#sphere"); |
|
|
|
svg.append("use") |
|
.attr("class", "ocean") |
|
.attr("xlink:href", "#sphere"); |
|
|
|
svg.append("path") |
|
.datum(graticule) |
|
.attr("class", "graticule"); |
|
|
|
svg.append("path") |
|
.datum(landMasses) |
|
.attr("class", "land thickhalo"); |
|
|
|
svg.append("path") |
|
.datum(landMasses) |
|
.attr("class", "land thinhalo"); |
|
|
|
svg.append("path") |
|
.datum(landMasses) |
|
.attr("class", "land"); |
|
|
|
svg.append("path").attr("class", "night"); |
|
|
|
d3.selectAll('.land, .night') |
|
.attr("clip-path", "url(#clip)"); |
|
|
|
svg.append('circle').datum([]) |
|
.attr('class','sun'); |
|
} |
|
|
|
function redraw(date) { |
|
date = date || new Date; |
|
|
|
var night = d3.selectAll('.night'), |
|
sunshine = d3.selectAll('.sun'), |
|
globeMaskCircle = d3.selectAll('#globeMaskCircle'); |
|
|
|
var sunPos = solarPosition(date), |
|
antiSunPos = antipode(sunPos); |
|
|
|
var noon = { |
|
earth: earth(sunPos), |
|
sky: sky(sunPos) |
|
} |
|
var midnight = { |
|
earth: earth(antiSunPos), |
|
sky: sky(antiSunPos) |
|
} |
|
|
|
night.datum(circle.origin(antiSunPos)).attr("d", path); |
|
|
|
d3.select('#nightGradient') |
|
/* |
|
this nastiness roughly calculates gradient center as a % of the shape it's being applied to |
|
based on where the projected sun 'location' falls within the viewport. |
|
*/ |
|
.attr('cx', ((midnight.earth[0]-(mapW-mapH)/2)/mapH*100)+'%') |
|
.attr('cy', (midnight.earth[1]/mapH*100)+'%') |
|
|
|
d3.select('#oceanGradient') |
|
.attr('cx', ((noon.earth[0]-(mapW-mapH)/2)/mapH*100)+'%') |
|
.attr('cy', (noon.earth[1]/mapH*100)+'%') |
|
|
|
var sunSizeFactor = 1-(degrees_from_center(sunPos)/180); |
|
|
|
sunshine |
|
.attr("cx", noon.sky[0]) |
|
.attr("cy", noon.sky[1]) |
|
.attr('r', sunScale(sunSizeFactor)); |
|
|
|
d3.selectAll('.land').attr('d',path); |
|
d3.selectAll('.graticule').attr("d", path); |
|
|
|
globeMaskCircle |
|
.classed('behind',function(){ |
|
// "behindGlobe" meaning 'appearing more distant from the viewer than the globe' |
|
return isSunBehindGlobe = degrees_from_center(sunPos) > 90 ? true : false;; |
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function degrees_from_center(d) { |
|
var distanceBetween = d3.geo.distance, |
|
centerPos = [ |
|
-earth.rotate()[0], |
|
-earth.rotate()[1] |
|
]; |
|
|
|
return distanceBetween(d,centerPos)*degrees; |
|
} |
|
|
|
function fade_at_edge(d) { |
|
var sunPos = d; |
|
|
|
// distance in degrees from centerpoint of globe rotation |
|
// to pt "underneath" the sun |
|
var distance = degrees_from_center(sunPos) |
|
|
|
// degrees on either side of 90 during which fading of the revolving 'sun' happens |
|
var fade_range = 10; |
|
|
|
var fade = d3.scale.linear() |
|
.domain([90+fade_range,90-fade_range]) |
|
.range([0.1,1]) // opacity range |
|
.clamp(true) |
|
|
|
return fade(distance) |
|
} |
|
|
|
// http://stackoverflow.com/questions/1197928/how-to-add-30-minutes-to-a-javascript-date-object |
|
function addMinutes(date, minutes) { |
|
return new Date(date.getTime() + minutes*60000); |
|
} |
|
|
|
|
|
|
|
// from http://bl.ocks.org/mbostock/4597134 |
|
|
|
function antipode(position) { |
|
return [position[0] + 180, -position[1]]; |
|
} |
|
|
|
function solarPosition(time) { |
|
var centuries = (time - Date.UTC(2000, 0, 1, 12)) / 864e5 / 36525, // since J2000 |
|
longitude = (d3.time.day.utc.floor(time) - time) / 864e5 * 360 - 180; |
|
return [ |
|
longitude - equationOfTime(centuries) * degrees, |
|
solarDeclination(centuries) * degrees |
|
]; |
|
} |
|
|
|
// Equations based on NOAA’s Solar Calculator; all angles in radians. |
|
// http://www.esrl.noaa.gov/gmd/grad/solcalc/ |
|
|
|
function equationOfTime(centuries) { |
|
var e = eccentricityEarthOrbit(centuries), |
|
m = solarGeometricMeanAnomaly(centuries), |
|
l = solarGeometricMeanLongitude(centuries), |
|
y = Math.tan(obliquityCorrection(centuries) / 2); |
|
y *= y; |
|
return y * Math.sin(2 * l) |
|
- 2 * e * Math.sin(m) |
|
+ 4 * e * y * Math.sin(m) * Math.cos(2 * l) |
|
- 0.5 * y * y * Math.sin(4 * l) |
|
- 1.25 * e * e * Math.sin(2 * m); |
|
} |
|
|
|
function solarDeclination(centuries) { |
|
return Math.asin(Math.sin(obliquityCorrection(centuries)) * Math.sin(solarApparentLongitude(centuries))); |
|
} |
|
|
|
function solarApparentLongitude(centuries) { |
|
return solarTrueLongitude(centuries) - (0.00569 + 0.00478 * Math.sin((125.04 - 1934.136 * centuries) * radians)) * radians; |
|
} |
|
|
|
function solarTrueLongitude(centuries) { |
|
return solarGeometricMeanLongitude(centuries) + solarEquationOfCenter(centuries); |
|
} |
|
|
|
function solarGeometricMeanAnomaly(centuries) { |
|
return (357.52911 + centuries * (35999.05029 - 0.0001537 * centuries)) * radians; |
|
} |
|
|
|
function solarGeometricMeanLongitude(centuries) { |
|
var l = (280.46646 + centuries * (36000.76983 + centuries * 0.0003032)) % 360; |
|
return (l < 0 ? l + 360 : l) / 180 * π; |
|
} |
|
|
|
function solarEquationOfCenter(centuries) { |
|
var m = solarGeometricMeanAnomaly(centuries); |
|
return (Math.sin(m) * (1.914602 - centuries * (0.004817 + 0.000014 * centuries)) |
|
+ Math.sin(m + m) * (0.019993 - 0.000101 * centuries) |
|
+ Math.sin(m + m + m) * 0.000289) * radians; |
|
} |
|
|
|
function obliquityCorrection(centuries) { |
|
return meanObliquityOfEcliptic(centuries) + 0.00256 * Math.cos((125.04 - 1934.136 * centuries) * radians) * radians; |
|
} |
|
|
|
function meanObliquityOfEcliptic(centuries) { |
|
return (23 + (26 + (21.448 - centuries * (46.8150 + centuries * (0.00059 - centuries * 0.001813))) / 60) / 60) * radians; |
|
} |
|
|
|
function eccentricityEarthOrbit(centuries) { |
|
return 0.016708634 - centuries * (0.000042037 + 0.0000001267 * centuries); |
|
} |
|
|
|
|
|
</script> |