block by curran 198f19dbfdf071390ee1

The Reactivis Concept

Full Screen

This is a scatter plot of the Iris data set.

The purpose of this example is to show one approach to isolate reusable reactive flows that are reusable between visualizations, in an API called Reactivis, which stands for “reactive visualizations”. This approach allows one to generalize D3 patterns ad infinitum.

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>
    <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;
      }

      circle:hover {
        stroke-width: 2px;
        stroke: black;
      }

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

      function Reactivis(model){
        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 //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 (){
            var xScale = d3.scale.linear();

            model.when("xColumn", function (xColumn){
              model.xAccessor = get(xColumn);
            });

            model.when(["data", "xAccessor"], function(data, xAccessor){
              model.xDomain = d3.extent(data, xAccessor);
            });

            model.when("innerWidth", function (innerWidth){
              model.xRange = [0, innerWidth];
            });

            model.when(["xDomain", "xRange"], function (xDomain, xRange){
              model.xScale = xScale
                .domain(xDomain)
                .range(xRange);
            });

            model.when(["xScale", "xAccessor"], function (xScale, xAccessor){
              model.x = compose(xScale, xAccessor);
            });

            return reactivis;
          },

          xAxis: function (){
            var xAxis = d3.svg.axis()
              .orient("bottom")
              .tickFormat(d3.format("s"))
              .outerTickSize(0);

            model.when(["xScale", "xAxisNumTicks"], function (xScale, xAxisNumTicks){
              model.xAxis = xAxis
                .scale(xScale)
                .ticks(xAxisNumTicks);
            });

            model.when(["xAxisG", "xAxis"], function (xAxisG, xAxis){
              xAxisG.call(xAxis);
            });

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

              model.xAxisG = g.append("g")
                .attr("class", "x axis");

              model.xAxisLabel = model.xAxisG.append("text")
                .style("text-anchor", "middle")
                .attr("class", "label");
            });

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

            model.when(["xAxisLabel", "innerWidth", "xAxisLabelOffset", "xAxisLabelText"],
                function(xAxisLabel,   innerWidth,   xAxisLabelOffset,   xAxisLabelText){
              xAxisLabel
                .attr("x", innerWidth / 2)
                .attr("y", xAxisLabelOffset)
                .text(xAxisLabelText);
            });
            return reactivis;
          },

          yScale: function (){
            var yScale = d3.scale.linear();

            model.when("yColumn", function(yColumn){
              model.yAccessor = get(yColumn);
            });

            model.when(["data", "yAccessor"], function(data, yAccessor){
              model.yDomain = d3.extent(data, yAccessor);
            });

            model.when("innerHeight", function (innerHeight){
              model.yRange = [innerHeight, 0];
            });

            model.when(["yDomain", "yRange"], function (yDomain, yRange){
              model.yScale = yScale
                .domain(yDomain)
                .range(yRange);
            });

            model.when(["yScale", "yAccessor"], function (yScale, yAccessor){
              model.y = compose(yScale, yAccessor);
            });

            return reactivis;
          },

          yAxis: function (){
            var yAxis = d3.svg.axis()
              .orient("left")
              .tickFormat(d3.format("s"))
              .outerTickSize(0);

            model.when(["yScale", "yAxisNumTicks"], function (yScale, yAxisNumTicks){
              model.yAxis = yAxis.scale(yScale).ticks(yAxisNumTicks);
            });

            model.when(["yAxisG", "yAxis"], function (yAxisG, yAxis){
              yAxisG.call(yAxis);
            });

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

              model.yAxisG = g.append("g")
                .attr("class", "y axis");

              model.yAxisLabel = model.yAxisG.append("text")
                .style("text-anchor", "middle")
                .attr("class", "label");
            });

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

            return reactivis;
          },

          rScale: function (){
            var rScale = d3.scale.linear();

            model.when("rColumn", function(rColumn){
              model.rAccessor = get(rColumn);
            });

            model.when(["data", "rAccessor"], function(data, rAccessor){
              model.rDomain = d3.extent(data, rAccessor);
            });

            model.when(["rMin", "rMax"], function (rMin, rMax){
              model.rRange = [rMin, rMax];
            });

            model.when(["rDomain", "rRange"], function (rDomain, rRange){
              model.rScale = rScale
                .domain(rDomain)
                .range(rRange);
            });

            model.when(["rScale", "rAccessor"], function (rScale, rAccessor){
              model.r = compose(rScale, rAccessor);
            });

            return reactivis;
          },

          colorScale: function (){
            var colorScale = d3.scale.ordinal();

            model.when("colorColumn", function(colorColumn){
              model.colorAccessor = get(colorColumn);
            });

            model.when(["data", "colorAccessor"], function(data, colorAccessor){
              model.colorDomain = data.map(colorAccessor);
            });

            model.when(["colorDomain", "colorRange"], function (colorDomain, colorRange){
              model.colorScale = colorScale
                .domain(colorDomain)
                .range(colorRange);
            });

            model.when(["colorScale", "colorAccessor"], function (colorScale, colorAccessor){
              model.color = compose(colorScale, colorAccessor);
            });

            return reactivis;
          }
        };
        return reactivis;
      }

      // //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]; };
      }

      function ScatterPlot(){

        var model = Model();

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

        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;
      }

      (function main(){

        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,
          xAxisNumTicks: 10,
          yAxisLabelText: "Petal Length (cm)",
          yAxisLabelOffset: 35,
          yAxisNumTicks: 5,
          container: container
        });

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

        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;
        }

        // Allow CSS to drive visualization size.
        function setSize(){
          var containerNode = container.node();
          scatterPlot.set({
            outerWidth:  containerNode.clientWidth,
            outerHeight: containerNode.clientHeight
          });
        }

        d3.select(window)
          .on("load"  , setSize)
          .on("resize", setSize);

      }());
    </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}();