A pseudo-3d solar system in d3 using orthographic projections and radial gradients for shading. Each planet rotates at its relative velocity to Earth. Hover over each planet to reveal info.
Future ideas: add a glowing Sun and orbiting moons.
license: mit |
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<style> | |
body { margin: 0; position: fixed; top: 0; right: 0; bottom: 0; left: 0; } | |
svg { | |
background-color: black; | |
} | |
.bounding-box { | |
fill: white; | |
fill-opacity: 0; | |
stroke: #fff; | |
stroke-opacity: 0.5; | |
stroke-dasharray: 3,3; | |
} | |
.label { | |
font-family: monospace; | |
opacity: 1; | |
font-size: 10px; | |
fill: #fff; | |
} | |
.info { | |
font-family: monospace; | |
opacity: 1; | |
font-size: 8px; | |
fill: #fff; | |
} | |
.planet circle, .graticule { | |
fill: none; | |
stroke: #123; | |
stroke-opacity: 0.15; | |
} | |
.planet circle { | |
stroke-width: 1px; | |
} | |
.axis-line { | |
stroke: #fff; | |
stroke-opacity: 0.3; | |
} | |
.star { | |
fill: white; | |
opacity: 1; | |
} | |
</style> | |
</head> | |
<body> | |
<script> | |
var margin = {top: 100, right: 50, bottom: 100, left: 50}; | |
var width = 960 - margin.left - margin.right, | |
height = 500 - margin.top - margin.bottom; | |
var svg = d3.select("body").append("svg") | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom) | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
var starArea = d3.select("svg").append("g"); | |
var config = { | |
padding: 10, | |
axisMultiplier: 1.4, | |
velocity: [0.01, 0], | |
starRadius: 1, | |
glowRadius: 2 | |
}; | |
var solar = [ | |
{name: "Mercury", tilt: 0.03, radius: 2439.7, period: 58.65, colours: ["#e7e8ec", "#b1adad"]}, | |
{name: "Venus", tilt: 2.64, radius: 6051.8, period: -243, colours: ["#f8e2b0", "#d3a567"]}, | |
{name: "Earth", tilt: 23.44, radius: 6371, period: 1, colours: ["#9fc164", "#6b93d6"]}, | |
{name: "Mars", tilt: 6.68, radius: 3389.5, period: 1.03, colours: ["#ef1501", "#ad0000"]}, | |
{name: "Jupiter", tilt: 25.19, radius: 69911, period: 0.41, colours: ["#d8ca9d", " #a59186"]}, | |
{name: "Saturn", tilt: 26.73, radius: 58232, period: 0.44, colours: ["#f4d587", "#f4a587"]}, | |
{name: "Uranus", tilt: 82.23, radius: 25362, period: -0.72, colours: ["#e1eeee", "#adb0c3"]}, | |
{name: "Neptune", tilt: 28.32, radius: 24622, period: 0.72, colours: ["#85addb", " #3f54ba"]} | |
]; | |
var definitions = d3.select("svg").append("defs"); | |
var filter = definitions.append("filter") | |
.attr("id", "glow"); | |
filter.append("feGaussianBlur") | |
.attr("class", "blur") | |
.attr("stdDeviation", config.glowRadius) | |
.attr("result","coloredBlur"); | |
var feMerge = filter.append("feMerge") | |
feMerge.append("feMergeNode") | |
.attr("in","coloredBlur"); | |
feMerge.append("feMergeNode") | |
.attr("in","SourceGraphic"); | |
function generateStars(number) { | |
var stars = starArea.selectAll("circle") | |
.data(d3.range(number).map(d => | |
i = {x: Math.random() * (width + margin.left + margin.right), y: Math.random() * (height + margin.top + margin.bottom), r: Math.random() * config.starRadius} | |
)) | |
.enter().append("circle") | |
.attr("class", "star") | |
.attr("cx", d => d.x) | |
.attr("cy", d => d.y) | |
.attr("r", d => d.r); | |
} | |
function displayPlanets(cfg, planets) { | |
var boundingSize = (width / planets.length) - cfg.padding; | |
var boundingArea = svg.append("g") | |
.selectAll("g") | |
.data(planets) | |
.enter().append("g") | |
.attr("transform", (d, i) => "translate(" + [i * (boundingSize + cfg.padding), height / 2] + ")") | |
.on("mouseover", showInfo) | |
.on("mouseout", hideInfo); | |
var boundingRect = boundingArea.append("rect") | |
.attr("class", "bounding-box") | |
.attr("y", -boundingSize / 2) | |
.attr("width", boundingSize) | |
.attr("height", boundingSize); | |
var info = boundingArea.append("g") | |
.attr("transform", "translate(" + [0, (boundingSize / 2) + 18] + ")") | |
.attr("class", "info") | |
.style("opacity", 0); | |
info.append("text") | |
.text(d => "Radius: " + d.radius + "km"); | |
info.append("text") | |
.attr("y", 12) | |
.text(d => "Tilt: " + d.tilt + "°"); | |
info.append("text") | |
.attr("y", 24) | |
.text(d => "Day Length: " + d.period); | |
var labels = boundingArea.append("text") | |
.attr("class", "label") | |
.attr("y", -boundingSize / 2) | |
.attr("dy", -12) | |
.text(d => d.name); | |
var radiusScale = d3.scaleLinear() | |
.domain([0, d3.max(planets, d => d.radius)]) | |
.range([0, (boundingSize / 2) - 3]); | |
var graticuleScale = d3.scaleLinear() | |
.domain(d3.extent(planets, d => d.radius)) | |
.range([20, 10]); | |
var planets = boundingArea.each(function(d) { | |
var x = d3.select(this); | |
drawPlanet(x, d); | |
}); | |
function drawPlanet(element, data) { | |
var rotation = [0, 0, data.tilt]; | |
var projection = d3.geoOrthographic() | |
.translate([0, 0]) | |
.scale(radiusScale(data.radius)) | |
.clipAngle(90) | |
.precision(0.1); | |
var path = d3.geoPath() | |
.projection(projection); | |
var graticule = d3.geoGraticule(); | |
var planet = element.append("g") | |
.attr("class", "planet") | |
.attr("transform", "translate(" + [boundingSize / 2, 0] + ")"); | |
var defs = d3.select("svg").select("defs"); | |
var gradient = defs.append("radialGradient") | |
.attr("id", "gradient" + data.name) | |
.attr("cx", "25%") | |
.attr("cy", "25%"); | |
// The offset at which the gradient starts | |
gradient.append("stop") | |
.attr("offset", "5%") | |
.attr("stop-color", data.colours[0]); | |
// The offset at which the gradient ends | |
gradient.append("stop") | |
.attr("offset", "100%") | |
.attr("stop-color", data.colours[1]); | |
var axis = planet.append("line") | |
.attr("class", "axis-line") | |
.attr("x1", -radiusScale(data.radius) * cfg.axisMultiplier) | |
.attr("x2", radiusScale(data.radius) * cfg.axisMultiplier) | |
.attr("transform", "rotate(" + (90 - data.tilt) + ")"); | |
var fill = planet.append("circle") | |
.attr("r", radiusScale(data.radius)) | |
.style("fill", "url(#gradient" + data.name + ")") | |
.style("filter", "url(#glow)"); | |
var gridLines = planet.append("path") | |
.attr("class", "graticule") | |
.datum(graticule.step([graticuleScale(data.radius), graticuleScale(data.radius)])) | |
.attr("d", path); | |
d3.timer(function(elapsed) { | |
// Rotate projection | |
projection.rotate([rotation[0] + elapsed * cfg.velocity[0] / data.period, rotation[1] + elapsed * cfg.velocity[1] / data.period, rotation[2]]); | |
// Redraw gridlines | |
gridLines.attr("d", path); | |
}) | |
} | |
} | |
function showInfo(d) { | |
d3.select(this).select("g.info") | |
.transition() | |
.style("opacity", 1); | |
} | |
function hideInfo(d) { | |
d3.select(this).select("g.info") | |
.transition() | |
.style("opacity", 0); | |
} | |
generateStars(500); | |
displayPlanets(config, solar); | |
starArea.lower(); | |
</script> | |
</body> |