block by armollica a6b26f0f0abad5505e08

2D/3D Scatterplot

Full Screen

Scrolling moves the z-axis. Points at the current z-position will be more visible than those far away from this z-position.

I was curious whether this kind of thing would be useful for exploring the correlation of three continuous variables at once. I don’t think it works all that well. Then again I haven’t tried it out on real data yet so who knows.

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
    <style>
      .axis {
        font: 12px sans-serif;
      }

      .axis path,
      .axis line {
        fill: none;
        stroke: #000;
        stroke-width: 1px;
      }
    </style>
  </head>
  <body>
    <script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
    <script>
      var margin = { top: 10, left: 170, bottom: 200, right: 300 },
          width = 960 - margin.left - margin.right,
          height = 600 - margin.top - margin.bottom;

      var scale = {
        x: d3.scale.linear().range([0, width]).nice(),
        y: d3.scale.linear().range([height, 0]).nice(),
        z: d3.scale.linear().range([0, width/2]).nice(),
        fill: d3.scale.sqrt().range(["#fff", "#000", "#fff"])
                .interpolate(d3.interpolateLab),
        opacity: d3.scale.sqrt().range([0.1, 1, 0.1]),
        r: d3.scale.sqrt().range([1, 5, 1])
      };

      var axis = {
        x: d3.svg.axis().scale(scale.x).orient("bottom").ticks(5),
        y: d3.svg.axis().scale(scale.y).orient("right").ticks(5),
        z: d3.svg.axis().scale(scale.z).orient("bottom").ticks(5)
      };

      var svg = d3.select("body").append("svg")
          .attr("width", width + margin.left + margin.right)
          .attr("height", height + margin.top + margin.bottom)
        .append("g")
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

      var eventRect = d3.select("svg").append("rect")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .style("opacity", 0);

      var data = createData();

      var zExtent = d3.extent(data, function(d) { return d.z; }),
          zDifferenceExtent = [
            zExtent[0] - zExtent[1],
            0,
            zExtent[1] - zExtent[0]
          ],
          currentZ = 0;

      scale.x.domain(d3.extent(data, function(d) { return d.x; }));
      scale.y.domain(d3.extent(data, function(d) { return d.y; }));
      scale.z.domain(zExtent);
      scale.fill.domain(zDifferenceExtent);
      scale.opacity.domain(zDifferenceExtent);
      scale.r.domain(zDifferenceExtent);

      svg.append("g").attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(axis.x);
      svg.append("g").attr("class", "y axis")
        .attr("transform", "translate(" + width + ",0)")
        .call(axis.y);
      svg.append("g").attr("class", "z axis")
        .attr("transform",
          "translate(" + (width - scale.z(currentZ)) + "," + (height + scale.z(currentZ)) + ") " +
          "skewY(-45)"
        )
        .call(axis.z)
        .selectAll("text")
        .attr("transform", "skewY(45)"); // un-skew text to make legible

      svg.call(renderPoints, data,  currentZ);
      eventRect.on("wheel", function() {
        event.preventDefault();
        var scrolled = d3.event.deltaY > 0 ? "down" : "up";
        currentZ += 0.1*(scrolled === "up" ? 1 : -1);
        currentZ = currentZ > zExtent[1] ? zExtent[1] :
                   currentZ < zExtent[0] ? zExtent[0] :
                   currentZ;

        svg.call(renderPoints, data, currentZ);
        svg.select(".z.axis")
          .attr("transform",
            "translate(" + (width - scale.z(currentZ)) + "," + (height + scale.z(currentZ)) + ") " +
            "skewY(-45)"
          )
      });

      function renderPoints(selection, data, focusedZ) {
        var points = selection.selectAll(".point").data(data);

        points.enter().append("circle")
          .attr("class", "point");

        points
          .attr("cx", function(d) { return scale.x(d.x); })
          .attr("cy", function(d) { return scale.y(d.y); })
          .attr("r", function(d) { return scale.r(focusedZ - d.z); })
          .style("fill", function(d) { return scale.fill(focusedZ - d.z); })
          .style("opacity", function(d) { return scale.opacity(focusedZ - d.z); });

        points.exit()
          .remove();
      }

      function createData() {
        return d3.range(200)
          .map(function() {
            var x = d3.random.normal(10)(),
                y = d3.random.normal(10)(),
                z = Math.cos(x) + Math.sin(y);
            return {x:x, y:y, z:z};
          });
      }
    </script>
  </body>
</html>