block by curran 35b5ec7ad6547cd95781

Generalizing D3 patterns ad infinitum

Full Screen

This is a scatter plot of the Iris data set.

This is an experiment to see how far one can go in generalizing D3 visualization patterns. The main file of interest here is reactivis.js. For example, a single code path creates all scales (x, y, size, color) and sets up their reactive dependencies. This is a proof-of-concept for a larger scale project that aims to provide a base layer for many different D3 visualizations.

Notice that if you open this in a new window, it responds when you resize the browser window. This is coded in such a way that the visualization size can be controlled via CSS.

The code for this is derived from example 106 of the screencast Introduction to D3.js.

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>D3 Example</title>
    <script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
    <script src="model-min.js"></script>
    <script src="reactivis.js"></script>
    <script src="scatterPlot.js"></script>
    <script src="barChart.js"></script>
    <link href='//fonts.googleapis.com/css?family=Poiret+One' rel='stylesheet' type='text/css'>
    <style>

      body {
        background-color: lightgray;
      }
    
      .axis text {
        font-family: 'Poiret One', cursive;
        font-size: 16pt;
      }
      .axis .label {
        font-size: 20pt;
      }

      .axis path, .axis line {
        fill: none;
        stroke: #000;
        shape-rendering: crispEdges;
      }

      .visualization {
        position: fixed;
        top: 50px;
        bottom: 50px;
        left: 280px;
        right: 280px;
        background-color: white;
        border-radius: 25px;
        border-style: dotted;
        border-width: 2px;
      }

    </style>
  </head>
  <body>
    <div class="visualization" id="scatterPlotContainer"></div>
    <script>

      function setupScatterPlot(){
        var container = d3.select("#scatterPlotContainer");
        var scatterPlot = ScatterPlot();

        scatterPlot.set({
          outerWidth: 100,
          outerHeight: 500,
          margin: { left: 80, top: 10, right: 15, bottom: 80 },
          rMin: 2, // "r" stands for radius
          rMax: 15,
          xColumn: "sepal_length",
          yColumn: "petal_length",
          rColumn: "sepal_width",
          colorColumn: "species",
          colorRange: d3.scale.category10().range(),
          xAxisLabelText: "Sepal Length (cm)",
          xAxisLabelOffset: 65,
          xAxisTicks: 10,
          xAxisTickFormat: d3.format("s"),    
          yAxisLabelText: "Petal Length (cm)",
          yAxisLabelOffset: 35,
          yAxisTicks: 5,
          yAxisTickFormat: d3.format("s"),    
          container: container
        });

        function type(d){
          d.sepal_length = +d.sepal_length;
          d.sepal_width  = +d.sepal_width;
          d.petal_length = +d.petal_length;
          d.petal_width  = +d.petal_width;
          return d;
        }

        d3.csv("iris.csv", type, function (data) {
          scatterPlot.data = data;
        });
      }

      setupScatterPlot();
    </script>
  </body>
</html>

iris.csv

