function solarSystemPlot(
orbits,
{
seed = 0.42,
width = 400,
height = 400,
simulate = false,
showLabels = true,
animate = false,
updateMilliseconds = 50,
minSpeed = 2,
maxSpeed = 4,
margin = { top: 50, right: 80, bottom: 50, left: 50 },
nudge = { x: 0.3, y: -0.25 },
font = {
family: "Inter",
size: "14px",
weight: "700"
}
} = {}
) {
let planetPositions = randomizePlanetPositions(orbits, seed);
const maxRadius = d3.max(orbits.map((d) => d.radius));
const scaleX = d3
.scaleLinear()
.domain([-maxRadius, maxRadius])
.range([-width / 2, width / 2]);
const scaleY = d3
.scaleLinear()
.domain([-maxRadius, maxRadius])
.range([-height / 2, height / 2]);
const domSvg = html`<svg></svg>`;
const svg = d3
.select(domSvg)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr(
"transform",
`translate(${width / 2 + margin.left}, ${height / 2 + margin.top})`
);
function adjustX(val) {
return val + nudge.x;
}
function adjustY(val) {
return val + nudge.y;
}
const orbitRings = svg
.selectAll("circle.orbit")
.data(orbits)
.enter()
.append("circle")
.attr("class", "orbit")
.attr("r", (d) => scaleX(d.radius))
.attr("cx", 0)
.attr("cy", 0);
const planets = svg
.selectAll("circle.planet")
.data(planetPositions)
.enter()
.append("circle")
.attr("class", "planet")
.attr("r", 6)
.attr("cx", (d) => scaleX(d.x))
.attr("cy", (d) => scaleY(d.y));
if (animate) {
function generateRandomSpeeds(planetNames) {
const speeds = {};
planetNames.forEach((planet) => {
speeds[planet] = Math.random() * (maxSpeed - minSpeed) + minSpeed;
});
return speeds;
}
let planetSpeeds = generateRandomSpeeds(
planetPositions.map((d) => d.planet)
);
function updatePlanetPositions(orbits, elapsedTime) {
const updatedPositions = orbits.map((orbit) => {
const speed = planetSpeeds[orbit.planet] || 1;
const angle = (elapsedTime * speed * Math.PI) / 180;
const x = orbit.radius * Math.cos(angle);
const y = orbit.radius * Math.sin(angle);
return { ...orbit, x, y };
});
return updatedPositions;
}
function updateVisualization() {
const elapsedTime = Date.now() / 1000;
planetPositions = updatePlanetPositions(orbits, elapsedTime);
planets
.data(planetPositions)
.attr("cx", (d) => scaleX(d.x))
.attr("cy", (d) => scaleY(d.y));
}
d3.interval(() => {
updateVisualization();
}, updateMilliseconds);
showLabels = false;
}
if (showLabels) {
const labels = svg
.selectAll("text")
.data(planetPositions)
.enter()
.append("text")
.text((d) => d.planet)
.attr("x", (d) => scaleX(adjustX(d.x)))
.attr("y", (d) => scaleY(adjustY(d.y)))
.attr("class", "planet-label")
.attr("text-anchor", "start");
if (simulate) {
function ticked() {
labels.attr("x", (d) => d.x).attr("y", (d) => d.y);
}
const simulation = d3
.forceSimulation(planetPositions)
.force("collision", d3.forceCollide(12)) // Prevents label overlap
.force("x", d3.forceX((d) => scaleX(adjustX(d.x))).strength(0.1))
.force("y", d3.forceY((d) => scaleY(adjustY(d.y))).strength(0.1))
.on("tick", ticked);
}
}
return domSvg;
}