Producing a SandDance visualization with Stardust. Instance-based visualization with animation between 3 layouts: scatterplot of longitude and latitude, group by state, and group by longitude and latitude in 3D.
Data: DemoVote from census.gov and Guardian
<!DOCTYPE html>
<meta charset="utf-8" />
<title>Stardust Example: Sanddance</title>
<link rel="stylesheet" href="../common/style.css" type="text/css" />
<canvas id="main-canvas"></canvas>
<div data-switch="mode">
<button class="active" data-value="mode1">Map</button><button data-value="mode3">Map Bins</button
><button data-value="mode2">State</button>
<div class="fps"></div>
</div>
<div class="initializing"><p>Initializing...</p></div>
<script src="//d3js.org/d3.v3.min.js" type="text/javascript"></script>
<script src="../common/stardust/stardust.bundle.js" type="text/javascript"></script>
<script src="../common/utils.js" type="text/javascript"></script>
<script type="text/javascript">
let width = 960;
let height = 470;
let canvas = document.getElementById("main-canvas");
let platform = Stardust.platform("webgl-2d", canvas, width, height);
platform.set3DView(Math.PI / 2, width / height);
platform.setPose(new Stardust.Pose(new Stardust.Vector3(0, 0, 200), new Stardust.Quaternion(0, 0, 0, 1)));
platform.clear([1, 1, 1, 1]);
platform.setCameraPosition(Stardust.Vector3(0, 0, 200));
loadData("demovoteclean.tsv", data => {
let demovote = data;
let mark = Stardust.mark.compile(`
import { Cube } from P3D;
let longitude: float;
let latitude: float;
let state: float;
let stateBinIndex: float;
let xBin: float;
let yBin: float;
let xyBinIndex: float;
let index: float;
function getPositionScatterplot(): Vector3 {
let scaleX = 0.2;
let scaleY = 0.3;
return Vector3(
scaleX * (longitude - (-95.9386152570054)),
scaleY * (latitude - (37.139536624928695)),
0
);
}
function getPositionStateBins(): Vector3 {
return Vector3(
(state - 48 / 2) * 0.3 + (stateBinIndex % 10 - 4.5) * 0.02,
floor(stateBinIndex / 10) * 0.02 - 2.0, 0
);
}
function getPositionXYBinning(): Vector3 {
let n = 6;
let txy = xyBinIndex % (n * n);
let tx = txy % n;
let ty = floor(txy / n);
let tz = floor(xyBinIndex / (n * n));
return Vector3(
(xBin - 9 / 2) * 0.6 + (tx - n / 2 + 0.5) * 0.04,
tz * 0.04 - 2.0,
(yBin - 6 / 2) * 0.6 + (ty - n / 2 + 0.5) * 0.04
);
}
function clamp01(t: float): float {
if(t < 0) t = 0;
if(t > 1) t = 1;
return t;
}
mark Mark(color: Color, t1: float, t2: float, t3: float, ki1: float, ki2: float, ki3: float) {
let p1 = getPositionScatterplot();
let p2 = getPositionStateBins();
let p3 = getPositionXYBinning();
let p = p1 * clamp01(t1 + ki1 * index) +
p2 * clamp01(t2 + ki2 * index) +
p3 * clamp01(t3 + ki3 * index);
Cube(
p * 50,
0.7,
color
);
}
`)["Mark"];
let marks = Stardust.mark.create(mark, Stardust.shader.lighting(), platform);
demovote.forEach(d => {
d.Longitude = +d.Longitude;
d.Latitude = +d.Latitude;
});
let longitudeExtent = d3.extent(demovote, d => d.Longitude);
let latitudeExtent = d3.extent(demovote, d => d.Latitude);
let longitudeScale = d3.scale
.linear()
.domain(longitudeExtent)
.range([0, 1]);
let latitudeScale = d3.scale
.linear()
.domain(latitudeExtent)
.range([0, 1]);
// Map states to integer.
let states = new Set();
let state2number = {};
let state2count = {};
demovote.forEach(d => states.add(d.StateAbb));
states = Array.from(states);
states.sort();
states.forEach((d, i) => {
state2number[d] = i;
state2count[d] = 0;
});
let xyBinCounter = {};
let xBinCount = 10;
let yBinCount = 7;
demovote.sort((a, b) => a.Obama - b.Obama);
demovote.forEach((d, i) => {
d.index = i;
if (state2count[d.StateAbb] == null) state2count[d.StateAbb] = 0;
d.stateBinIndex = state2count[d.StateAbb]++;
let xBin = Math.floor(longitudeScale(d.Longitude) * xBinCount);
let yBin = Math.floor(latitudeScale(d.Latitude) * yBinCount);
let bin = yBin * (xBinCount + 1) + xBin;
d.xBin = xBin;
d.yBin = yBin;
if (xyBinCounter[bin] == null) xyBinCounter[bin] = 0;
d.xyBinIndex = xyBinCounter[bin]++;
});
let s1 = d3.interpolateLab("#f7f7f7", "#0571b0");
let s2 = d3.interpolateLab("#f7f7f7", "#ca0020");
let strToRGBA = str => {
let rgb = d3.rgb(str);
return [rgb.r / 255, rgb.g / 255, rgb.b / 255, 1];
};
let scaleColor = value => {
if (value > 0.5) {
return strToRGBA(s1((value - 0.5) * 2));
} else {
return strToRGBA(s2((0.5 - value) * 2));
}
};
marks
.attr("index", d => d.index / (demovote.length - 1))
.attr("longitude", d => d.Longitude)
.attr("latitude", d => d.Latitude)
.attr("state", d => state2number[d.StateAbb])
.attr("stateBinIndex", d => d.stateBinIndex)
.attr("xBin", d => d.xBin)
.attr("yBin", d => d.yBin)
.attr("xyBinIndex", d => d.xyBinIndex)
.attr("color", d => scaleColor(d.Obama));
let skewing = 1;
function transition12(t) {
let tt = t * (1 + skewing) - skewing;
marks
.attr("t1", 1 - tt)
.attr("t2", tt)
.attr("t3", 0)
.attr("ki1", -skewing)
.attr("ki2", +skewing)
.attr("ki3", 0);
}
function transition23(t) {
let tt = t * (1 + skewing) - skewing;
marks
.attr("t1", 0)
.attr("t2", 1 - tt)
.attr("t3", tt)
.attr("ki1", 0)
.attr("ki2", -skewing)
.attr("ki3", +skewing);
}
function transition31(t) {
let tt = t * (1 + skewing) - skewing;
marks
.attr("t1", tt)
.attr("t2", 0)
.attr("t3", 1 - tt)
.attr("ki1", +skewing)
.attr("ki2", 0)
.attr("ki3", -skewing);
}
marks.data(demovote);
function render() {
platform.clear([1, 1, 1, 1]);
marks.render();
}
transition12(0);
render();
var transitions = {
mode1mode2: t => transition12(t),
mode2mode1: t => transition12(1 - t),
mode2mode3: t => transition23(t),
mode3mode2: t => transition23(1 - t),
mode3mode1: t => transition31(t),
mode1mode3: t => transition31(1 - t)
};
switches.mode_changed = (newMode, previousMode) => {
beginTransition(t => {
transitions[previousMode + newMode](t);
render();
});
};
});
</script>