function D3ScatterPlotMatrix(placeholderSelector, data, config) { var self = this; this.utils = new ChartsD3Utils(); this.placeholderSelector = placeholderSelector; this.svg = null; this.defaultConfig = { width: undefined, //svg width (default: computed using cell size and margins) size: 200, //scatter plot cell size padding: 20, //scatter plot cell padding brush: true, guides: true, //show axis guides tooltip: true, //show tooltip on dot hover ticks: undefined, //ticks number, (default: computed using cell size) margin: { left: 30, right: 30, top: 30, bottom: 30 }, x: {// X axis config orient: "bottom", scale: "linear" }, y: {// Y axis config orient: "left", scale: "linear" }, dot: { radius: 2, color: null, // string or function returning color's value for color scale d3ColorCategory: 'category10' }, variables: { labels: [], //optional array of variable labels (for the diagonal of the plot). keys: [], //optional array of variable keys value: function (d, variableKey) {// variable value accessor return d[variableKey]; } }, groups:{ key: undefined, //object property name or array index with grouping variable includeInPlot: false, //include group as variable in plot, boolean (default: false) value: function(d) { return d[self.config.groups.key] }, // grouping value accessor, label: "" } }; this.setConfig(config||{}); if (data) { this.setData(data); } this.init(); } D3ScatterPlotMatrix.prototype.setData = function (data) { this.data = data; return this; }; D3ScatterPlotMatrix.prototype.setConfig = function (config) { this.config = this.utils.deepExtend({}, this.defaultConfig, config); return this; }; D3ScatterPlotMatrix.prototype.initPlot = function () { var self = this; var margin = this.config.margin; var conf = this.config; this.plot = { x: {}, y: {}, dot: { color: null//color scale mapping function } }; this.setupVariables(); this.plot.size = conf.size; var width = conf.width; var placeholderNode = d3.select(this.placeholderSelector).node(); if (!width) { var maxWidth = margin.left + margin.right + this.plot.variables.length*this.plot.size; width = Math.min(placeholderNode.getBoundingClientRect().width, maxWidth); } var height = width; if (!height) { height = placeholderNode.getBoundingClientRect().height; } this.plot.width = width - margin.left - margin.right; this.plot.height = height - margin.top - margin.bottom; if(conf.ticks===undefined){ conf.ticks = this.plot.size / 40; } this.setupX(); this.setupY(); if (conf.dot.d3ColorCategory) { this.plot.dot.colorCategory = d3.scale[conf.dot.d3ColorCategory](); } var colorValue = conf.dot.color; if (colorValue) { this.plot.dot.colorValue = colorValue; if (typeof colorValue === 'string' || colorValue instanceof String) { this.plot.dot.color = colorValue; } else if (this.plot.dot.colorCategory) { this.plot.dot.color = function (d) { return self.plot.dot.colorCategory(self.plot.dot.colorValue(d)); } } }else if(conf.groups.key){ this.plot.dot.color = function (d) { return self.plot.dot.colorCategory(d[conf.groups.key]); } } return this; }; D3ScatterPlotMatrix.prototype.setupVariables = function () { var variablesConf = this.config.variables; var data = this.data; var plot = this.plot; plot.domainByVariable = {}; plot.variables = variablesConf.keys; if(!plot.variables || !plot.variables.length){ plot.variables = this.utils.inferVariables(data, this.config.groups.key, this.config.includeInPlot); } plot.labels = []; plot.labelByVariable = {}; plot.variables.forEach(function(variableKey, index) { plot.domainByVariable[variableKey] = d3.extent(data, function(d) { return variablesConf.value(d, variableKey) }); var label = variableKey; if(variablesConf.labels && variablesConf.labels.length>index){ label = variablesConf.labels[index]; } plot.labels.push(label); plot.labelByVariable[variableKey] = label; }); console.log(plot.labelByVariable); plot.subplots = []; }; D3ScatterPlotMatrix.prototype.setupX = function () { var plot = this.plot; var x = plot.x; var conf = this.config; x.value = conf.variables.value; x.scale = d3.scale[conf.x.scale]().range([conf.padding / 2, plot.size - conf.padding / 2]); x.map = function (d, variable) { return x.scale(x.value(d, variable)); }; x.axis = d3.svg.axis().scale(x.scale).orient(conf.x.orient).ticks(conf.ticks); x.axis.tickSize(plot.size * plot.variables.length); }; D3ScatterPlotMatrix.prototype.setupY = function () { var plot = this.plot; var y = plot.y; var conf = this.config; y.value = conf.variables.value; y.scale = d3.scale[conf.y.scale]().range([ plot.size - conf.padding / 2, conf.padding / 2]); y.map = function (d, variable) { return y.scale(y.value(d, variable)); }; y.axis= d3.svg.axis().scale(y.scale).orient(conf.y.orient).ticks(conf.ticks); y.axis.tickSize(-plot.size * plot.variables.length); }; D3ScatterPlotMatrix.prototype.drawPlot = function () { var self =this; var n = self.plot.variables.length; var conf = this.config; self.svgG.selectAll(".mw-axis-x.mw-axis") .data(self.plot.variables) .enter().append("g") .attr("class", "mw-axis-x mw-axis"+(conf.guides ? '' : ' mw-no-guides')) .attr("transform", function(d, i) { return "translate(" + (n - i - 1) * self.plot.size + ",0)"; }) .each(function(d) { self.plot.x.scale.domain(self.plot.domainByVariable[d]); d3.select(this).call(self.plot.x.axis); }); self.svgG.selectAll(".mw-axis-y.mw-axis") .data(self.plot.variables) .enter().append("g") .attr("class", "mw-axis-y mw-axis"+(conf.guides ? '' : ' mw-no-guides')) .attr("transform", function(d, i) { return "translate(0," + i * self.plot.size + ")"; }) .each(function(d) { self.plot.y.scale.domain(self.plot.domainByVariable[d]); d3.select(this).call(self.plot.y.axis); }); if(conf.tooltip){ self.plot.tooltip = this.utils.selectOrAppend(d3.select(self.placeholderSelector), 'div.mw-tooltip', 'div') .attr("class", "mw-tooltip") .style("opacity", 0); } var cell = self.svgG.selectAll(".mw-cell") .data(self.utils.cross(self.plot.variables, self.plot.variables)) .enter().append("g") .attr("class", "mw-cell") .attr("transform", function(d) { return "translate(" + (n - d.i - 1) * self.plot.size + "," + d.j * self.plot.size + ")"; }); if(conf.brush){ this.drawBrush(cell); } cell.each(plotSubplot); //Labels cell.filter(function(d) { return d.i === d.j; }).append("text") .attr("x", conf.padding) .attr("y", conf.padding) .attr("dy", ".71em") .text(function(d) { return self.plot.labelByVariable[d.x]; }); function plotSubplot(p) { var plot = self.plot; plot.subplots.push(p); var cell = d3.select(this); plot.x.scale.domain(plot.domainByVariable[p.x]); plot.y.scale.domain(plot.domainByVariable[p.y]); cell.append("rect") .attr("class", "mw-frame") .attr("x", conf.padding / 2) .attr("y", conf.padding / 2) .attr("width", conf.size - conf.padding) .attr("height", conf.size - conf.padding); p.update = function(){ var subplot = this; var dots = cell.selectAll("circle") .data(self.data); dots.enter().append("circle"); dots.attr("cx", function(d){return plot.x.map(d, subplot.x)}) .attr("cy", function(d){return plot.y.map(d, subplot.y)}) .attr("r", self.config.dot.radius); if (plot.dot.color) { dots.style("fill", plot.dot.color) } if(plot.tooltip){ dots.on("mouseover", function(d) { plot.tooltip.transition() .duration(200) .style("opacity", .9); var html = "(" + plot.x.value(d, subplot.x) + ", " +plot.y.value(d, subplot.y) + ")"; plot.tooltip.html(html) .style("left", (d3.event.pageX + 5) + "px") .style("top", (d3.event.pageY - 28) + "px"); var group = self.config.groups.value(d); if(group || group===0 ){ html+="
"; var label = self.config.groups.label; if(label){ html+=label+": "; } html+=group } plot.tooltip.html(html) .style("left", (d3.event.pageX + 5) + "px") .style("top", (d3.event.pageY - 28) + "px"); }) .on("mouseout", function(d) { plot.tooltip.transition() .duration(500) .style("opacity", 0); }); } dots.exit().remove(); }; p.update(); } }; D3ScatterPlotMatrix.prototype.update = function () { this.plot.subplots.forEach(function(p){p.update()}); }; D3ScatterPlotMatrix.prototype.initSvg = function () { var self = this; var config = this.config; var width = self.plot.width+ config.margin.left + config.margin.right; var height = self.plot.height+ config.margin.top + config.margin.bottom; var aspect = width / height; self.svg = d3.select(self.placeholderSelector).select("svg"); if(!self.svg.empty()){ self.svg.remove(); } self.svg = d3.select(self.placeholderSelector).append("svg"); self.svg .attr("width", width) .attr("height", height) .attr("viewBox", "0 0 "+" "+width+" "+height) .attr("preserveAspectRatio", "xMidYMid meet") .attr("class", "mw-d3-scatterplot-matrix"); self.svgG = self.svg.append("g") .attr("class", "mw-container") .attr("transform", "translate(" + config.margin.left + "," + config.margin.top + ")"); if(!config.width || config.height ){ d3.select(window) .on("resize", function() { //TODO add responsiveness if width/height not specified }); } }; D3ScatterPlotMatrix.prototype.init = function () { var self = this; self.initPlot(); self.initSvg(); self.drawPlot(); }; D3ScatterPlotMatrix.prototype.drawBrush = function (cell) { var self = this; var brush = d3.svg.brush() .x(self.plot.x.scale) .y(self.plot.y.scale) .on("brushstart", brushstart) .on("brush", brushmove) .on("brushend", brushend); cell.append("g").call(brush); var brushCell; // Clear the previously-active brush, if any. function brushstart(p) { if (brushCell !== this) { d3.select(brushCell).call(brush.clear()); self.plot.x.scale.domain(self.plot.domainByVariable[p.x]); self.plot.y.scale.domain(self.plot.domainByVariable[p.y]); brushCell = this; } } // Highlight the selected circles. function brushmove(p) { var e = brush.extent(); self.svgG.selectAll("circle").classed("hidden", 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]; }); } // If the brush is empty, select all circles. function brushend() { if (brush.empty()) self.svgG.selectAll(".hidden").classed("hidden", false); } }; function D3ScatterPlot(placeholderSelector, data, config){ var self = this; this.utils = new ChartsD3Utils(); this.placeholderSelector = placeholderSelector; this.svg=null; this.config = undefined; this.defaultConfig = { width: 0, height: 0, guides: false, //show axis guides tooltip: true, //show tooltip on dot hover margin:{ left: 50, right: 30, top: 30, bottom: 50 }, x:{// X axis config label: 'X', // axis label key: 0, value: function(d) { return d[self.config.x.key] }, // x value accessor orient: "bottom", scale: "linear" }, y:{// Y axis config label: 'Y', // axis label, key: 1, value: function(d) { return d[self.config.y.key] }, // y value accessor orient: "left", scale: "linear" }, groups:{ key: 2, value: function(d) { return d[self.config.groups.key] }, // grouping value accessor, label: "" }, dot:{ radius: 2, color: function(d) { return self.config.groups.value(d) }, // string or function returning color's value for color scale d3ColorCategory: 'category10' } }; if(data){ this.setData(data); } if(config){ this.setConfig(config); } this.init(); } D3ScatterPlot.prototype.setData = function (data){ this.data = data; return this; }; D3ScatterPlot.prototype.setConfig = function (config){ this.config = this.utils.deepExtend({}, this.defaultConfig, config); return this; }; D3ScatterPlot.prototype.initPlot = function (){ var self=this; var margin = this.config.margin; var conf = this.config; this.plot={ x: {}, y: {}, dot: { color: null//color scale mapping function } }; var width = conf.width; var placeholderNode = d3.select(this.placeholderSelector).node(); if(!width){ width =placeholderNode.getBoundingClientRect().width; } var height = conf.height; if(!height){ height =placeholderNode.getBoundingClientRect().height; } this.plot.width = width - margin.left - margin.right; this.plot.height = height - margin.top - margin.bottom; this.setupX(); this.setupY(); if(conf.dot.d3ColorCategory){ this.plot.dot.colorCategory = d3.scale[conf.dot.d3ColorCategory](); } var colorValue = conf.dot.color; if(colorValue){ this.plot.dot.colorValue = colorValue; if (typeof colorValue === 'string' || colorValue instanceof String){ this.plot.dot.color = colorValue; }else if(this.plot.dot.colorCategory){ this.plot.dot.color = function(d){ return self.plot.dot.colorCategory(self.plot.dot.colorValue(d)); } } }else{ } return this; }; D3ScatterPlot.prototype.setupX = function (){ var plot = this.plot; var x = plot.x; var conf = this.config.x; /* * value accessor - returns the value to encode for a given data object. * scale - maps value to a visual display encoding, such as a pixel position. * map function - maps from data value to display value * axis - sets up axis */ x.value = conf.value; x.scale = d3.scale[conf.scale]().range([0, plot.width]); x.map = function(d) { return x.scale(x.value(d));}; x.axis = d3.svg.axis().scale(x.scale).orient(conf.orient); var data = this.data; plot.x.scale.domain([d3.min(data, plot.x.value)-1, d3.max(data, plot.x.value)+1]); if(this.config.guides) { x.axis.tickSize(-plot.height); } }; D3ScatterPlot.prototype.setupY = function (){ var plot = this.plot; var y = plot.y; var conf = this.config.y; /* * value accessor - returns the value to encode for a given data object. * scale - maps value to a visual display encoding, such as a pixel position. * map function - maps from data value to display value * axis - sets up axis */ y.value = conf.value; y.scale = d3.scale[conf.scale]().range([plot.height, 0]); y.map = function(d) { return y.scale(y.value(d));}; y.axis = d3.svg.axis().scale(y.scale).orient(conf.orient); if(this.config.guides){ y.axis.tickSize(-plot.width); } var data = this.data; plot.y.scale.domain([d3.min(data, plot.y.value)-1, d3.max(data, plot.y.value)+1]); }; D3ScatterPlot.prototype.draw = function (){ this.drawAxisX(); this.drawAxisY(); this.update(); }; D3ScatterPlot.prototype.drawAxisX = function (){ var self = this; var plot = self.plot; var axisConf = this.config.x; self.svgG.append("g") .attr("class", "mw-axis-x mw-axis"+(self.config.guides ? '' : ' mw-no-guides')) .attr("transform", "translate(0," + plot.height + ")") .call(plot.x.axis) .append("text") .attr("class", "mw-label") .attr("transform", "translate("+ (plot.width/2) +","+ (self.config.margin.bottom) +")") // text is drawn off the screen top left, move down and out and rotate .attr("dy", "-1em") .style("text-anchor", "middle") .text(axisConf.label); }; D3ScatterPlot.prototype.drawAxisY = function (){ var self = this; var plot = self.plot; var axisConf = this.config.y; self.svgG.append("g") .attr("class", "mw-axis mw-axis-y"+(self.config.guides ? '' : ' mw-no-guides')) .call(plot.y.axis) .append("text") .attr("class", "mw-label") .attr("transform", "translate("+ -self.config.margin.left +","+(plot.height/2)+")rotate(-90)") // text is drawn off the screen top left, move down and out and rotate .attr("dy", "1em") .style("text-anchor", "middle") .text(axisConf.label); }; D3ScatterPlot.prototype.update = function (){ var self = this; var plot = self.plot; var data = this.data; var dots = self.svgG.selectAll(".mw-dot") .data(data); dots.enter().append("circle") .attr("class", "mw-dot"); dots.attr("r", self.config.dot.radius) .attr("cx", plot.x.map) .attr("cy", plot.y.map); if(plot.tooltip){ dots.on("mouseover", function(d) { plot.tooltip.transition() .duration(200) .style("opacity", .9); var html = "(" + plot.x.value(d) + ", " +plot.y.value(d) + ")"; var group = self.config.groups.value(d); if(group || group===0 ){ html+="
"; var label = self.config.groups.label; if(label){ html+=label+": "; } html+=group } plot.tooltip.html(html) .style("left", (d3.event.pageX + 5) + "px") .style("top", (d3.event.pageY - 28) + "px"); }) .on("mouseout", function(d) { plot.tooltip.transition() .duration(500) .style("opacity", 0); }); } if(plot.dot.color){ dots.style("fill", plot.dot.color) } dots.exit().remove(); }; D3ScatterPlot.prototype.initSvg = function (){ var self = this; var config = this.config; var width = self.plot.width+ config.margin.left + config.margin.right; var height = self.plot.height+ config.margin.top + config.margin.bottom; var aspect = width / height; self.svg = d3.select(self.placeholderSelector).select("svg"); if(!self.svg.empty()){ self.svg.remove(); } self.svg = d3.select(self.placeholderSelector).append("svg"); self.svg .attr("width", width) .attr("height", height) .attr("viewBox", "0 0 "+" "+width+" "+height) .attr("preserveAspectRatio", "xMidYMid meet") .attr("class", "mw-d3-scatterplot"); self.svgG = self.svg.append("g") .attr("transform", "translate(" + config.margin.left + "," + config.margin.top + ")"); if(config.tooltip){ self.plot.tooltip = this.utils.selectOrAppend(d3.select(self.placeholderSelector), 'div.mw-tooltip', 'div') .attr("class", "mw-tooltip") .style("opacity", 0); } if(!config.width || config.height ){ d3.select(window) .on("resize", function() { //TODO add responsiveness if width/height not specified }); } }; D3ScatterPlot.prototype.init = function (){ var self = this; self.initPlot(); self.initSvg(); self.draw(); }; function ChartsD3Utils() { } // usage example deepExtend({}, objA, objB); => should work similar to $.extend(true, {}, objA, objB); ChartsD3Utils.prototype.deepExtend = function (out) { var utils = this; var emptyOut = {}; if (!out && arguments.length > 1 && Array.isArray(arguments[1])) { out = []; } out = out || {}; for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; if (!source) continue; for (var key in source) { if (!source.hasOwnProperty(key)) { continue; } var isArray = Array.isArray(out[key]); var isObject = utils.isObject(out[key]); var srcObj = utils.isObject(source[key]); if (isObject && !isArray && srcObj) { utils.deepExtend(out[key], source[key]); } else { out[key] = source[key]; } } } return out; }; ChartsD3Utils.prototype.cross = function (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; }; ChartsD3Utils.prototype.inferVariables = function (data, groupKey, includeGroup) { var res = []; if (data.length) { var d = data[0]; if (d instanceof Array) { res= d.map(function (v, i) { return i; }); }else if (typeof d === 'object'){ for (var prop in d) { if(!d.hasOwnProperty(prop)) continue; res.push(prop); } } } if(!includeGroup){ var index = res.indexOf(groupKey); if (index > -1) { res.splice(index, 1); } } return res }; ChartsD3Utils.prototype.isObject = function(a) { return a !== null && typeof a === 'object'; }; ChartsD3Utils.prototype.isNumber = function(a) { return !isNaN(a) && typeof a === 'number'; }; ChartsD3Utils.prototype.isFunction = function(a) { return typeof a === 'function'; }; ChartsD3Utils.prototype.selectOrAppend = function (parent, selector, element) { var selection = parent.select(selector); if(selection.empty()){ return parent.append(element || selector); } return selection; };