sepal_length,sepal_width,petal_length,petal_width,species
5.1,3.5,1.4,0.2,setosa
4.9,3.0,1.4,0.2,setosa
4.7,3.2,1.3,0.2,setosa
4.6,3.1,1.5,0.2,setosa
5.0,3.6,1.4,0.2,setosa
5.4,3.9,1.7,0.4,setosa
4.6,3.4,1.4,0.3,setosa
5.0,3.4,1.5,0.2,setosa
4.4,2.9,1.4,0.2,setosa
4.9,3.1,1.5,0.1,setosa
5.4,3.7,1.5,0.2,setosa
4.8,3.4,1.6,0.2,setosa
4.8,3.0,1.4,0.1,setosa
4.3,3.0,1.1,0.1,setosa
5.8,4.0,1.2,0.2,setosa
5.7,4.4,1.5,0.4,setosa
5.4,3.9,1.3,0.4,setosa
5.1,3.5,1.4,0.3,setosa
5.7,3.8,1.7,0.3,setosa
5.1,3.8,1.5,0.3,setosa
5.4,3.4,1.7,0.2,setosa
5.1,3.7,1.5,0.4,setosa
4.6,3.6,1.0,0.2,setosa
5.1,3.3,1.7,0.5,setosa
4.8,3.4,1.9,0.2,setosa
5.0,3.0,1.6,0.2,setosa
5.0,3.4,1.6,0.4,setosa
5.2,3.5,1.5,0.2,setosa
5.2,3.4,1.4,0.2,setosa
4.7,3.2,1.6,0.2,setosa
4.8,3.1,1.6,0.2,setosa
5.4,3.4,1.5,0.4,setosa
5.2,4.1,1.5,0.1,setosa
5.5,4.2,1.4,0.2,setosa
4.9,3.1,1.5,0.1,setosa
5.0,3.2,1.2,0.2,setosa
5.5,3.5,1.3,0.2,setosa
4.9,3.1,1.5,0.1,setosa
4.4,3.0,1.3,0.2,setosa
5.1,3.4,1.5,0.2,setosa
5.0,3.5,1.3,0.3,setosa
4.5,2.3,1.3,0.3,setosa
4.4,3.2,1.3,0.2,setosa
5.0,3.5,1.6,0.6,setosa
5.1,3.8,1.9,0.4,setosa
4.8,3.0,1.4,0.3,setosa
5.1,3.8,1.6,0.2,setosa
4.6,3.2,1.4,0.2,setosa
5.3,3.7,1.5,0.2,setosa
5.0,3.3,1.4,0.2,setosa
7.0,3.2,4.7,1.4,versicolor
6.4,3.2,4.5,1.5,versicolor
6.9,3.1,4.9,1.5,versicolor
5.5,2.3,4.0,1.3,versicolor
6.5,2.8,4.6,1.5,versicolor
5.7,2.8,4.5,1.3,versicolor
6.3,3.3,4.7,1.6,versicolor
4.9,2.4,3.3,1.0,versicolor
6.6,2.9,4.6,1.3,versicolor
5.2,2.7,3.9,1.4,versicolor
5.0,2.0,3.5,1.0,versicolor
5.9,3.0,4.2,1.5,versicolor
6.0,2.2,4.0,1.0,versicolor
6.1,2.9,4.7,1.4,versicolor
5.6,2.9,3.6,1.3,versicolor
6.7,3.1,4.4,1.4,versicolor
5.6,3.0,4.5,1.5,versicolor
5.8,2.7,4.1,1.0,versicolor
6.2,2.2,4.5,1.5,versicolor
5.6,2.5,3.9,1.1,versicolor
5.9,3.2,4.8,1.8,versicolor
6.1,2.8,4.0,1.3,versicolor
6.3,2.5,4.9,1.5,versicolor
6.1,2.8,4.7,1.2,versicolor
6.4,2.9,4.3,1.3,versicolor
6.6,3.0,4.4,1.4,versicolor
6.8,2.8,4.8,1.4,versicolor
6.7,3.0,5.0,1.7,versicolor
6.0,2.9,4.5,1.5,versicolor
5.7,2.6,3.5,1.0,versicolor
5.5,2.4,3.8,1.1,versicolor
5.5,2.4,3.7,1.0,versicolor
5.8,2.7,3.9,1.2,versicolor
6.0,2.7,5.1,1.6,versicolor
5.4,3.0,4.5,1.5,versicolor
6.0,3.4,4.5,1.6,versicolor
6.7,3.1,4.7,1.5,versicolor
6.3,2.3,4.4,1.3,versicolor
5.6,3.0,4.1,1.3,versicolor
5.5,2.5,4.0,1.3,versicolor
5.5,2.6,4.4,1.2,versicolor
6.1,3.0,4.6,1.4,versicolor
5.8,2.6,4.0,1.2,versicolor
5.0,2.3,3.3,1.0,versicolor
5.6,2.7,4.2,1.3,versicolor
5.7,3.0,4.2,1.2,versicolor
5.7,2.9,4.2,1.3,versicolor
6.2,2.9,4.3,1.3,versicolor
5.1,2.5,3.0,1.1,versicolor
5.7,2.8,4.1,1.3,versicolor
6.3,3.3,6.0,2.5,virginica
5.8,2.7,5.1,1.9,virginica
7.1,3.0,5.9,2.1,virginica
6.3,2.9,5.6,1.8,virginica
6.5,3.0,5.8,2.2,virginica
7.6,3.0,6.6,2.1,virginica
4.9,2.5,4.5,1.7,virginica
7.3,2.9,6.3,1.8,virginica
6.7,2.5,5.8,1.8,virginica
7.2,3.6,6.1,2.5,virginica
6.5,3.2,5.1,2.0,virginica
6.4,2.7,5.3,1.9,virginica
6.8,3.0,5.5,2.1,virginica
5.7,2.5,5.0,2.0,virginica
5.8,2.8,5.1,2.4,virginica
6.4,3.2,5.3,2.3,virginica
6.5,3.0,5.5,1.8,virginica
7.7,3.8,6.7,2.2,virginica
7.7,2.6,6.9,2.3,virginica
6.0,2.2,5.0,1.5,virginica
6.9,3.2,5.7,2.3,virginica
5.6,2.8,4.9,2.0,virginica
7.7,2.8,6.7,2.0,virginica
6.3,2.7,4.9,1.8,virginica
6.7,3.3,5.7,2.1,virginica
7.2,3.2,6.0,1.8,virginica
6.2,2.8,4.8,1.8,virginica
6.1,3.0,4.9,1.8,virginica
6.4,2.8,5.6,2.1,virginica
7.2,3.0,5.8,1.6,virginica
7.4,2.8,6.1,1.9,virginica
7.9,3.8,6.4,2.0,virginica
6.4,2.8,5.6,2.2,virginica
6.3,2.8,5.1,1.5,virginica
6.1,2.6,5.6,1.4,virginica
7.7,3.0,6.1,2.3,virginica
6.3,3.4,5.6,2.4,virginica
6.4,3.1,5.5,1.8,virginica
6.0,3.0,4.8,1.8,virginica
6.9,3.1,5.4,2.1,virginica
6.7,3.1,5.6,2.4,virginica
6.9,3.1,5.1,2.3,virginica
5.8,2.7,5.1,1.9,virginica
6.8,3.2,5.9,2.3,virginica
6.7,3.3,5.7,2.5,virginica
6.7,3.0,5.2,2.3,virginica
6.3,2.5,5.0,1.9,virginica
6.5,3.0,5.2,2.0,virginica
6.2,3.4,5.4,2.3,virginica
5.9,3.0,5.1,1.8,virginica

