block by curran cf4b98fff0517ca04667

Scatter Plot Zooming

Full Screen

This program makes a scatter plot from Iris data set. Brushing in one plot zooms in the other.

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">

    <!-- Use RequireJS for module loading. -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.14/require.js"></script>

    <!-- Configure AMD modules. -->
    <script>
      requirejs.config({
        paths: {
          d3: "//d3js.org/d3.v3.min",
          jquery: "//code.jquery.com/jquery-2.1.1.min"
        }
      });
    </script>

    <!-- Include CSS that styles the visualization. -->
    <link rel="stylesheet" href="styles.css">

    <title>Scatter Plot</title>
  </head>
  <body>

    <!-- The visualization will be injected into this div. -->
    <div id="container"></div>
    
    <!-- Run the main program. -->
    <script src="main.js"></script>

  </body>
</html>

iris-metadata.json

{
  "sepal_length":{
    "type": "Q",
    "label": "sepal length (cm)"
  },
  "sepal_width": {
    "type": "Q",
    "label": "sepal width (cm)"
  },
  "petal_length": {
    "type": "Q",
    "label": "petal length (cm)"
  },
  "petal_width": {
    "type": "Q",
    "label": "petal width (cm)"
  },
  "class": {
    "type": "N",
    "label": "species"
  }
}

iris.csv

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

main.js

// This is the main program that sets up a scatter plot to visualize the Iris data set.
// Curran Kelleher March 2015
require(["scatterPlot"], function (ScatterPlot) {

  // Initialize the scatter plot.
  var options = {
        
        // Tell the visualization which DOM element to insert itself into.
        container: d3.select("#container").node(),

        // Specify the margin and text label offsets.
        margin: {
          top: 10,
          right: 10,
          bottom: 45,
          left: 55
        },
        yAxisLabelOffset: 1.8, // Unit is CSS "em"s
        xAxisLabelOffset: 1.9,
        titleOffset: 0.3
      },
      scatterPlot1 = ScatterPlot(options),
      scatterPlot2 = ScatterPlot(options);

  // Fetch the column metadata.
  d3.json("iris-metadata.json", function (metadata) {

    var xColumn = "sepal_length",
        yColumn = "petal_length",
        sizeColumn = "petal_width",
        colorColumn = "class",
        xyOptions = {
          xColumn: xColumn,
          xAxisLabel: metadata[xColumn].label,
          yColumn: yColumn,
          yAxisLabel: metadata[yColumn].label
        };

    // Use the same X and Y for all plots.
    scatterPlot1.set(xyOptions);
    scatterPlot2.set(xyOptions);

    // Load the data from a CSV file.
    d3.csv("iris.csv", function (data){

      // Parse quantitative values from strings to numbers.
      var quantitativeColumns = Object.keys(metadata).filter(function (column){
        return metadata[column].type === "Q";
      });
      data.forEach(function (d){
        quantitativeColumns.forEach(function (column){
          d[column] = parseFloat(d[column]);
        });
      });

      // Pass the data into the plots.
      scatterPlot1.data = data;
      scatterPlot2.data = data;
    });

    // Use the first plot to zoom in the second plot.
    scatterPlot1.brushEnabled = true;
    scatterPlot1.when("brushedIntervals", function (brushedIntervals){
      scatterPlot2.xDomainMin = brushedIntervals[xColumn][0];
      scatterPlot2.xDomainMax = brushedIntervals[xColumn][1];
      scatterPlot2.yDomainMin = brushedIntervals[yColumn][0];
      scatterPlot2.yDomainMax = brushedIntervals[yColumn][1];
    });

    // Initialize the default brush.
    scatterPlot1.brushedIntervals = {
      "sepal_length": [ 4.82, 7.77 ],
      "petal_length": [ 2.84, 6.80 ]
    };
  });

  // Sets the `box` model property
  // based on the size of the container,
  function computeBoxes(){
    var width = container.clientWidth,
        height = container.clientHeight,
        padding = 10,
        plotWidth = (width - padding * 2) / 2,
        plotHeight = height - padding * 2;
    scatterPlot1.box = {
      x: padding,
      y: padding,
      width: plotWidth,
      height: plotHeight
    };
    scatterPlot2.box = {
      x: plotWidth + padding * 2,
      y: padding,
      width: plotWidth,
      height: plotHeight
    };
  }

  // once to initialize `model.box`, and
  computeBoxes();

  // whenever the browser window resizes in the future.
  window.addEventListener("resize", computeBoxes);
});

