├── LICENSE.txt ├── README.md ├── bower.json ├── demo ├── demo.css ├── demo.csv ├── demo.html └── demo.png ├── scatter-matrix.css └── scatter-matrix.js /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2014 Ginkgo Bioworks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # scatter-matrix.js 3 | 4 | scatter-matrix.js (SM) is a JavaScript library for drawing scatterplot matrix. 5 | SM handles matrix data in CSV format: rows represent samples and columns 6 | represent observations. SM interprets the first row as a header. All numeric 7 | columns appear as rows and columns of the scatterplot matrix. 8 | 9 | SM is a simple extension/generalization of [Mike Bostock's scatterplot matrix 10 | example](http://mbostock.github.io/d3/talk/20111116/iris-splom.html). 11 | Additional features include 12 | 13 | * User can color dots by values of a non-numeric observation. 14 | * User can filter data by values of a non-numeric observation. 15 | * User can decide what numeric observations to include in the matrix. 16 | * User can expand the matrix and view data by fixing one or more observations at set values. 17 | 18 | For demo, see http://benjiec.github.io/scatter-matrix/demo/demo.html 19 | 20 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scatter-matrix", 3 | "version": "0.1.0", 4 | "main": ["scatter-matrix.css", "scatter-matrix.js"], 5 | "ignore": [ "demo", ".gitignore", ".git" ], 6 | "dependencies": { 7 | "d3": "3.3.10" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: auto; 3 | width: 1200px; 4 | font-weight: 300; font-family: "Helvetica Neue"; 5 | overflow: auto; 6 | } 7 | 8 | h2 { font-weight: inherit; margin: 0; } 9 | pre { background-color: #eee; border-radius: 10px; border: 1px solid #ddd; width: 600px; padding: 10px; } 10 | 11 | .scatter-matrix-control { float: left; margin-right: 20px; } 12 | .scatter-matrix-variable-control ul { list-style: none; padding-left: 10px; } 13 | .scatter-matrix-svg { float: left; margin-top: 15px; width: 1000px; } 14 | 15 | -------------------------------------------------------------------------------- /demo/demo.csv: -------------------------------------------------------------------------------- 1 | species,sepal length,sepal width,petal length,petal width 2 | setosa,5.1,3.5,1.4,0.2 3 | setosa,4.9,3,1.4,0.2 4 | setosa,4.7,3.2,1.3,0.2 5 | setosa,4.6,3.1,1.5,0.2 6 | setosa,5,3.6,1.4,0.2 7 | setosa,5.4,3.9,1.7,0.4 8 | setosa,4.6,3.4,1.4,0.3 9 | setosa,5,3.4,1.5,0.2 10 | setosa,4.4,2.9,1.4,0.2 11 | setosa,4.9,3.1,1.5,0.1 12 | setosa,5.4,3.7,1.5,0.2 13 | setosa,4.8,3.4,1.6,0.2 14 | setosa,4.8,3,1.4,0.1 15 | setosa,4.3,3,1.1,0.1 16 | setosa,5.8,4,1.2,0.2 17 | setosa,5.7,4.4,1.5,0.4 18 | setosa,5.4,3.9,1.3,0.4 19 | setosa,5.1,3.5,1.4,0.3 20 | setosa,5.7,3.8,1.7,0.3 21 | setosa,5.1,3.8,1.5,0.3 22 | setosa,5.4,3.4,1.7,0.2 23 | setosa,5.1,3.7,1.5,0.4 24 | setosa,4.6,3.6,1,0.2 25 | setosa,5.1,3.3,1.7,0.5 26 | setosa,4.8,3.4,1.9,0.2 27 | setosa,5,3,1.6,0.2 28 | setosa,5,3.4,1.6,0.4 29 | setosa,5.2,3.5,1.5,0.2 30 | setosa,5.2,3.4,1.4,0.2 31 | setosa,4.7,3.2,1.6,0.2 32 | setosa,4.8,3.1,1.6,0.2 33 | setosa,5.4,3.4,1.5,0.4 34 | setosa,5.2,4.1,1.5,0.1 35 | setosa,5.5,4.2,1.4,0.2 36 | setosa,4.9,3.1,1.5,0.2 37 | setosa,5,3.2,1.2,0.2 38 | setosa,5.5,3.5,1.3,0.2 39 | setosa,4.9,3.6,1.4,0.1 40 | setosa,4.4,3,1.3,0.2 41 | setosa,5.1,3.4,1.5,0.2 42 | setosa,5,3.5,1.3,0.3 43 | setosa,4.5,2.3,1.3,0.3 44 | setosa,4.4,3.2,1.3,0.2 45 | setosa,5,3.5,1.6,0.6 46 | setosa,5.1,3.8,1.9,0.4 47 | setosa,4.8,3,1.4,0.3 48 | setosa,5.1,3.8,1.6,0.2 49 | setosa,4.6,3.2,1.4,0.2 50 | setosa,5.3,3.7,1.5,0.2 51 | setosa,5,3.3,1.4,0.2 52 | versicolor,7,3.2,4.7,1.4 53 | versicolor,6.4,3.2,4.5,1.5 54 | versicolor,6.9,3.1,4.9,1.5 55 | versicolor,5.5,2.3,4,1.3 56 | versicolor,6.5,2.8,4.6,1.5 57 | versicolor,5.7,2.8,4.5,1.3 58 | versicolor,6.3,3.3,4.7,1.6 59 | versicolor,4.9,2.4,3.3,1 60 | versicolor,6.6,2.9,4.6,1.3 61 | versicolor,5.2,2.7,3.9,1.4 62 | versicolor,5,2,3.5,1 63 | versicolor,5.9,3,4.2,1.5 64 | versicolor,6,2.2,4,1 65 | versicolor,6.1,2.9,4.7,1.4 66 | versicolor,5.6,2.9,3.6,1.3 67 | versicolor,6.7,3.1,4.4,1.4 68 | versicolor,5.6,3,4.5,1.5 69 | versicolor,5.8,2.7,4.1,1 70 | versicolor,6.2,2.2,4.5,1.5 71 | versicolor,5.6,2.5,3.9,1.1 72 | versicolor,5.9,3.2,4.8,1.8 73 | versicolor,6.1,2.8,4,1.3 74 | versicolor,6.3,2.5,4.9,1.5 75 | versicolor,6.1,2.8,4.7,1.2 76 | versicolor,6.4,2.9,4.3,1.3 77 | versicolor,6.6,3,4.4,1.4 78 | versicolor,6.8,2.8,4.8,1.4 79 | versicolor,6.7,3,5,1.7 80 | versicolor,6,2.9,4.5,1.5 81 | versicolor,5.7,2.6,3.5,1 82 | versicolor,5.5,2.4,3.8,1.1 83 | versicolor,5.5,2.4,3.7,1 84 | versicolor,5.8,2.7,3.9,1.2 85 | versicolor,6,2.7,5.1,1.6 86 | versicolor,5.4,3,4.5,1.5 87 | versicolor,6,3.4,4.5,1.6 88 | versicolor,6.7,3.1,4.7,1.5 89 | versicolor,6.3,2.3,4.4,1.3 90 | versicolor,5.6,3,4.1,1.3 91 | versicolor,5.5,2.5,4,1.3 92 | versicolor,5.5,2.6,4.4,1.2 93 | versicolor,6.1,3,4.6,1.4 94 | versicolor,5.8,2.6,4,1.2 95 | versicolor,5,2.3,3.3,1 96 | versicolor,5.6,2.7,4.2,1.3 97 | versicolor,5.7,3,4.2,1.2 98 | versicolor,5.7,2.9,4.2,1.3 99 | versicolor,6.2,2.9,4.3,1.3 100 | versicolor,5.1,2.5,3,1.1 101 | versicolor,5.7,2.8,4.1,1.3 102 | virginica,6.3,3.3,6,2.5 103 | virginica,5.8,2.7,5.1,1.9 104 | virginica,7.1,3,5.9,2.1 105 | virginica,6.3,2.9,5.6,1.8 106 | virginica,6.5,3,5.8,2.2 107 | virginica,7.6,3,6.6,2.1 108 | virginica,4.9,2.5,4.5,1.7 109 | virginica,7.3,2.9,6.3,1.8 110 | virginica,6.7,2.5,5.8,1.8 111 | virginica,7.2,3.6,6.1,2.5 112 | virginica,6.5,3.2,5.1,2 113 | virginica,6.4,2.7,5.3,1.9 114 | virginica,6.8,3,5.5,2.1 115 | virginica,5.7,2.5,5,2 116 | virginica,5.8,2.8,5.1,2.4 117 | virginica,6.4,3.2,5.3,2.3 118 | virginica,6.5,3,5.5,1.8 119 | virginica,7.7,3.8,6.7,2.2 120 | virginica,7.7,2.6,6.9,2.3 121 | virginica,6,2.2,5,1.5 122 | virginica,6.9,3.2,5.7,2.3 123 | virginica,5.6,2.8,4.9,2 124 | virginica,7.7,2.8,6.7,2 125 | virginica,6.3,2.7,4.9,1.8 126 | virginica,6.7,3.3,5.7,2.1 127 | virginica,7.2,3.2,6,1.8 128 | virginica,6.2,2.8,4.8,1.8 129 | virginica,6.1,3,4.9,1.8 130 | virginica,6.4,2.8,5.6,2.1 131 | virginica,7.2,3,5.8,1.6 132 | virginica,7.4,2.8,6.1,1.9 133 | virginica,7.9,3.8,6.4,2 134 | virginica,6.4,2.8,5.6,2.2 135 | virginica,6.3,2.8,5.1,1.5 136 | virginica,6.1,2.6,5.6,1.4 137 | virginica,7.7,3,6.1,2.3 138 | virginica,6.3,3.4,5.6,2.4 139 | virginica,6.4,3.1,5.5,1.8 140 | virginica,6,3,4.8,1.8 141 | virginica,6.9,3.1,5.4,2.1 142 | virginica,6.7,3.1,5.6,2.4 143 | virginica,6.9,3.1,5.1,2.3 144 | virginica,5.8,2.7,5.1,1.9 145 | virginica,6.8,3.2,5.9,2.3 146 | virginica,6.7,3.3,5.7,2.5 147 | virginica,6.7,3,5.2,2.3 148 | virginica,6.3,2.5,5,1.9 149 | virginica,6.5,3,5.2,2 150 | virginica,6.2,3.4,5.4,2.3 151 | virginica,5.9,3,5.1,1.8 152 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Fork me on GitHub 13 | 14 |