model-min.js

// This file comes from https://github.com/curran/model
!function(){function n(n){function t(n,t,r){r=r||this,n=n instanceof Array?n:[n];var u=o(function(){var o=n.map(function(n){return p[n]});e(o)&&t.apply(r,o)});return u(),n.forEach(function(n){f(n,u)}),u}function o(n){var t=!1;return function(){t||(t=!0,setTimeout(function(){t=!1,n()},0))}}function e(n){return!n.some(function(n){return"undefined"==typeof n||null===n})}function f(n,t,o){o=o||this,r(n).push(t),u(n,o)}function r(n){return d[n]||(d[n]=[])}function u(n,t){n in l||(l[n]=!0,p[n]=s[n],Object.defineProperty(s,n,{get:function(){return p[n]},set:function(o){var e=p[n];p[n]=o,r(n).forEach(function(n){n.call(t,o,e)})}}))}function i(n){for(var t in d)c(t,n)}function c(n,t){d[n]=d[n].filter(function(n){return n!==t})}function a(n){for(var t in n)s[t]=n[t]}var s={},p={},d={},l={};return a(n),s.when=t,s.cancel=i,s.on=f,s.off=c,s.set=a,s}n.None="__NONE__","function"==typeof define&&define.amd?define([],function(){return n}):"object"==typeof exports?module.exports=n:this.Model=n}();

reactivis.js