model.js

// A functional reactive model library.
//
(function(){

  // The D3 conventional graph representation.
  // See https://github.com/mbostock/d3/wiki/Force-Layout#nodes
  var nodes, links, idCounter, map;

  function resetFlowGraph(){
    nodes = [];
    links = [];
    idCounter = 0;
    map = {};
  }

  function getFlowGraph(){
    return {
      nodes: nodes,
      links: links
    };
  }

  resetFlowGraph();

  // Adds the nodes and links to the data flow graph for one
  // particular reactive function.
  function updateLambda(modelId, lambdaId, inProperties, outProperties){
    var lambda = lambdaNode(lambdaId);
    inProperties.forEach(function(property){
      link(propertyNode(modelId, property), lambda);
    });
    outProperties.forEach(function(property){
      link(lambda, propertyNode(modelId, property));
    });
  }

  function lambdaNode(id){
    return getOrCreate(id, nodes, createLambda);
  }

  function createLambda(index){
    return {
      type: "lambda",
      index: index
    };
  }

  function propertyNode(modelId, property){
    var id = modelId + "." + property;
    return getOrCreate(id, nodes, createPropertyNode(property));
  }

  function createPropertyNode(property){
    return function(index){
      return {
        type: "property",
        index: index,
        property: property
      };
    };
  }

  function link(sourceNode, targetNode){
    var source = sourceNode.index,
        target = targetNode.index,
        id = source + "-" + target;
    getOrCreate(id, links, createLink(source, target));
  }

  function createLink(source, target){
    return function(index){
      return {
        source: source,
        target: target
      };
    };
  }

  function getOrCreate(id, things, createThing){
    var thing = map[id];
    if(!thing){
      thing = map[id] = createThing(things.length);
      things.push(thing);
    } 
    return thing;
  }

  // The constructor function, accepting default values.
  function Model(defaults){

    // The returned public API object.
    var model = {},

        // The internal stored values for tracked properties. { property -> value }
        values = {},

        // The callback functions for each tracked property. { property -> [callback] }
        listeners = {},

        // The set of tracked properties. { property -> true }
        trackedProperties = {},

        modelId = idCounter++,
        changedProperties = {};

    // The functional reactive "when" operator.
    //
    //  * `properties` An array of property names (can also be a single property string).
    //  * `callback` A callback function that is called:
    //    * with property values as arguments, ordered corresponding to the properties array,
    //    * only if all specified properties have values,
    //    * once for initialization,
    //    * whenever one or more specified properties change,
    //    * on the next tick of the JavaScript event loop after properties change,
    //    * only once as a result of one or more synchronous changes to dependency properties.
    function when(properties, callback, thisArg){

      var lambdaId = idCounter++;
      
      // Make sure the default `this` becomes 
      // the object you called `.on` on.
      thisArg = thisArg || this;

      // Handle either an array or a single string.
      properties = (properties instanceof Array) ? properties : [properties];

      // This function will trigger the callback to be invoked.
      var triggerCallback = debounce(function (){
        var args = properties.map(function(property){
          return values[property];
        });
        if(allAreDefined(args)){
          changedProperties = {};

          callback.apply(thisArg, args);

          updateLambda(modelId, lambdaId, properties, Object.keys(changedProperties));
        }
      });

      // Trigger the callback once for initialization.
      triggerCallback();
      
      // Trigger the callback whenever specified properties change.
      properties.forEach(function(property){
        on(property, triggerCallback);
      });

      // Return this function so it can be removed later.
      return triggerCallback;
    }

    // Returns a debounced version of the given function.
    // See http://underscorejs.org/#debounce
    function debounce(callback){
      var queued = false;
      return function () {
        if(!queued){
          queued = true;
          setTimeout(function () {
            queued = false;
            callback();
          }, 0);
        }
      };
    }

    // Returns true if all elements of the given array are defined, false otherwise.
    function allAreDefined(arr){
      return !arr.some(function (d) {
        return typeof d === 'undefined' || d === null;
      });
    }

    // Adds a change listener for a given property with Backbone-like behavior.
    // Similar to http://backbonejs.org/#Events-on
    function on(property, callback, thisArg){

      // Make sure the default `this` becomes 
      // the object you called `.on` on.
      thisArg = thisArg || this;
      getListeners(property).push(callback);
      track(property, thisArg);
    }
    
    // Gets or creates the array of listener functions for a given property.
    function getListeners(property){
      return listeners[property] || (listeners[property] = []);
    }

    // Tracks a property if it is not already tracked.
    function track(property, thisArg){
      if(!(property in trackedProperties)){
        trackedProperties[property] = true;
        values[property] = model[property];
        Object.defineProperty(model, property, {
          get: function () { return values[property]; },
          set: function(newValue) {
            var oldValue = values[property];
            values[property] = newValue;
            getListeners(property).forEach(function(callback){
              callback.call(thisArg, newValue, oldValue);
            });

            changedProperties[property] = true;
          }
        });
      }
    }

    // Removes a listener added using `when()`.
    function cancel(listener){
      for(var property in listeners){
        off(property, listener);
      }
    }

    // Removes a change listener added using `on`.
    function off(property, callback){
      listeners[property] = listeners[property].filter(function (listener) {
        return listener !== callback;
      });
    }

    // Sets all of the given values on the model.
    // `newValues` is an object { property -> value }.
    function set(newValues){
      for(var property in newValues){
        model[property] = newValues[property];
      }
    }

    // Transfer defaults passed into the constructor to the model.
    set(defaults);

    // Expose the public API.
    model.when = when;
    model.cancel = cancel;
    model.on = on;
    model.off = off;
    model.set = set;
    return model;
  }

  Model.getFlowGraph = getFlowGraph;
  Model.resetFlowGraph = resetFlowGraph;

  // Support AMD (RequireJS), CommonJS (Node), and browser globals.
  // Inspired by https://github.com/umdjs/umd
  if (typeof define === "function" && define.amd) {
    define([], function () { return Model; });
  } else if (typeof exports === "object") {
    module.exports = Model;
  } else {
    this.Model = Model;
  }
})();

scatterPlot.js

styles.css

/* Remove the default margin. */
body {
  margin: 0px;
}

/* Make the visualization container fill the page. */
#container {

  /* Use the default size from bl.ocks.org */
  width: 960px;
  height: 500px;
}

/* Put a border around each plot. */
svg {
  border-style: solid;
  border-color: lightgray;
  border-width: 1px;
}

/* Style the visualization. Draws from http://bl.ocks.org/mbostock/3887118 */

/* Tick mark labels */
.axis .tick text {
  font: 8pt sans-serif;
}

/* Axis labels */
.axis text {
  font: 14pt sans-serif;
}

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

.line {
  fill: none;
  stroke: black;
  stroke-width: 1.5px;
}

.title-text {
  text-anchor: middle;
  font: 24pt sans-serif;
}

/* Style the brush. Draws from http://bl.ocks.org/mbostock/4343214 */
.brush .extent {
  stroke: gray;
  fill-opacity: .125;
  shape-rendering: crispEdges;
}