about summary refs log tree commit diff
path: root/gn2/wqflask/static/new/javascript/scatter-matrix.js
diff options
context:
space:
mode:
Diffstat (limited to 'gn2/wqflask/static/new/javascript/scatter-matrix.js')
-rw-r--r--gn2/wqflask/static/new/javascript/scatter-matrix.js551
1 files changed, 551 insertions, 0 deletions
diff --git a/gn2/wqflask/static/new/javascript/scatter-matrix.js b/gn2/wqflask/static/new/javascript/scatter-matrix.js
new file mode 100644
index 00000000..31cb384b
--- /dev/null
+++ b/gn2/wqflask/static/new/javascript/scatter-matrix.js
@@ -0,0 +1,551 @@
+// 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;
+    }
+  });
+
+};
+