Skip to content

Instantly share code, notes, and snippets.

@bryik bryik/.block
Last active Jun 29, 2017

Embed
What would you like to do?
Planetary Scaling (Linear vs Log)
license: mit
// Transpiled with Babel
"use strict";
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var display = d3.select("#display");
var label = d3.select("#label");
// STARTUP (parse CSV)
d3.csv("planet-data.csv", row, start);
function row(d) {
return {
name: d.Body,
radius: +d["Mean radius (km)"]
};
}
function start(d) {
var planets = new planetDisplay(d);
planets.initDisplay();
// On click, transition to other scale
d3.select(document).on("click", planets.updateDisplay.bind(planets));
}
// TODO: use scale instead of radius (more efficient transitions)
// also clean up this mess...
var planetDisplay = function () {
function planetDisplay(data) {
_classCallCheck(this, planetDisplay);
this.data = data;
this.xSum = 0;
this.spacing = 2;
this.currentScale = "linear";
this.radiusData = data.map(function (datum) {
return datum.radius;
});
this.rScale = d3.scaleLinear().domain(d3.extent(this.radiusData)).range([0.1, 1.5]);
}
// Changes scale to linear (if currentScale is log), or to log (if currentScale is linear)
_createClass(planetDisplay, [{
key: "toggleScale",
value: function toggleScale() {
if (this.currentScale === "linear") {
this.rScale = d3.scaleLog().domain(d3.extent(this.radiusData)).range([0.1, 1.5]);
this.currentScale = "log";
} else {
this.rScale = d3.scaleLinear().domain(d3.extent(this.radiusData)).range([0.1, 1.5]);
this.currentScale = "linear";
}
label.attr("value", this.currentScale);
}
// Calculates x-coordinate of a sphere given radius and index
}, {
key: "positioner",
value: function positioner(d, i) {
var currentRadius = this.rScale(d.radius);
var previousRadius;
if (i === 0) {
previousRadius = 0;
} else {
previousRadius = this.rScale(this.data[i - 1].radius);
}
// (previousRadius + currentRadius) has spheres touching, (spacing) ensures separation
this.xSum += previousRadius + currentRadius + this.spacing;
var x = this.xSum;
var y = currentRadius;
var z = 0;
return x + " " + y + " " + z;
}
}, {
key: "centerDisplay",
value: function centerDisplay() {
var _this = this;
display.attr("position", function () {
var x = -(_this.xSum / 2) - 2;
var y = 0;
var z = -7;
return x + " " + y + " " + z;
});
}
}, {
key: "initDisplay",
value: function initDisplay() {
var _this2 = this;
display.selectAll("a-sphere").data(this.data).enter().append("a-sphere").attr("position", this.positioner.bind(this)).attr("src", function (d) {
return "#" + d.name.toLowerCase();
}
//.attr("radius", (d) => this.rScale(d.radius))
// Radius is encoded in scale (for performance)
).attr("scale", function (d) {
var radius = _this2.rScale(d.radius);
return radius + " " + radius + " " + radius;
});
this.centerDisplay();
}
}, {
key: "updateDisplay",
value: function updateDisplay() {
this.xSum = 0;
this.toggleScale();
var self = this;
display.selectAll("a-sphere").data(this.data).attr("position", this.positioner.bind(this));
display.selectAll("a-sphere").data(this.data).transition().duration(2000
// .attrTween("radius", function (d, i) {
// var el = d3.select(this);
// var oldRadius = AFRAME.utils.coordinates.stringify(el.attr("radius"));
// return d3.interpolate(oldRadius, self.rScale(d.radius));
// })
).attrTween("scale", function (d, i) {
var el = d3.select(this);
var oldScale = AFRAME.utils.coordinates.stringify(el.attr("scale"));
var newScale = self.rScale(d.radius);
newScale = newScale + " " + newScale + " " + newScale;
return d3.interpolate(oldScale, newScale);
});
this.centerDisplay();
}
}]);
return planetDisplay;
}();

Click the screen to switch between log and linear scales.

Comparing the sizes of planets in our Solar System is difficult because of the difference in magnitude between the smallest (Mercury, radius 2439.7 km) and the largest (Jupiter, radius 71,492 km). Linear scaling tends to compress small and similar values concealing differences in relative size. If I wanted to add the Sun (radius 695,700 km) or stars (e.g. VY Canis Majoris, radius 1.6 billion km) the situation would be even worse.

Logarithmic scales are a nice alternative to linear scales as they preserve relative differences (with linear scaling, Mercury and Mars appear nearly the same size as Venus and Earth). However, one might say they reduce appreciation for the great gulf in size between giants like Jupiter and our relatively tiny Earth.

Notes

It may take a couple seconds for textures to appear. If performance is bad, try this stripped down version.

Initially, I transitioned radius directly but this is generally a bad idea because a new sphere geometry is created each time. Better performance can be had by using the scale property to encode radius (scaling is cheap).

References

Star and planet radius data collected from Wolfram Alpha.

