aboutsummaryrefslogtreecommitdiff
// Heavily influenced by Mike Bostock's Scatter Matrix example
// http://mbostock.github.io/d3/talk/20111116/iris-splom.html
//

/*
ScatterMatrix = function(url) {
  this.__url = url;
  this.__data = undefined;
  this.__cell_size = 140;
};
*/

ScatterMatrix = function(csv_string) {
  this.__csv_string = csv_string;
  this.__data = undefined;
  this.__cell_size = 140;
};

ScatterMatrix.prototype.cellSize = function(n) {
  this.__cell_size = n;
  return this;
};

/*
ScatterMatrix.prototype.onData = function(cb) {
  if (this.__data) { cb(); return; }
  var self = this;
  d3.csv(self.__url, function(data) {
    self.__data = data;
    cb();
  });
};
*/

ScatterMatrix.prototype.onData = function(cb) {
  if (this.__data) { cb(); return; }
  var self = this;
  console.log("self.csv_string:", self.__csv_string)

  data = d3.csv.parse(self.__csv_string);
  self.__data = data;
  cb();

/*
  d3.csv.parseRows(self.__csv_string, function(data) {
    self.__data = data;
    cb();
  });
*/

};

ScatterMatrix.prototype.render = function () {
  var self = this;

  var container = d3.select('#scatterplot_container').append('div')
                                   .attr('class', 'scatter-matrix-container');
  var control = container.append('div')
                         .attr('class', 'scatter-matrix-control')
                         .style({'float':'left', 'margin-right':'50px'})
  var svg = container.append('div')
                     .attr('class', 'scatter-matrix-svg')
                     .style({'float':'left'})
                     .html('<em>Loading data...</em>');

  this.onData(function() {
    var data = self.__data;

    // Fetch data and get all string variables
    var string_variables = [undefined];
    var numeric_variables = [];
    var numeric_variable_values = {};

    for (k in data[0]) {
      if (isNaN(+data[0][k])) { string_variables.push(k); }
      else { numeric_variables.push(k); numeric_variable_values[k] = []; }
    }

    console.log("data:", data)

    data.forEach(function(d) {
      for (var j in numeric_variables) {
        var k = numeric_variables[j];
        var value = d[k];
        if (numeric_variable_values[k].indexOf(value) < 0) {
          numeric_variable_values[k].push(value);
        }
      }
    });

    var size_control = control.append('div').attr('class', 'scatter-matrix-size-control');
    var color_control = control.append('div').attr('class', 'scatter-matrix-color-control');
    var filter_control = control.append('div').attr('class', 'scatter-matrix-filter-control');
    var variable_control = control.append('div').attr('class', 'scatter-matrix-variable-control');
    var drill_control = control.append('div').attr('class', 'scatter-matrix-drill-control');

    // shared control states
    var to_include = [];
    var color_variable = undefined;
    var selected_colors = undefined;
    for (var j in numeric_variables) {
      var v = numeric_variables[j];
      to_include.push(v);
    }
    var drill_variables = [];

    function set_filter(variable) {
      filter_control.selectAll('*').remove();
      if (variable) {
        // Get unique values for this variable
        var values = [];
        data.forEach(function(d) {
          var v = d[variable];
          if (values.indexOf(v) < 0) { values.push(v); }
        });

        selected_colors = [];
        for (var j in values) {
          var v = values[j];
          selected_colors.push(v);
        }

        var filter_li =
          filter_control
            .append('p').text('Filter by '+variable+': ')
            .append('ul')
            .selectAll('li')
            .data(values)
            .enter().append('li');

        filter_li.append('input')
                   .attr('type', 'checkbox')
                   .attr('checked', 'checked')
                   .on('click', function(d, i) {
                     var new_selected_colors = [];
                     for (var j in selected_colors) {
                       var v = selected_colors[j];
                       if (v !== d || this.checked) { new_selected_colors.push(v); }
                     }
                     if (this.checked) { new_selected_colors.push(d); }
                     selected_colors = new_selected_colors;
                     self.__draw(self.__cell_size, svg, color_variable, selected_colors, to_include, drill_variables);
                   });
        filter_li.append('label')
                   .html(function(d) { return d; });
      }
    }

    size_a = size_control.append('p').text('Change cell size: ');
    size_a.append('a')
          .attr('href', '#')
          .html('-')
          .on('click', function() {
            self.__cell_size *= 0.75;
            self.__draw(self.__cell_size, svg, color_variable, selected_colors, to_include, drill_variables);
          });
    size_a.append('span').html('&nbsp;');
    size_a.append('a')
          .attr('href', '#')
          .html('+')
          .on('click', function() {
            self.__cell_size *= 1.25;
            self.__draw(self.__cell_size, svg, color_variable, selected_colors, to_include, drill_variables);
          });

    color_control.append('p').text('Select a variable to color:')
    color_control
      .append('ul')
      .selectAll('li')
      .data(string_variables)
      .enter().append('li')
        .append('a')
          .attr('href', '#')
          .text(function(d) { return d ? d : 'None'; })
          .on('click', function(d, i) {
            color_variable = d;
            selected_colors = undefined;
            self.__draw(self.__cell_size, svg, color_variable, selected_colors, to_include, drill_variables);
            set_filter(d);
          });

    var variable_li =
      variable_control
        .append('p').text('Include variables: ')
        .append('ul')
        .selectAll('li')
        .data(numeric_variables)
        .enter().append('li');

    variable_li.append('input')
               .attr('type', 'checkbox')
               .attr('checked', 'checked')
               .on('click', function(d, i) {
                 var new_to_include = [];
                 for (var j in to_include) {
                   var v = to_include[j];
                   if (v !== d || this.checked) { new_to_include.push(v); }
                 }
                 if (this.checked) { new_to_include.push(d); }
                 to_include = new_to_include;
                 self.__draw(self.__cell_size, svg, color_variable, selected_colors, to_include, drill_variables);
               });
    variable_li.append('label')
               .html(function(d) { return d; });

    drill_li =
      drill_control
        .append('p').text('Drill and Expand: ')
        .append('ul')
        .selectAll('li')
        .data(numeric_variables)
        .enter().append('li');

    drill_li.append('input')
            .attr('type', 'checkbox')
            .on('click', function(d, i) {
               var new_drill_variables = [];
               for (var j in drill_variables) {
                 var v = drill_variables[j];
                 if (v !== d || this.checked) { new_drill_variables.push(v); }
               }
               if (this.checked) { new_drill_variables.push(d); }
               drill_variables = new_drill_variables;
               self.__draw(self.__cell_size, svg, color_variable, selected_colors, to_include, drill_variables);
             });
    drill_li.append('label')
            .html(function(d) { return d+' ('+numeric_variable_values[d].length+')'; });

    self.__draw(self.__cell_size, svg, color_variable, selected_colors, to_include, drill_variables);
  });
};

