Published
Edited
Jan 4, 2020
2 stars
Insert cell
Insert cell
viewof timer = timerComponent
Insert cell
{
const width = 1000;
const height = 1000;
const svg = d3.select(DOM.svg(width, height))
.style("overflow", "visible")
.style("background-color", "black");

const solarSystem = new SolarSystem();
solarSystem.run({svg, height, width, timer});

return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class SolarSystem {
run({svg, height, width, timer}) {
const radius = planetSize;
this._bodies = [
// sun
new Body({radius, translationRadius: 0, translationPeriodInSecs: 0, color: '#ffe942'}),
// mercury
new Body({radius, translationRadius: 35, translationPeriodInSecs: 88, color: '#8a8781'}),
// venus
new Body({radius, translationRadius: 67, translationPeriodInSecs: 225, color: '#edce8c'}),
// earth
new Body({radius, translationRadius: 92, translationPeriodInSecs: 365, color: '#5279c7'}),
// mars
new Body({radius, translationRadius: 141, translationPeriodInSecs: 687, color: '#d9721e'}),
// jupiter
new Body({radius, translationRadius: 483, translationPeriodInSecs: 4300, color: '#91543c'}),
// saturn
new Body({radius, translationRadius: 890, translationPeriodInSecs: 11000, color: '#87794c'}),
// uranus
new Body({radius, translationRadius: 1784, translationPeriodInSecs: 31000, color: '#7d9cb0'}),
]
const orbitsContainer = svg.append("g");
this.renderOrbits({orbitsContainer, height, width});

const bodiesContainer = svg.append("g");
this.renderBodies({bodiesContainer, height, width, timer});
}
renderOrbits({orbitsContainer, height, width}) {
const scaleFactor = this.getDistanceScaleFactor({height, width});
const data = this._bodies.map(body => {
const radius = body.getTranslationRadius();
return {r: radius * scaleFactor};
});

const offsetY = height / 2;
const offsetX = width / 2;

const circle = orbitsContainer.selectAll("circle").data(data);
circle.enter().append("circle")
.attr("r", row => row.r)
.attr("cx", row => offsetX)
.attr("cy", row => offsetY)
.attr("fill", 'none')
.attr("stroke", "white")
.attr("stroke-opacity", "0.5")
.attr("stroke-dasharray", "1 4");
}

getDistanceScaleFactor({height, width}) {
// Scale radius to fit on screen
const maxTranslationRadius = Math.max(
...this._bodies.map(body => body.getTranslationRadius())
);
// Multiply by 0.9 to add some margin
return Math.min(height, width)*0.9 / (2.0 * maxTranslationRadius);
}
renderBodies(params) {
const {bodiesContainer, height, width, timer} = params;
const scaleFactor = this.getDistanceScaleFactor({height, width});
// Scale time so that 1 min elapsed is a full orbit of the slowest planet.
const maxPeriod = Math.max(
...this._bodies.map(body => body.getPeriod())
);
const periodScaleFactor = maxPeriod / 10000.0;
const offsetY = height / 2;
const offsetX = width / 2;

const data = this._bodies.map(body => {
const {x, y} = body.getPosition(timer * periodScaleFactor);
const radius = body.getRadius();
const pixelY = y * scaleFactor + offsetY;
const pixelX = x * scaleFactor + offsetX;
return {r: body.getRadius(), cx: pixelX, cy: pixelY, color: body.getColor()};
});
const circle = bodiesContainer.selectAll("circle").data(data);
// Adding new circles
circle.enter().append("circle")
.attr("r", row => row.r)
.attr("cx", row => row.cx)
.attr("cy", row => row.cy)
.attr("fill", row => row.color);
// Updating
circle.transition()
.attr("cx", row => row.cx)
.attr("cy", row => row.cy);
}
};
Insert cell
class Body {
constructor({
color, radius,
translationRadius, // Distance from the anchor
translationPeriodInSecs, // Time to go around the anchor
}) {
this._color = color;
this._radius = radius;
this._translationRadius = translationRadius;
this._translationPeriodInSecs = translationPeriodInSecs;
}
getTranslationRadius() {
return this._translationRadius;
}

getColor() {
return this._color;
}

getPeriod() {
return this._translationPeriodInSecs;
}

getRadius() {
return this._radius;
}
getPosition(timestampInSecs) {
const radius = this._translationRadius;
if (radius === 0) {
return {x: 0, y: 0};
}
const translationPeriodInSecs = this._translationPeriodInSecs;
const timeInCurrentRotationInSecs = timestampInSecs % translationPeriodInSecs;
const angle = (2 * Math.PI * timeInCurrentRotationInSecs) / translationPeriodInSecs;
const x = Math.sin(angle) * radius;
const y = Math.cos(angle) * radius;
return {x, y};
}
}
Insert cell
Insert cell
import {timerComponent} from "@kunigami/animation-utils"
Insert cell
d3 = require("d3@5")
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