Home Examples Documentation Playground Source

Stardust Example: SandDance

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

index.html

<!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>