function Reactivis(model){

  var scales = {
    linear:  d3.scale.linear,
    ordinal: d3.scale.ordinal
  };

  function scale(prefix, type){

    type = type || "linear";

    var scale = scales[type]();

    var columnProperty    = prefix + "Column";
    var accessorProperty  = prefix + "Accessor";
    var domainProperty    = prefix + "Domain";
    var rangeProperty     = prefix + "Range";
    var scaleProperty     = prefix + "Scale";

    model.when(columnProperty, function (column){
      model[accessorProperty] = get(column);
    });

    model.when(["data", accessorProperty], function(data, accessor){
      if(type === "linear"){
        model[domainProperty] = d3.extent(data, accessor);
      } else if(type === "ordinal"){
        model[domainProperty] = data.map(accessor);
      }
    });

    model.when([domainProperty, rangeProperty], function (domain, range){
      model[scaleProperty] = scale
        .domain(domain)
        .range(range);
    });

    model.when([scaleProperty, accessorProperty], function (scale, accessor){
      model[prefix] = compose(scale, accessor);
    });
  }

  function axis(prefix){
    var axis = d3.svg.axis()
      .outerTickSize(0);

    var scaleProperty = prefix + "Scale";
    var axisProperty  = prefix + "Axis";
    var axisGProperty = axisProperty + "G";
    var ticksProperty = axisProperty + "Ticks";
    var labelProperty = axisProperty + "Label";
    var textProperty  = labelProperty + "Text";
    var tickFormatProperty = axisProperty + "TickFormat";

    model.when([scaleProperty, ticksProperty, tickFormatProperty],
        function(scale, ticks, tickFormat){
      model[axisProperty] = axis
        .scale(scale)
        .ticks(ticks)
        .tickFormat(tickFormat);
    });

    model.when([axisGProperty, axisProperty], function (axisG, axis){
      axisG.call(axis);
    });

    model.when("g", function (g){

      var axisG = g.append("g")
        .attr("class", prefix + " axis");

      model[axisGProperty] = axisG;
      model[labelProperty] = axisG.append("text")
        .style("text-anchor", "middle")
        .attr("class", "label");
    });

    model.when([labelProperty, textProperty], function(label, text){
      label.text(text);
    });

    return axis;
  }

  // http://en.wikipedia.org/wiki/Function_composition
  function compose(g, f){
    return function(d){ return g(f(d)); };
  }

  // Abstracts the common pattern of accessing an object property.
  function get(property){
    return function(d){ return d[property]; };
  }

  var reactivis = {

    svg: function (){

      model.when("container", function (container) {
        model.svg = container.append("svg");
      });

      model.when(["svg", "outerWidth"], function(svg, outerWidth){
        svg.attr("width", outerWidth);
      });

      model.when(["svg", "outerHeight"], function(svg, outerHeight){
        svg.attr("height", outerHeight);
      });

      model.when("svg", function (svg){
        model.g = svg.append("g");
      });

      model.when(["g", "margin"], function (g, margin){
        g.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
      });

      return reactivis;
    },

    // This encapsulates the D3 margin convention from http://bl.ocks.org/mbostock/3019563
    margin: function (){

      model.when(["outerWidth", "margin"], function(outerWidth, margin){
        model.innerWidth = outerWidth - margin.left - margin.right;
      });

      model.when(["outerHeight", "margin"], function(outerHeight, margin){
        model.innerHeight = outerHeight - margin.top - margin.bottom
      });

      return reactivis;
    },

    xScale: function (type){
      scale("x", type);
      model.when("innerWidth", function (innerWidth){
        model.xRange = [0, innerWidth];
      });
      return reactivis;
    },

    yScale: function (type){
      scale("y", type);
      model.when("innerHeight", function (innerHeight){
        model.yRange = [innerHeight, 0];
      });
      return reactivis;
    },

    xAxis: function (){

      axis("x").orient("bottom");

      model.when(["xAxisG", "innerHeight"], function (xAxisG, innerHeight){
        xAxisG.attr("transform", "translate(0," + innerHeight + ")");
      });

      model.when(["xAxisLabel", "innerWidth"], function(xAxisLabel, innerWidth){
        xAxisLabel.attr("x", innerWidth / 2);
      });

      model.when(["xAxisLabel", "xAxisLabelOffset"], function(xAxisLabel, xAxisLabelOffset){
        xAxisLabel.attr("y", xAxisLabelOffset)
      });

      return reactivis;
    },

    yAxis: function (){

      axis("y").orient("left");

      model.when(["yAxisLabel", "innerHeight", "yAxisLabelOffset"],
          function(yAxisLabel,   innerHeight,   yAxisLabelOffset){
        yAxisLabel
          .attr("transform", "translate(-" + yAxisLabelOffset + "," + (innerHeight / 2) + ") rotate(-90)")
      });

      return reactivis;
    },

    rScale: function (){
      scale("r");
      model.when(["rMin", "rMax"], function (rMin, rMax){
        model.rRange = [rMin, rMax];
      });
      return reactivis;
    },

    colorScale: function (){
      scale("color", "ordinal");
      return reactivis;
    },

    resize: function (){
      model.when("container", function (container){
        function setSize(){
          var containerNode = container.node();
          model.set({
            outerWidth:  containerNode.clientWidth,
            outerHeight: containerNode.clientHeight
          });
        }
        d3.select(window).on("resize", setSize);
        setSize();
      });
    }
  };

  return reactivis;
}

scatterPlot.js

function ScatterPlot(){

  var model = Model();

  Reactivis(model)
    .svg()
    .margin()
    .xScale().xAxis()
    .yScale().yAxis()
    .rScale()
    .colorScale()
    .resize();

  model.when(["g", "data", "x", "y", "r", "color"], function(g, data, x, y, r, color){
    var circles = g.selectAll("circle").data(data);
    circles.enter().append("circle");
    circles
      .attr("cx", x)
      .attr("cy", y)
      .attr("r", r)
      .attr("fill", color)
    circles.exit().remove();
  });

  return model;
}