Skip to content

Instantly share code, notes, and snippets.

@mbostock mbostock/.block
Last active Sep 6, 2019

Embed
What would you like to do?
Solar Path
license: gpl-3.0

This chart shows the path of the Sun in the sky today based on your current location. The red dot represents the current apparent position of the Sun in terms of azimuth and elevation.

The outer black circle represents the horizon, where the elevation of the Sun is 0°. If the Sun is outside this circle, it is below the horizon. (This means it’s dark outside, though note that some definitions of twilight extend as far as 18° below the horizon.) Where the path intersects the horizon determines sunrise and sunset. The inner concentric circles represent higher solar elevations.

The radiating lines of the graticule represent azimuth. For example, 0° azimuth means that the Sun is due North of your current location. The azimuth tells you the direction of shadows cast by the Sun, while the elevation determines their length.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
text {
font: 10px sans-serif;
}
.horizon {
stroke: #000;
stroke-width: 1.5px;
}
.graticule {
stroke: #000;
stroke-opacity: .15;
}
.solar-path {
stroke: #f00;
stroke-width: 2px;
}
.sun circle {
fill: red;
stroke: #000;
}
.sun text {
text-anchor: middle;
}
.ticks--sun circle {
fill: red;
stroke: #fff;
stroke-width: 2px;
}
.ticks--sun text {
text-shadow: 0 1px 0 #fff, 0 -1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff;
}
.ticks line {
stroke: #000;
}
.ticks text {
text-anchor: middle;
}
.ticks--azimuth text:nth-of-type(9n + 1) {
font-weight: bold;
font-size: 14px;
}
#waiting {
font: 14px sans-serif;
position: absolute;
top: 540px;
left: 240px;
width: 480px;
margin: auto;
text-align: center;
}
#waiting b {
font-size: 24px;
line-height: 1.5em;
}
</style>
<svg width="960" height="960"></svg>
<div id="waiting">
<b>Determining your location… please wait.</b>
<br>(If prompted, please allow this page to access your location.)
</div>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="solar-calculator.js"></script>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
scale = width * .45;
var formatTime = d3.time.format("%-I %p"),
formatNumber = d3.format(".1f"),
formatAngle = function(d) { return formatNumber(d) + "°"; };
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);
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 + "°"; });
navigator.geolocation.getCurrentPosition(located);
function located(geolocation) {
var solar = solarCalculator([geolocation.coords.longitude, geolocation.coords.latitude]);
d3.select("#waiting").transition()
.style("opacity", 0)
.remove();
svg.insert("path", ".sphere")
.attr("class", "solar-path");
var sun = svg.insert("g", ".sphere")
.attr("class", "sun");
sun.append("circle")
.attr("r", 5);
sun.append("text")
.attr("class", "sun-label sun-label--azimuth")
.attr("dy", ".71em")
.attr("y", 10);
sun.append("text")
.attr("class", "sun-label sun-label--elevation")
.attr("dy", "1.81em")
.attr("y", 10);
var tickSun = svg.insert("g", ".sphere")
.attr("class", "ticks ticks--sun")
.selectAll("g");
refresh();
setInterval(refresh, 1000);
function refresh() {
var now = new Date,
start = d3.time.day.floor(now),
end = d3.time.day.offset(start, 1);
svg.select(".solar-path")
.datum({type: "LineString", coordinates: d3.time.minutes(start, end).map(solar.position)})
.attr("d", path);
sun
.datum(solar.position(now))
.attr("transform", function(d) { return "translate(" + projection(d) + ")"; });
sun.select(".sun-label--azimuth")
.text(function(d) { return formatAngle(d[0]) + " φ"; });
sun.select(".sun-label--elevation")
.text(function(d) { return formatAngle(d[1]) + " θ"; });
tickSun = tickSun
.data(d3.time.hours(start, end), function(d) { return +d; });
tickSun.exit().remove();
var tickSunEnter = tickSun.enter().append("g")
.attr("transform", function(d) { return "translate(" + projection(solar.position(d)) + ")"; });
tickSunEnter.append("circle")
.attr("r", 2.5);
tickSunEnter.append("text")
.attr("dy", "-.31em")
.attr("y", -6)
.text(formatTime);
}
}
d3.select(self.frameElement).style("height", height + "px");
function flippedStereographic(λ, φ) {
var cosλ = Math.cos(λ),
cosφ = Math.cos(φ),
k = 1 / (1 + cosλ * cosφ);
return [
k * cosφ * Math.sin(λ),
-k * Math.sin(φ)
];
}
</script>
// Equations based on NOAA’s Solar Calculator; all angles in radians.
// http://www.esrl.noaa.gov/gmd/grad/solcalc/
(function() {
var J2000 = Date.UTC(2000, 0, 1, 12),
π = Math.PI,
τ = 2 * π,
radians = π / 180,
degrees = 180 / π;
solarCalculator = function(location) {
var longitude = location[0],
minutesOffset = 720 - longitude * 4,
λ = location[0] * radians,
φ = location[1] * radians,
cosφ = Math.cos(φ),
sinφ = Math.sin(φ);
function position(date) {
var centuries = (date - J2000) / (864e5 * 36525),
θ = solarDeclination(centuries),
cosθ = Math.cos(θ),
sinθ = Math.sin(θ),
azimuth = ((date - d3.time.day.utc.floor(date)) / 864e5 * τ + equationOfTime(centuries) + λ) % τ - π,
zenith = Math.acos(Math.max(-1, Math.min(1, sinφ * sinθ + cosφ * cosθ * Math.cos(azimuth)))),
azimuthDenominator = cosφ * Math.sin(zenith);
if (azimuth < -π) azimuth += τ;
if (Math.abs(azimuthDenominator) > 1e-6) azimuth = (azimuth > 0 ? -1 : 1) *- Math.acos(Math.max(-1, Math.min(1, (sinφ * Math.cos(zenith) - sinθ) / azimuthDenominator))));
if (azimuth < 0) azimuth += τ;
// Correct for atmospheric refraction.
var atmosphere = 90 - zenith * degrees;
if (atmosphere <= 85) {
var te = Math.tan(atmosphere * radians);
zenith -= (atmosphere > 5 ? 58.1 / te - .07 / (te * te * te) + .000086 / (te * te * te * te * te)
: atmosphere > -.575 ? 1735 + atmosphere * (-518.2 + atmosphere * (103.4 + atmosphere * (-12.79 + atmosphere * .711)))
: -20.774 / te) / 3600 * radians;
}
// Note: if zenith > 108°, it’s dark.
return [azimuth * degrees, 90 - zenith * degrees];
}
function noon(date) {
var centuries = (d3.time.day.utc.floor(date) - J2000) / (864e5 * 36525),
minutes = (minutesOffset - (equationOfTime(centuries + (minutesOffset - (equationOfTime(centuries - longitude / (360 * 365.25 * 100)) * degrees * 4)) / (1440 * 365.25 * 100)) * degrees * 4) - date.getTimezoneOffset()) % 1440;
if (minutes < 0) minutes += 1440;
return new Date(+d3.time.day.floor(date) + minutes * 60 * 1000);
}
return {
position: position,
noon: noon
};
};
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);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.