Published
Edited
Mar 22, 2022
Insert cell
Insert cell
<div id="viz">
<h2>the Planets in Motion &mdash; making things move</h2>
<div>
<button id="btn-showhide">show/hide orbits</button>
<button id="btn-actualsize">actual planet sizes</button>
<button id="btn-wrongsize">wrong sizes</button>
</div>

<svg id="solarsystem" width="1000" height="1000" viewbox="0 0 1000 1000">
<style>
#viz {background-color: #1a1a1a; text-align: center; color: #eeeeee; font-family: Arial, Helvetica, sans-serif; padding: 20px;}
.planet {fill: white;}
.sun {fill: yellow;}
.orbit {fill: none; stroke: #aaa; stroke-width: 0.5; stroke-dasharray: 3;}
#orbits {visibility: hidden;}
button {font-size: 10px; background-color: transparent; border: 1px solid #333333; color: #aaa; border-radius: 6px; padding: 6px}
button:hover {cursor: pointer;}
#notes, #notes a {font-size: 8px; color: #777}
</style>
<g id="container">
<g id="orbits"></g>
<g id="planets"></g>
</g>
</svg>

<div id="notes">
orbits are abstracted into circles for simplicity.<br>
the sun is shown very small and completely incorrectly in size, so as to see the planets.<br>
planet relative sizes are correct, but their size relative to their orbits is far from correct. They would be too tiny to see.<br>
rotational movements and time scales are correct, and is the primary purpose of this visual.<br>
<br>
concept based on, and working to demonstrate, Jonathan Corum / NY Times' <a href="https://archive.nytimes.com/www.nytimes.com/interactive/science/space/keplers-tally-of-planets.html">Kepler's Tally of Planets</a>.
</div>
</div>
Insert cell
md`### an SVG container object ^^
within an HTML DIV framework can be setup first to give you a graphics space.
Notice that it has two layers inside of a container, one for orbits and the other for planets. These layers are <g> (group) elements, and are used later to target things specifically within them, while not getting things that are not in them.

**Note:** it is critical to name this cell in observable so it can be referenced later. Click on the cell, then see the name in the panel at the bottom of your Observable interface. This is named planetsinmotion`
Insert cell
Insert cell
data = FileAttachment("planets.csv").csv()
Insert cell
toNum = (text) => Number(text.replace(/,/g,''));
// this function cleans up numbers as "text" from the CSV, removes thousands ,s and makes a number
Insert cell
data.forEach((d) => {
d.distance = toNum(d.distance); // now we use toNum to clean each data field
d.radius = toNum(d.radius);
d.orbit_time = toNum(d.orbit_time);
d.orbit_ratio = toNum(d.orbit_ratio);
d.orbit_distance = toNum(d.orbit_distance);
});
Insert cell
Insert cell
viz = d3.select(planetsinmotion).select("#solarsystem") // use d3 to grab the base SVG element to work on.
Insert cell
vizWidth = viz.attr("width") // get it's width
Insert cell
vizHeight = viz.attr("width") // get it's height
Insert cell
center = {
return {"x":vizWidth/2, "y":vizHeight/2}; // an x,y pair of where the solarsystem center is on screen
}
Insert cell
Insert cell
scaleOrbit = d3.scaleLinear().domain([0,d3.max(data, d=>d.distance)]).range([0,vizWidth/2+100]);
Insert cell
scalePlanetRadius = 1/10000; // just a visual scale.
Insert cell
Insert cell
orbits = viz.select("#orbits").selectAll(".orbit")
.data(data)
.join("circle")
.attr("cx", center.x)
.attr("cy", center.y)
.attr("r", d => scaleOrbit(d.distance))
.classed("orbit",true);
Insert cell
planets = viz.select("#planets").selectAll("circle")
.data(data)
.join("circle")
.attr("cx", d => center.x + scaleOrbit(d.distance)) // distance from the sun, in the data.
.attr("cy",center.y)
.attr("r", d => d.radius*scalePlanetRadius) // the planet sizes are way overscaled for visibility. Ratios are correct.
//.attr("r", d => scaleOrbit(d.radius)) // this would be the right way. uncomment and try it. See if you can see them.
.classed("planet",true);
Insert cell
planets.filter(d=>d.name=="Sun")
.classed("sun",true)
.attr("r",3); // Override the sun. I need to waaaaaay underscale it to see the other planets.
Insert cell
Insert cell
orbitTurn = -3 // set the speed and direction of orbit as a variable (actually a constant)
Insert cell
{
let frameOrbit;
function orbit() {
planets.filter(d=>d.name != "Sun") // find all of them other than the sun (!= "Sun")
.attr("transform", d => "rotate(" + frameOrbit / d.orbit_ratio * orbitTurn +" "+center.x+" "+center.y+")"); // change rotation
frameOrbit = requestAnimationFrame(orbit);
}
invalidation.then(() => cancelAnimationFrame(frameOrbit));
frameOrbit = requestAnimationFrame(orbit);
}
Insert cell
Insert cell
function showHideOrbits() {
let orbitLayer = viz.select("#orbits");
if (orbitLayer.style("visibility") == "hidden") {orbitLayer.style("visibility","visible")} // visibility style property of svg object
else {orbitLayer.style("visibility","hidden")}
}
Insert cell
function actualPlanetSizes () {
planets.attr("r", d => scaleOrbit(d.radius)); // use the correct radius from the data
}
Insert cell
function wrongPlanetSizes () {
planets.attr("r", d => d.radius*scalePlanetRadius); // scale the actual radius by a factor for (overscaled) visibility
planets.filter(d=>d.name=="Sun").attr("r",3); // and override the sun to a static radius value
}
Insert cell
{
let ui = d3.select(planetsinmotion); // grab the main div (the viz container) to be able to work on.
ui.select("#btn-showhide").on("click",showHideOrbits); // and assign the button to run the function when it is clicked.
ui.select("#btn-actualsize").on("click",actualPlanetSizes);
ui.select("#btn-wrongsize").on("click",wrongPlanetSizes);
}
Insert cell
Insert cell
function zoomed({transform}) {d3.select("#container").attr("transform", transform);}
Insert cell
viz.call(d3.zoom()
.extent([[0,0],[vizWidth,vizHeight]])
.scaleExtent([1,20])
.on("zoom", zoomed));
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more