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> |