var π = Math.PI, τ = 2 * π, radians = π / 180, degrees = 180 / π; var width = 960, height = 960, scale = width * .45; var solar = solarCalculator([-122, 37]), start = d3.time.year.utc.floor(new Date), end = d3.time.year.utc.offset(start, 1); var projection = d3.geo.projection(flippedStereographic) .scale(scale) .clipAngle(130) .rotate([0, -90]) .translate([width / 2 + .5, height / 2 + .5]) .precision(.1); var path = d3.geo.path() .projection(projection); var svg = d3.select('body').append('svg') .attr('class', 'sun') .attr('width', width) .attr('height', height); svg.append('path') .datum(d3.geo.circle().origin([0, 90]).angle(90)) .attr('class', 'horizon') .attr('d', path); svg.append('path') .datum(d3.geo.graticule()) .attr('class', 'graticule') .attr('d', path); var ticksAzimuth = svg.append('g') .attr('class', 'ticks ticks--azimuth'); ticksAzimuth.selectAll('line') .data(d3.range(360)) .enter().append('line') .each(function(d) { var p0 = projection([d, 0]), p1 = projection([d, d % 10 ? -1 : -2]); d3.select(this) .attr('x1', p0[0]) .attr('y1', p0[1]) .attr('x2', p1[0]) .attr('y2', p1[1]); }); ticksAzimuth.selectAll('text') .data(d3.range(0, 360, 10)) .enter().append('text') .each(function(d) { var p = projection([d, -4]); d3.select(this) .attr('x', p[0]) .attr('y', p[1]); }) .attr('dy', '.35em') .text(function(d) { return d === 0 ? 'N' : d === 90 ? 'E' : d === 180 ? 'S' : d === 270 ? 'W' : d + '°'; }); svg.append('g') .attr('class', 'ticks ticks--elevation') .selectAll('text') .data(d3.range(10, 91, 10)) .enter().append('text') .each(function(d) { var p = projection([0, d]); d3.select(this) .attr('x', p[0]) .attr('y', p[1]); }) .attr('dy', '.35em') .text(function(d) { return d + '°'; }); function analemmaForHour(h) { function pos(d) { return solar.position(d3.time.hour.utc.offset(d, h)); } return { type: 'LineString', coordinates: d3.time.days.utc(start, end).map(pos) }; } var analemmas = svg.insert('g', '.sphere') .attr('class', 'analemmas') .selectAll('path') .data(d3.range(24)) .enter().append('path') .datum(analemmaForHour) .attr('d', path); function setLocation(lat, lng) { solar = solarCalculator([lng, lat]); analemmas.remove(); analemmas = svg.select('g.analemmas') .selectAll('path') .data(d3.range(24)) .enter().append('path') .datum(analemmaForHour) .attr('d', path); } function flippedStereographic(λ, φ) { var cosλ = Math.cos(λ), cosφ = Math.cos(φ), k = 1 / (1 + cosλ * cosφ); return [ k * cosφ * Math.sin(λ), -k * Math.sin(φ) ]; }