ScatterMatrix.prototype.__draw =
  function(cell_size, container_el, color_variable, selected_colors, to_include, drill_variables) {
  var self = this;
  this.onData(function() {
    var data = self.__data;

    if (color_variable && selected_colors) {
      data = [];
      self.__data.forEach(function(d) {
        if (selected_colors.indexOf(d[color_variable]) >= 0) { data.push(d); }
      });
    }

    container_el.selectAll('*').remove();

    // If no data, don't do anything
    if (data.length == 0) { return; }

    // Parse headers from first row of data
    var numeric_variables = [];
    for (k in data[0]) {
      if (!isNaN(+data[0][k]) && to_include.indexOf(k) >= 0) { numeric_variables.push(k); }
    }
    numeric_variables.sort();

    // Get values of the string variable
    var colors = [];
    if (color_variable) {
      // Using self.__data, instead of data, so our css classes are consistent when
      // we filter by value.
      self.__data.forEach(function(d) {
        var s = d[color_variable];
        if (colors.indexOf(s) < 0) { colors.push(s); }
      });
    }

    function color_class(d) {
      var c = d;
      if (color_variable && d[color_variable]) { c = d[color_variable]; }
      return colors.length > 0 ? 'color-'+colors.indexOf(c) : 'color-2';
    }

    // Size parameters
    var size = cell_size, padding = 10,
        axis_width = 20, axis_height = 15, legend_width = 200, label_height = 15;

    // Get x and y scales for each numeric variable
    var x = {}, y = {};
    numeric_variables.forEach(function(trait) {
      // Coerce values to numbers.
      data.forEach(function(d) { d[trait] = +d[trait]; });

      var value = function(d) { return d[trait]; },
          domain = [d3.min(data, value), d3.max(data, value)],
          range_x = [padding / 2, size - padding / 2],
          range_y = [padding / 2, size - padding / 2];

      x[trait] = d3.scale.linear().domain(domain).range(range_x);
      y[trait] = d3.scale.linear().domain(domain).range(range_y.reverse());
    });

    // When drilling, user select one or more variables. The first drilled
    // variable becomes the x-axis variable for all columns, and each column
    // contains only data points that match specific values for each of the
    // drilled variables other than the first.

    var drill_values = [];
    var drill_degrees = []
    drill_variables.forEach(function(variable) {
      // Skip first one, since that's just the x axis
      if (drill_values.length == 0) {
        drill_values.push([]);
        drill_degrees.push(1);
      }
      else {
        var values = [];
        data.forEach(function(d) {
          var v = d[variable];
          if (v !== undefined && values.indexOf(v) < 0) { values.push(v); }
        });
        values.sort();
        drill_values.push(values);
        drill_degrees.push(values.length);
      }
    });
    var total_columns = 1;
    drill_degrees.forEach(function(d) { total_columns *= d; });

    // Pick out stuff to draw on horizontal and vertical dimensions

    if (drill_variables.length > 0) {
      // First drill is now the x-axis variable for all columns
      x_variables = [];
      for (var i=0; i<total_columns; i++) {
        x_variables.push(drill_variables[0]);
      }
    }
    else {
      x_variables = numeric_variables.slice(0);
    }

    if (drill_variables.length > 0) {
      // Don't draw any of the "drilled" variables in vertical dimension
      y_variables = [];
      numeric_variables.forEach(function(variable) {
        if (drill_variables.indexOf(variable) < 0) { y_variables.push(variable); }
      });
    }
    else {
      y_variables = numeric_variables.slice(0);
    }

    var filter_descriptions = 0;
    if (drill_variables.length > 1) {
      filter_descriptions = drill_variables.length-1;
    }

    // Axes
    var x_axis = d3.svg.axis();
    var y_axis = d3.svg.axis();
    var intf = d3.format('d');
    var fltf = d3.format('.f');
    var scif = d3.format('e');

    x_axis.ticks(5)
          .tickSize(size * y_variables.length)
          .tickFormat(function(d) {
            if (Math.abs(+d) > 10000 || (Math.abs(d) < 0.001 && Math.abs(d) != 0)) { return scif(d); }
            if (parseInt(d) == +d) { return intf(d); }
            return fltf(d);
          });

    y_axis.ticks(5)
          .tickSize(size * x_variables.length)
          .tickFormat(function(d) {
            if (Math.abs(+d) > 10000 || (Math.abs(d) < 0.001 && Math.abs(d) != 0)) { return scif(d); }
            if (parseInt(d) == +d) { return intf(d); }
            return fltf(d);
          });

    // Brush - for highlighting regions of data
    var brush = d3.svg.brush()
        .on("brushstart", brushstart)
        .on("brush", brush)
        .on("brushend", brushend);

    // Root panel
    var svg = container_el.append("svg:svg")
        .attr("width", label_height + size * x_variables.length + axis_width + padding + legend_width)
        .attr("height", size * y_variables.length + axis_height + label_height + label_height*filter_descriptions)
      .append("svg:g")
        .attr("transform", "translate("+label_height+",0)");

    // Push legend to the side
    var legend = svg.selectAll("g.legend")
        .data(colors)
      .enter().append("svg:g")
        .attr("class", "legend")
        .attr("transform", function(d, i) {
          return "translate(" + (label_height + size * x_variables.length + padding) + "," + (i*20+10) + ")";
        });

    legend.append("svg:circle")
        .attr("class", function(d, i) { return color_class(d); })
        .attr("r", 3);

    legend.append("svg:text")
        .attr("x", 12)
        .attr("dy", ".31em")
        .text(function(d) { return d; });

    // Draw X-axis
    svg.selectAll("g.x.axis")
        .data(x_variables)
      .enter().append("svg:g")
        .attr("class", "x axis")
        .attr("transform", function(d, i) { return "translate(" + i * size + ",0)"; })
        .each(function(d) { d3.select(this).call(x_axis.scale(x[d]).orient("bottom")); });

    // Draw Y-axis
    svg.selectAll("g.y.axis")
        .data(y_variables)
      .enter().append("svg:g")
        .attr("class", "y axis")
        .attr("transform", function(d, i) { return "translate(0," + i * size + ")"; })
        .each(function(d) { d3.select(this).call(y_axis.scale(y[d]).orient("right")); });

    // Draw scatter plot
    var cell = svg.selectAll("g.cell")
        .data(cross(x_variables, y_variables))
      .enter().append("svg:g")
        .attr("class", "cell")
        .attr("transform", function(d) { return "translate(" + d.i * size + "," + d.j * size + ")"; })
        .each(plot);

    // Add titles for y variables
    cell.filter(function(d) { return d.i == 0; }).append("svg:text")
        .attr("x", padding-size)
        .attr("y", -label_height)
        .attr("dy", ".71em")
        .attr("transform", function(d) { return "rotate(-90)"; })
        .text(function(d) { return d.y; });

    function plot(p) {
      // console.log(p);

      var data_to_draw = data;

      // If drilling, compute what values of the drill variables correspond to
      // this column.
      //
      var filter = {};
      if (drill_variables.length > 1) {
        var column = p.i;

        var cap = 1;
        for (var i=drill_variables.length-1; i > 0; i--) {
          var var_name = drill_variables[i];
          var var_value = undefined;

          if (i == drill_variables.length-1) {
            // for the last drill variable, we index by %
            var_value = drill_values[i][column % drill_degrees[i]];
          }
          else {
            // otherwise divide by capacity of subsequent variables to get value array index
            var_value = drill_values[i][parseInt(column/cap)];
          }

          filter[var_name] = var_value;
          cap *= drill_degrees[i];
        }

        data_to_draw = [];
        data.forEach(function(d) {
          var pass = true;
          for (k in filter) { if (d[k] != filter[k]) { pass = false; break; } }
          if (pass === true) { data_to_draw.push(d); }
        });
      }

      var cell = d3.select(this);

      // Frame
      cell.append("svg:rect")
          .attr("class", "frame")
          .attr("x", padding / 2)
          .attr("y", padding / 2)
          .attr("width", size - padding)
          .attr("height", size - padding);

      // Scatter plot dots
      cell.selectAll("circle")
          .data(data_to_draw)
        .enter().append("svg:circle")
          .attr("class", function(d) { return color_class(d); })
          .attr("cx", function(d) { return x[p.x](d[p.x]); })
          .attr("cy", function(d) { return y[p.y](d[p.y]); })
          .attr("r", 5);

      // Add titles for x variables and drill variable values
      if (p.j == y_variables.length-1) {
        cell.append("svg:text")
            .attr("x", padding)
            .attr("y", size+axis_height)
            .attr("dy", ".71em")
            .text(function(d) { return d.x; });

        if (drill_variables.length > 1) {
          var i = 0;
          for (k in filter) {
            i += 1;
            cell.append("svg:text")
                .attr("x", padding)
                .attr("y", size+axis_height+label_height*i)
                .attr("dy", ".71em")
                .text(function(d) { return filter[k]+': '+k; });
          }
        }
      }

      // Brush
      cell.call(brush.x(x[p.x]).y(y[p.y]));
    }

    // Clear the previously-active brush, if any
    function brushstart(p) {
      if (brush.data !== p) {
        cell.call(brush.clear());
        brush.x(x[p.x]).y(y[p.y]).data = p;
      }
    }

    // Highlight selected circles
    function brush(p) {
      var e = brush.extent();
      svg.selectAll(".cell circle").attr("class", function(d) {
        return e[0][0] <= d[p.x] && d[p.x] <= e[1][0]
            && e[0][1] <= d[p.y] && d[p.y] <= e[1][1]
            ? color_class(d) : null;
      });
    }

    // If brush is empty, select all circles
    function brushend() {
      if (brush.empty()) svg.selectAll(".cell circle").attr("class", function(d) {
        return color_class(d);
      });
    }

    function cross(a, b) {
      var c = [], n = a.length, m = b.length, i, j;
      for (i = -1; ++i < n;) for (j = -1; ++j < m;) c.push({x: a[i], i: i, y: b[j], j: j});
      return c;
    }
  });

};