Skip to content

Instantly share code, notes, and snippets.

@chriswmackey
Last active October 23, 2019 20:41
Show Gist options
  • Save chriswmackey/6838c64f7d2c7410bdc5f8925119285a to your computer and use it in GitHub Desktop.
Save chriswmackey/6838c64f7d2c7410bdc5f8925119285a to your computer and use it in GitHub Desktop.
Solar Path With Sliders
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.

forked from mbostock's block: Solar Path

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<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>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
#inputSliders { font-family:sans-serif;outline:none;margin-top:10px}
.inputgroup {border:none;}
.slider { width:420px;float:left;padding:10px;}
.slider2 { width:290px;float:left;padding:10px;}
label { float:left;font-weight:bold;padding-bottom:10px;}
input[type=range] { float:left;clear:left;margin-right:10px;width:380px;}
.slider2 input[type=range] { float:left;clear:left;margin-right:10px;width:180px;}
input[type=range]::-ms-track { background: transparent;border-color: transparent;color: transparent;-webkit-appearance: none}
input[type=range]::-webkit-slider-runnable-track { height: 5px;background:#7c7c7c; margin-top: -4px;}
input[type=range]::-webkit-slider-thumb { margin-top:-6px;}
#inputSliders p {padding-top:10px;}
</style>
</head>
<body>
<div id="inputSliders">
<form id="sliders" autocomplete="off">
<fieldset class="inputgroup">
<div class="slider2">
<label>Latitude</label>
<input type="range" name="lat" id="lat" value="45" min="-90" max="90" step = "1"><p id="latoutput">45</p></div>
<div class="slider2" >
<label>Longitude</label>
<input type="range" name="lon" id="lon" value="-70" min="-180" max="180" step = "1"><p id="lonoutput">-70</p></div>
<div class="slider2" >
<label>Time Zone</label>
<input type="range" name="tz" id="tz" value="-4" min="-12" max="12" step = "1"><p id="zoneoutput">GMT-4</p></div>
<div class="slider2">
<label>Month</label>
<input type="range" name="month" id="month" value="3" min="1" max="12" step = "1"><p id="monthoutput">3</p></div>
<div class="slider2" >
<label>Day</label>
<input type="range" name="day" id="day" value="21" min="1" max="31" step = "1"><p id="dayoutput">21</p></div>
<div class="slider2">
<label>Hour</label>
<input type="range" name="hour" id="hour" value="10.5" min="1" max="24" step = "0.25"><p id="houroutput">10.5</p></div>
</fieldset>
</form>
</div>
<svg width="960" height="960"></svg>
<script src="solar-calculator.js"></script>
<script>
// Inputs
var Lat = parseFloat($("#lat").val());
var Lon = parseFloat($("#lon").val());
var TimeZone = parseFloat($("#tz").val());
var Month = parseFloat($("#month").val());
var Day = parseFloat($("#day").val());
var Hour = parseFloat($("#hour").val());
// Update the displayed values for the sliders
$("#lat").on("input", function(event) {
Lat = parseFloat($(this).val());
$("#latoutput").text(Lat.toString());
refresh();
});
$("#lon").on("input", function(event) {
Lon = parseFloat($(this).val());
$("#lonoutput").text(Lon.toString());
refresh();
});
$("#tz").on("input", function(event) {
TimeZone = parseFloat($(this).val());
$("#zoneoutput").text('GMT' + TimeZone.toString());
refresh();
});
$("#month").on("input", function(event) {
Month = parseFloat($(this).val());
$("#monthoutput").text(Month.toString());
refresh();
});
$("#day").on("input", function(event) {
Day = parseFloat($(this).val());
$("#dayoutput").text(Day.toString());
refresh();
});
$("#hour").on("input", function(event) {
Hour = parseFloat($(this).val());
$("#houroutput").text(Hour.toString());
refresh();
});
// Sunpath
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
scale = width * .45;
var formatTime = d3.time.format("%I"),
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 + "°"; });
refresh();
function refresh() {
var solar = solarCalculator([Lon, Lat]);
svg.selectAll('.solar-path').remove();
svg.selectAll('.sun').remove();
svg.selectAll('.ticks--sun').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");
offset = (new Date().getTimezoneOffset())/60
now = new Date(2000, Month-1, Day, Hour - TimeZone - offset, (Hour%parseInt(Hour))*60)
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(function(d) { return d.getHours() + TimeZone + offset});
}
d3.select(self.frameElement).style("height", height + 200 + "px");
function flippedStereographic(λ, φ) {
var cosλ = Math.cos(λ),
cosφ = Math.cos(φ),
k = 1 / (1 + cosλ * cosφ);
return [
k * cosφ * Math.sin(λ),
-k * Math.sin(φ)
];
}
</script>
</body>
// 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