// 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('Loading data...');
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(' ');
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 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;
}
});
};