Planetary textures downloaded from Celestia Motherlode.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Linear vs Log Planet Scaling</title>
<meta name="description" content="log-linear-scale-comparison">
<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<a-scene antialias="true">
<a-assets>
<img id="groundTexture" src="https://cdn.aframe.io/a-painter/images/floor.jpg">
<img id="skyTexture" src="https://cdn.aframe.io/a-painter/images/sky.jpg">
<!-- Planet textures -->
<img id="earth" src="https://cdn.rawgit.com/bryik/Assets/eb18a301/images/planet_textures/earth.jpg">
<img id="mercury" src="https://cdn.rawgit.com/bryik/Assets/eb18a301/images/planet_textures/mercury.jpg">
<img id="venus" src="https://cdn.rawgit.com/bryik/Assets/eb18a301/images/planet_textures/venus.jpg">
<img id="mars" src="https://cdn.rawgit.com/bryik/Assets/eb18a301/images/planet_textures/mars.jpg">
<img id="neptune" src="https://cdn.rawgit.com/bryik/Assets/eb18a301/images/planet_textures/neptune.jpg">
<img id="uranus" src="https://cdn.rawgit.com/bryik/Assets/eb18a301/images/planet_textures/uranus.png">
<img id="saturn" src="https://cdn.rawgit.com/bryik/Assets/eb18a301/images/planet_textures/saturn.png">
<img id="jupiter" src="https://cdn.rawgit.com/bryik/Assets/eb18a301/images/planet_textures/jupiter.jpg">
</a-assets>
<!-- Planets will be appended to this element. -->
<a-entity id="display"></a-entity>
<!-- What scale is being used? -->
<a-text id="label" position="-1.728 2.195 3.730" value="linear"></a-text>
<!-- Camera -->
<a-entity position="0 1.6 6">
<a-entity camera="fov: 60" look-controls wasd-controls></a-entity>
</a-entity>
<!-- Background sky. -->
<a-sky height="2048" radius="30" src="#skyTexture" theta-length="90" width="2048"></a-sky>
<!-- Ground. -->
<a-circle src="#groundTexture" rotation="-90 0 0" radius="32"></a-circle>
</a-scene>
<script src=".hidden.babel-visualization.js"></script>
</body>
</html>
Body Mean radius (km)
Mercury 2439.7
Mars 3396.2
Venus 6051.9
Earth 6378.1
Neptune 24764
Uranus 25559
Saturn 60268
Jupiter 71492
// Raw ES6. Block is running a hidden transpiled version.
const display = d3.select("#display");
const label = d3.select("#label");
// STARTUP (parse CSV)
d3.csv("planet-data.csv", row, start);
function row (d) {
return {
name: d.Body,
radius: +d["Mean radius (km)"]
};
}
function start (d) {
var planets = new planetDisplay(d);
planets.initDisplay();
// On click, transition to other scale
d3.select(document).on("click", planets.updateDisplay.bind(planets));
}
class planetDisplay {
constructor (data) {
this.data = data;
this.xSum = 0;
this.spacing = 2;
this.currentScale = "linear";
this.radiusData = data.map((datum) => { return datum.radius; });
this.rScale = d3.scaleLinear()
.domain(d3.extent(this.radiusData))
.range([0.1, 1.5]);
}
// Changes scale to linear (if currentScale is log), or to log (if currentScale is linear)
toggleScale () {
if (this.currentScale === "linear") {
this.rScale = d3.scaleLog()
.domain(d3.extent(this.radiusData))
.range([0.1, 1.5]);
this.currentScale = "log";
} else {
this.rScale = d3.scaleLinear()
.domain(d3.extent(this.radiusData))
.range([0.1, 1.5]);
this.currentScale = "linear";
}
label.attr("value", this.currentScale);
}
// Calculates x-coordinate of a sphere given radius and index
positioner (d, i) {
var currentRadius = this.rScale(d.radius);
var previousRadius;
if (i === 0) {
previousRadius = 0;
} else {
previousRadius = this.rScale(this.data[i-1].radius);
}
// (previousRadius + currentRadius) has spheres touching, (spacing) ensures separation
this.xSum += previousRadius + currentRadius + this.spacing;
var x = this.xSum;
var y = currentRadius;
var z = 0;
return x + " " + y + " " + z;
}
centerDisplay () {
display.attr("position", () => {
var x = -(this.xSum / 2) - 2;
var y = 0;
var z = -7;
return x + " " + y + " " + z;
});
}
initDisplay () {
display.selectAll("a-sphere")
.data(this.data)
.enter()
.append("a-sphere")
.attr("position", this.positioner.bind(this))
.attr("src", (d) => "#" + d.name.toLowerCase())
//.attr("radius", (d) => this.rScale(d.radius))
// Radius is encoded in scale (for performance)
.attr("scale", (d) => {
var radius = this.rScale(d.radius);
return radius + " " + radius + " " + radius;
})
this.centerDisplay();
}
updateDisplay () {
this.xSum = 0;
this.toggleScale();
var self = this;
display.selectAll("a-sphere")
.data(this.data)
.attr("position", this.positioner.bind(this));
display.selectAll("a-sphere")
.data(this.data)
.transition()
.duration(2000)
// .attrTween("radius", function (d, i) {
// var el = d3.select(this);
// var oldRadius = AFRAME.utils.coordinates.stringify(el.attr("radius"));
// return d3.interpolate(oldRadius, self.rScale(d.radius));
// })
.attrTween("scale", function (d, i) {
var el = d3.select(this);
var oldScale = AFRAME.utils.coordinates.stringify(el.attr("scale"));
var newScale = self.rScale(d.radius);
newScale = newScale + " " + newScale + " " + newScale;
return d3.interpolate(oldScale, newScale);
});
this.centerDisplay();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.