block by curran 9aafca5fba0c7fde13aa

Reusable Scatter Plot with Model.js

Full Screen

This is a scatter plot of the Iris data set.

The purpose of this example is to show one approach for using Model.js to make reusable visualization modules. This also introduces a way to isolate reusable reactive flows that are reusable between 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.

This example is a lead-in to The Reactivis Concept, which takes generalization of D3 patterns further.

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 ScatterPlot(container){

        var model = Model();

        var svg = container.append("svg");
        var g = svg.append("g");

        var xAxisG = g.append("g")
          .attr("class", "x axis");
        var xAxisLabel = xAxisG.append("text")
          .style("text-anchor", "middle")
          .attr("class", "label");

        var yAxisG = g.append("g")
          .attr("class", "y axis");
        var yAxisLabel = yAxisG.append("text")
          .style("text-anchor", "middle")
          .attr("class", "label");

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

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

        // This encapsulates the D3 margin convention from //bl.ocks.org/mbostock/3019563
        model.when(["outerWidth", "outerHeight", "margin"],
            function(outerWidth ,  outerHeight ,  margin){
          model.set({
            innerWidth:  outerWidth  - margin.left - margin.right,
            innerHeight: outerHeight - margin.top  - margin.bottom
          });
        });

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

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

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

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

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

        (function (){
          var xScale = d3.scale.linear();

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

          model.when(["xScaleDomain", "innerWidth"], function (xScaleDomain, innerWidth){
            model.xScale = xScale.domain(xScaleDomain).range([0, innerWidth]);
          });
        }());

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

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

          model.when(["yScaleDomain", "innerHeight"], function (yScaleDomain, innerHeight){
            model.yScale = yScale.domain(yScaleDomain).range([innerHeight, 0]);
          });
        }());

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

          model.when(["data", "rColumn"], function(data, rColumn){
            model.rScaleDomain = d3.extent(data, function (d){ return d[rColumn]; });
          });

          model.when(["rScaleDomain", "rMin", "rMax"], function (rScaleDomain, rMin, rMax){
            model.rScale = rScale.domain(rScaleDomain).range([rMin, rMax]);
          });
        }());

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

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

        model.when("xColumn", function (xColumn){
          model.xAccessor = function (d){ return d[xColumn]; };
        });

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

        model.when("yColumn", function(yColumn){
          model.yAccessor = function (d){ return d[yColumn]; };
        });

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

        model.when("rColumn", function(rColumn){
          model.rAccessor = function (d){ return d[rColumn]; };
        });

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

        model.when("colorColumn", function(colorColumn){
          model.colorAccessor = function (d){ return d[colorColumn]; };
        });

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

        model.when(["data", "x", "y", "r", "color"], function(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;
      }

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

      function main(){

        var container = d3.select("#scatterPlotContainer");

        var scatterPlot = ScatterPlot(container);

        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",
          colorScale: d3.scale.category10(),
          xAxisLabelText: "Sepal Length (cm)",
          xAxisLabelOffset: 65,
          xAxisNumTicks: 10,
          yAxisLabelText: "Petal Length (cm)",
          yAxisLabelOffset: 35,
          yAxisNumTicks: 5
        });

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

        function setSize(){
          var containerNode = container.node();
          scatterPlot.set({
            outerWidth:  containerNode.clientWidth,
            outerHeight: containerNode.clientHeight
          });
        }

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

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