15 | Scatter-Matrix.js 16 |

17 | 18 |

19 | Explore matrix data (samples as rows, observations as columns) using 20 | scatterplot matrix. Simply supply a CSV with a header. For example: 21 |

22 | 23 |
24 | <script type="text/javascript">
25 |   var sm = new ScatterMatrix('http://my.domain/data.csv');
26 |   sm.render();
27 | </script>
28 | 
29 | 30 | 31 | 32 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /demo/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjiec/scatter-matrix/f8340aca14d6165d8b59f9896a7b7df0f270a376/demo/demo.png -------------------------------------------------------------------------------- /scatter-matrix.css: -------------------------------------------------------------------------------- 1 | svg { font-size: 14px; } 2 | 3 | .axis { shape-rendering: crispEdges; } 4 | .axis line { stroke: #ddd; stroke-width: 1px; } 5 | .axis path { display: none; } 6 | .axis text { font-size: 0.8em; } 7 | 8 | rect.extent { fill: #000; fill-opacity: .125; stroke: #fff; } 9 | rect.frame { fill: #fff; fill-opacity: .7; stroke: #aaa; } 10 | 11 | circle { fill: #ccc; fill-opacity: .5; } 12 | 13 | .legend circle { fill-opacity: 1; } 14 | .legend text { font-size: 18px; font-style: oblique; } 15 | 16 | .cell text { pointer-events: none; } 17 | 18 | .color-0 { fill: #800; } 19 | .color-1 { fill: #080; } 20 | .color-2 { fill: #008; } 21 | .color-3 { fill: #440; } 22 | .color-4 { fill: #044; } 23 | .color-5 { fill: #404; } 24 | .color-6 { fill: #400; } 25 | .color-7 { fill: #040; } 26 | .color-8 { fill: #004; } 27 | .color-9 { fill: #cc0; } 28 | .color-10 { fill: #0cc; } 29 | .color-11 { fill: #c0c; } 30 | .color-12 { fill: #880; } 31 | .color-13 { fill: #088; } 32 | .color-14 { fill: #808; } 33 | .color-15 { fill: #c00; } 34 | .color-16 { fill: #0c0; } 35 | .color-17 { fill: #00c; } 36 | 37 | .scatter-matrix-filter-control ul { list-style: none; padding-left: 10px; } 38 | .scatter-matrix-variable-control ul { list-style: none; padding-left: 10px; } 39 | .scatter-matrix-drill-control ul { list-style: none; padding-left: 10px; } 40 | 41 | -------------------------------------------------------------------------------- /scatter-matrix.js: -------------------------------------------------------------------------------- 1 | // Heavily influenced by Mike Bostock's Scatter Matrix example 2 | // http://mbostock.github.io/d3/talk/20111116/iris-splom.html 3 | // 4 | 5 | ScatterMatrix = function(url, data, dom_id, el) { 6 | this.__url = url; 7 | if (data === undefined || data === null) { this.__data = undefined; } 8 | else { this.__data = d3.csv.parse(data); } 9 | this.__cell_size = 140; 10 | if (dom_id === undefined) { this.__dom_id = 'body'; } 11 | else { this.__dom_id = "#"+dom_id; } 12 | if (el) 13 | this.__dom_id = el; 14 | }; 15 | 16 | ScatterMatrix.prototype.cellSize = function(n) { 17 | this.__cell_size = n; 18 | return this; 19 | }; 20 | 21 | ScatterMatrix.prototype.onData = function(cb) { 22 | if (this.__data) { cb(); return; } 23 | var self = this; 24 | d3.csv(self.__url, function(data) { 25 | self.__data = data; 26 | cb(); 27 | }); 28 | }; 29 | 30 | ScatterMatrix.prototype._numeric_to_str_key = function(k) { return k+'_'; }; 31 | ScatterMatrix.prototype._is_numeric_str_key = function(k) { return k[k.length-1] === '_'; }; 32 | ScatterMatrix.prototype._str_to_numeric_key = function(k) { 33 | if (this._is_numeric_str_key(k)) { return k.slice(0, k.length-1); } 34 | return null; 35 | }; 36 | 37 | ScatterMatrix.prototype.render = function () { 38 | var self = this; 39 | 40 | var container = d3.select(this.__dom_id).append('div') 41 | .attr('class', 'scatter-matrix-container'); 42 | var control = container.append('div') 43 | .attr('class', 'scatter-matrix-control'); 44 | var svg = container.append('div') 45 | .attr('class', 'scatter-matrix-svg') 46 | .html('Loading data...'); 47 | 48 | this.onData(function() { 49 | var data = self.__data; 50 | 51 | // Divide variables into string and numeric variables 52 | 53 | var string_variables = []; 54 | var original_numeric_variables = []; 55 | self.__string_variable_values = {}; 56 | self.__numeric_variables = []; 57 | 58 | for (k in data[0]) { 59 | var is_numeric = true; 60 | data.forEach(function(d) { 61 | var v = d[k]; 62 | if (isNaN(+v)) is_numeric = false; 63 | }); 64 | if (is_numeric) { 65 | self.__numeric_variables.push(k); 66 | original_numeric_variables.push(k); 67 | } 68 | else { 69 | string_variables.push(k); 70 | self.__string_variable_values[k] = []; 71 | } 72 | } 73 | 74 | // For string variables, make a numeric counterpart that has as value the 75 | // index of the value 76 | 77 | data.forEach(function(d) { 78 | for (var j in string_variables) { 79 | var k = string_variables[j]; 80 | var value = d[k]; 81 | if (self.__string_variable_values[k].indexOf(value) < 0) 82 | self.__string_variable_values[k].push(value); 83 | } 84 | }); 85 | 86 | // sort, then assign index 87 | for (var j in string_variables) { 88 | var k = string_variables[j]; 89 | self.__string_variable_values[k].sort(); 90 | } 91 | data.forEach(function(d) { 92 | for (var j in string_variables) { 93 | var k = string_variables[j]; 94 | var value = d[k]; 95 | var index = self.__string_variable_values[k].indexOf(value); 96 | d[self._numeric_to_str_key(k)] = index; 97 | } 98 | }); 99 | 100 | for (var j in string_variables) { 101 | var k = string_variables[j]; 102 | self.__numeric_variables.push(self._numeric_to_str_key(k)); 103 | } 104 | 105 | // Add controls on the left 106 | 107 | var size_control = control.append('div').attr('class', 'scatter-matrix-size-control'); 108 | var color_control = control.append('div').attr('class', 'scatter-matrix-color-control'); 109 | var filter_control = control.append('div').attr('class', 'scatter-matrix-filter-control'); 110 | var variable_control = control.append('div').attr('class', 'scatter-matrix-variable-control'); 111 | var drill_control = control.append('div').attr('class', 'scatter-matrix-drill-control'); 112 | 113 | // shared control states 114 | var to_include = self.__numeric_variables.slice(0, 5); 115 | var color_variable = undefined; 116 | var selected_colors = undefined; 117 | var drill_variables = []; 118 | 119 | function set_filter(variable) { 120 | filter_control.selectAll('*').remove(); 121 | if (variable) { 122 | // Get unique values for this variable 123 | var values = []; 124 | data.forEach(function(d) { 125 | var v = d[variable]; 126 | if (values.indexOf(v) < 0) { values.push(v); } 127 | }); 128 | 129 | selected_colors = values.slice(0, 5); 130 | 131 | var filter_li = 132 | filter_control 133 | .append('p').text('Filter by '+variable+': ') 134 | .append('ul') 135 | .selectAll('li') 136 | .data(values) 137 | .enter().append('li'); 138 | 139 | filter_li.append('input') 140 | .attr('type', 'checkbox') 141 | .attr('checked', function(d, i) { 142 | if (selected_colors.indexOf(d) >= 0) 143 | return 'checked'; 144 | return null; 145 | }) 146 | .on('click', function(d, i) { 147 | var new_selected_colors = []; 148 | for (var j in selected_colors) { 149 | var v = selected_colors[j]; 150 | if (v !== d || this.checked) { new_selected_colors.push(v); } 151 | } 152 | if (this.checked) { new_selected_colors.push(d); } 153 | selected_colors = new_selected_colors; 154 | self.__draw(self.__cell_size, svg, color_variable, 155 | selected_colors, to_include, drill_variables); 156 | }); 157 | filter_li.append('label') 158 | .html(function(d) { return d; }); 159 | } 160 | } 161 | 162 | size_a = size_control.append('p').text('Change plot size: '); 163 | size_a.append('a') 164 | .attr('href', 'javascript:void(0);') 165 | .html('-') 166 | .on('click', function() { 167 | self.__cell_size *= 0.75; 168 | self.__draw(self.__cell_size, svg, color_variable, selected_colors, to_include, drill_variables); 169 | }); 170 | size_a.append('span').html(' '); 171 | size_a.append('a') 172 | .attr('href', 'javascript:void(0);') 173 | .html('+') 174 | .on('click', function() { 175 | self.__cell_size *= 1.25; 176 | self.__draw(self.__cell_size, svg, color_variable, selected_colors, to_include, drill_variables); 177 | }); 178 | 179 | color_control.append('p').text('Select a variable to color:'); 180 | color_control 181 | .append('ul') 182 | .selectAll('li') 183 | .data([undefined].concat(string_variables)) 184 | .enter().append('li') 185 | .append('a') 186 | .attr('href', 'javascript:void(0);') 187 | .text(function(d) { return d ? d : 'None'; }) 188 | .on('click', function(d, i) { 189 | color_variable = d; 190 | set_filter(d); 191 | self.__draw(self.__cell_size, svg, color_variable, selected_colors, to_include, drill_variables); 192 | }); 193 | 194 | var variable_li = 195 | variable_control 196 | .append('p').text('Include variables: ') 197 | .append('ul') 198 | .selectAll('li') 199 | .data(self.__numeric_variables) 200 | .enter().append('li'); 201 | 202 | variable_li.append('input') 203 | .attr('type', 'checkbox') 204 | .attr('checked', function(d, i) { if (to_include.indexOf(d) >= 0) return "checked"; return null; }) 205 | .on('click', function(d, i) { 206 | var new_to_include = []; 207 | for (var j in to_include) { 208 | var v = to_include[j]; 209 | if (v !== d || this.checked) { new_to_include.push(v); } 210 | } 211 | if (this.checked) { new_to_include.push(d); } 212 | to_include = new_to_include; 213 | self.__draw(self.__cell_size, svg, color_variable, selected_colors, to_include, drill_variables); 214 | }); 215 | variable_li.append('label') 216 | .html(function(d) { 217 | var i = self.__numeric_variables.indexOf(d)+1; 218 | return ''+i+': '+d; 219 | }); 220 | 221 | drill_li = 222 | drill_control 223 | .append('p').text('Drill and Expand: ') 224 | .append('ul') 225 | .selectAll('li') 226 | .data(original_numeric_variables.concat(string_variables)) 227 | .enter().append('li'); 228 | 229 | drill_li.append('input') 230 | .attr('type', 'checkbox') 231 | .on('click', function(d, i) { 232 | var new_drill_variables = []; 233 | for (var j in drill_variables) { 234 | var v = drill_variables[j]; 235 | if (v !== d || this.checked) { new_drill_variables.push(v); } 236 | } 237 | if (this.checked) { new_drill_variables.push(d); } 238 | drill_variables = new_drill_variables; 239 | self.__draw(self.__cell_size, svg, color_variable, selected_colors, to_include, drill_variables); 240 | }); 241 | drill_li.append('label') 242 | .html(function(d) { return d; }); 243 | 244 | self.__draw(self.__cell_size, svg, color_variable, selected_colors, to_include, drill_variables); 245 | }); 246 | }; 247 | 248 | ScatterMatrix.prototype.__draw = 249 | function(cell_size, container_el, color_variable, selected_colors, to_include, drill_variables) { 250 | var self = this; 251 | this.onData(function() { 252 | var data = self.__data; 253 | 254 | // filter data by selected colors 255 | if (color_variable && selected_colors) { 256 | data = []; 257 | self.__data.forEach(function(d) { 258 | if (selected_colors.indexOf(d[color_variable]) >= 0) { data.push(d); } 259 | }); 260 | } 261 | 262 | container_el.selectAll('*').remove(); 263 | 264 | // If no data, don't do anything 265 | if (data.length == 0) { return; } 266 | 267 | // Parse headers from first row of data 268 | var variables_to_draw = to_include.slice(0); 269 | 270 | // Get values of the string variable 271 | var colors = []; 272 | if (color_variable) { 273 | // Using self.__data (all data), instead of data (data to be drawn), so 274 | // our css classes are consistent when we filter by value. 275 | self.__data.forEach(function(d) { 276 | var s = d[color_variable]; 277 | if (colors.indexOf(s) < 0) { colors.push(s); } 278 | }); 279 | } 280 | 281 | function color_class(d) { 282 | var c = d; 283 | if (color_variable && d[color_variable]) { c = d[color_variable]; } 284 | return colors.length > 0 ? 'color-'+colors.indexOf(c) : 'color-2'; 285 | } 286 | 287 | // Size parameters 288 | var size = cell_size, padding = 10, 289 | axis_width = 20, axis_height = 15, legend_width = 200, label_height = 15; 290 | 291 | // Get x and y scales for each numeric variable 292 | var x = {}, y = {}; 293 | variables_to_draw.forEach(function(trait) { 294 | // Coerce values to numbers. 295 | data.forEach(function(d) { d[trait] = +d[trait]; }); 296 | 297 | var value = function(d) { return d[trait]; }, 298 | domain = [d3.min(data, value), d3.max(data, value)], 299 | range_x = [padding / 2, size - padding / 2], 300 | range_y = [padding / 2, size - padding / 2]; 301 | 302 | x[trait] = d3.scale.linear().domain(domain).range(range_x); 303 | y[trait] = d3.scale.linear().domain(domain).range(range_y.reverse()); 304 | }); 305 | 306 | // When drilling, user select one or more variables. The first drilled 307 | // variable becomes the x-axis variable for all columns, and each column 308 | // contains only data points that match specific values for each of the 309 | // drilled variables other than the first. 310 | 311 | var drill_values = []; 312 | var drill_degrees = [] 313 | drill_variables.forEach(function(variable) { 314 | // Skip first one, since that's just the x axis 315 | if (drill_values.length == 0) { 316 | drill_values.push([]); 317 | drill_degrees.push(1); 318 | } 319 | else { 320 | var values = []; 321 | data.forEach(function(d) { 322 | var v = d[variable]; 323 | if (v !== undefined && values.indexOf(v) < 0) { values.push(v); } 324 | }); 325 | values.sort(); 326 | drill_values.push(values); 327 | drill_degrees.push(values.length); 328 | } 329 | }); 330 | var total_columns = 1; 331 | drill_degrees.forEach(function(d) { total_columns *= d; }); 332 | 333 | // Pick out stuff to draw on horizontal and vertical dimensions 334 | 335 | if (drill_variables.length > 0) { 336 | // First drill is now the x-axis variable for all columns 337 | x_variables = []; 338 | for (var i=0; i 0) { 346 | // Don't draw any of the "drilled" variables in vertical dimension 347 | y_variables = []; 348 | variables_to_draw.forEach(function(variable) { 349 | if (drill_variables.indexOf(variable) < 0) { y_variables.push(variable); } 350 | }); 351 | } 352 | else 353 | y_variables = variables_to_draw.slice(0); 354 | y_variables = y_variables.reverse(); 355 | var filter_descriptions = 0; 356 | if (drill_variables.length > 1) { 357 | filter_descriptions = drill_variables.length-1; 358 | } 359 | 360 | // Formatting for axis 361 | var intf = d3.format('d'); 362 | var fltf = d3.format('.f'); 363 | var scif = d3.format('.1e'); 364 | 365 | // Brush - for highlighting regions of data 366 | var brush = d3.svg.brush() 367 | .on("brushstart", brushstart) 368 | .on("brush", brush) 369 | .on("brushend", brushend); 370 | 371 | // Root panel 372 | var svg = container_el.append("svg:svg") 373 | .attr("width", label_height + size * x_variables.length + axis_width + padding + legend_width) 374 | .attr("height", size * y_variables.length + axis_height + label_height + label_height*filter_descriptions) 375 | .append("svg:g") 376 | .attr("transform", "translate("+label_height+",0)"); 377 | 378 | // Push legend to the side 379 | var legend = svg.selectAll("g.legend") 380 | .data(colors) 381 | .enter().append("svg:g") 382 | .attr("class", "legend") 383 | .attr("transform", function(d, i) { 384 | return "translate(" + (label_height + size * x_variables.length + padding) + "," + (i*20+10) + ")"; 385 | }); 386 | 387 | legend.append("svg:circle") 388 | .attr("class", function(d, i) { return color_class(d); }) 389 | .attr("r", 3); 390 | 391 | legend.append("svg:text") 392 | .attr("x", 12) 393 | .attr("dy", ".31em") 394 | .text(function(d) { return d; }); 395 | 396 | var shorten = function (s) { 397 | if (s.length > 16) 398 | return s.slice(0, 12)+'...'+s.slice(s.length-8, s.length); 399 | return s; 400 | }; 401 | 402 | var reshape_axis = function (axis, k) { 403 | if (self._is_numeric_str_key(k)) { 404 | var sk = self._str_to_numeric_key(k); 405 | axis.tickFormat(function(d) { return self.__string_variable_values[sk][d]; }); 406 | if (self.__string_variable_values[sk].length < 10) 407 | axis.ticks(self.__string_variable_values[sk].length); 408 | else 409 | axis.ticks(2); 410 | } 411 | else 412 | axis.ticks(5) 413 | .tickFormat(function (d) { 414 | if (Math.abs(+d) > 10000 || (Math.abs(d) < 0.001 && Math.abs(d) != 0)) { return scif(d); } 415 | if (parseInt(d) == +d) { return intf(d); } 416 | return fltf(d); 417 | }); 418 | return axis; 419 | }; 420 | 421 | // Draw X-axis 422 | svg.selectAll("g.x.axis") 423 | .data(x_variables) 424 | .enter().append("svg:g") 425 | .attr("class", "x axis") 426 | .attr("transform", function(d, i) { return "translate(" + i * size + ",0)"; }) 427 | .each(function(k) { 428 | var axis = reshape_axis(d3.svg.axis(), k); 429 | axis.tickSize(size * y_variables.length); 430 | d3.select(this).call(axis.scale(x[k]).orient('bottom')); 431 | }); 432 | 433 | // Draw Y-axis 434 | svg.selectAll("g.y.axis") 435 | .data(y_variables) 436 | .enter().append("svg:g") 437 | .attr("class", "y axis") 438 | .attr("transform", function(d, i) { return "translate(0," + i * size + ")"; }) 439 | .each(function(k) { 440 | var axis = reshape_axis(d3.svg.axis(), k); 441 | axis.tickSize(size * x_variables.length); 442 | d3.select(this).call(axis.scale(y[k]).orient('right')); 443 | }); 444 | 445 | // Draw scatter plot 446 | 447 | var cell = svg.selectAll("g.cell") 448 | .data(cross(x_variables, y_variables)) 449 | .enter().append("svg:g") 450 | .attr("class", "cell") 451 | .attr("transform", function(d) { return "translate(" + d.i * size + "," + d.j * size + ")"; }) 452 | .each(plot); 453 | 454 | // Add titles for y variables 455 | cell.filter(function(d) { return d.i == 0; }).append("svg:text") 456 | .attr("x", padding-size) 457 | .attr("y", -label_height) 458 | .attr("dy", ".71em") 459 | .attr("transform", function(d) { return "rotate(-90)"; }) 460 | .text(function(d) { 461 | var s = self.__numeric_variables.indexOf(d.y)+1; 462 | s = ''+s+': '+d.y; 463 | return shorten(s); 464 | }); 465 | 466 | function plot(p) { 467 | // console.log(p); 468 | 469 | var data_to_draw = data; 470 | 471 | // If drilling, compute what values of the drill variables correspond to 472 | // this column. 473 | // 474 | var filter = {}; 475 | if (drill_variables.length > 1) { 476 | var column = p.i; 477 | 478 | var cap = 1; 479 | for (var i=drill_variables.length-1; i > 0; i--) { 480 | var var_name = drill_variables[i]; 481 | var var_value = undefined; 482 | 483 | if (i == drill_variables.length-1) { 484 | // for the last drill variable, we index by % 485 | var_value = drill_values[i][column % drill_degrees[i]]; 486 | } 487 | else { 488 | // otherwise divide by capacity of subsequent variables to get value array index 489 | var_value = drill_values[i][parseInt(column/cap)]; 490 | } 491 | 492 | filter[var_name] = var_value; 493 | cap *= drill_degrees[i]; 494 | } 495 | 496 | data_to_draw = []; 497 | data.forEach(function(d) { 498 | var pass = true; 499 | for (k in filter) { if (d[k] != filter[k]) { pass = false; break; } } 500 | if (pass === true) { data_to_draw.push(d); } 501 | }); 502 | } 503 | 504 | var cell = d3.select(this); 505 | 506 | // Frame 507 | cell.append("svg:rect") 508 | .attr("class", "frame") 509 | .attr("x", padding / 2) 510 | .attr("y", padding / 2) 511 | .attr("width", size - padding) 512 | .attr("height", size - padding); 513 | 514 | // Scatter plot dots 515 | cell.selectAll("circle") 516 | .data(data_to_draw) 517 | .enter().append("svg:circle") 518 | .attr("class", function(d) { return color_class(d); }) 519 | .attr("cx", function(d) { return x[p.x](d[p.x]); }) 520 | .attr("cy", function(d) { return y[p.y](d[p.y]); }) 521 | .attr("r", 5); 522 | 523 | // Add titles for x variables and drill variable values 524 | if (p.j == y_variables.length-1) { 525 | cell.append("svg:text") 526 | .attr("x", padding) 527 | .attr("y", size+axis_height) 528 | .attr("dy", ".71em") 529 | .text(function(d) { 530 | var s = self.__numeric_variables.indexOf(d.x)+1; 531 | s = ''+s+': '+d.x; 532 | return shorten(s); 533 | }); 534 | 535 | if (drill_variables.length > 1) { 536 | var i = 0; 537 | for (k in filter) { 538 | i += 1; 539 | cell.append("svg:text") 540 | .attr("x", padding) 541 | .attr("y", size+axis_height+label_height*i) 542 | .attr("dy", ".71em") 543 | .text(function(d) { return shorten(filter[k]+': '+k); }); 544 | } 545 | } 546 | } 547 | 548 | // Brush 549 | cell.call(brush.x(x[p.x]).y(y[p.y])); 550 | } 551 | 552 | // Clear the previously-active brush, if any 553 | function brushstart(p) { 554 | if (brush.data !== p) { 555 | cell.call(brush.clear()); 556 | brush.x(x[p.x]).y(y[p.y]).data = p; 557 | } 558 | } 559 | 560 | // Highlight selected circles 561 | function brush(p) { 562 | var e = brush.extent(); 563 | svg.selectAll(".cell circle").attr("class", function(d) { 564 | return e[0][0] <= d[p.x] && d[p.x] <= e[1][0] 565 | && e[0][1] <= d[p.y] && d[p.y] <= e[1][1] 566 | ? color_class(d) : null; 567 | }); 568 | } 569 | 570 | // If brush is empty, select all circles 571 | function brushend() { 572 | if (brush.empty()) svg.selectAll(".cell circle").attr("class", function(d) { 573 | return color_class(d); 574 | }); 575 | } 576 | 577 | function cross(a, b) { 578 | var c = [], n = a.length, m = b.length, i, j; 579 | for (i = -1; ++i < n;) for (j = -1; ++j < m;) c.push({x: a[i], i: i, y: b[j], j: j}); 580 | return c; 581 | } 582 | }); 583 | 584 | }; 585 | --------------------------------------------------------------------------------