1 /** 2 * 3 * 4 * @class An interactive cubbyhole plot. A cubbyhole is a graph of nominal/ordinal data vs. nominal/ordinal data. 5 * The data is wiggled such that the distribution for any given value on the continuous scale is more apparent. 6 * The circular shape is generated such that there is initially separation for small distributions, with larger distributions 7 * packed into the cubbyhole. 8 * 9 * 10 * 11 * The JSON Object passed into the {@link CubbyHole#draw} function as input to the visualization: 12 * 13 * 14 * <pre> { 15 * data_array : {Array}, 16 * xcolumnid : {string}, 17 * ycolumnid : {string}, 18 * valuecolumnid : {string}, 19 * xcolumnvalue : {string}, 20 * ycolumnvalue : {string}, 21 * valuecolumnvalue : {string}, 22 * tooltip_items : {Function}, 23 * fill_style : {Function} or {string}, 24 * stroke_style : {Function} or {string}, 25 * radius : {Number}, 26 * radial_interval : {Number}, 27 * fill_style : {Function} or {string}, 28 * stroke_style : {Function} or {string}, 29 * show_points : {Boolean}, 30 * notifier : {Function}, 31 * PLOT : { 32 * width : {Number}, 33 * height : {Number}, 34 * container : {string} or {HTMLElement} 35 * vertical_padding : {Number}, 36 * horizontal_padding : {Number}, 37 * } 38 * } 39 * </pre> 40 * 41 * @extends vq.Vis 42 */ 43 44 vq.CubbyHole = function() { 45 vq.Vis.call(this); 46 // private variables 47 48 this.height(300); // defaults 49 this.width(700); // defaults 50 this.vertical_padding(20); 51 this.horizontal_padding(30); 52 this.selectedProbesetId(''); 53 54 }; 55 vq.CubbyHole.prototype = pv.extend(vq.Vis); 56 57 vq.CubbyHole.prototype 58 .property('selectedProbesetId'); 59 60 61 /** @private **/ 62 vq.CubbyHole.prototype._setOptionDefaults = function(options) { 63 // PUBLIC OPTIONS 64 // padding 65 if (options.selectedProbesetId) { 66 this._selectedProbesetId = options.selectedProbesetId; 67 } 68 69 if (options.height != null) { 70 this.height(options.height); 71 } 72 73 if (options.width != null) { 74 this.width(options.width); 75 } 76 77 if (options.container != null) { 78 this.container(options.container); 79 } 80 81 if (options.vertical_padding != null) { 82 this.vertical_padding(options.vertical_padding); 83 } 84 85 if (options.horizontal_padding != null) { 86 this.horizontal_padding(options.horizontal_padding); 87 } 88 }; 89 90 91 /** @name vq.CubbyHole.selectedProbesetId **/ 92 vq.CubbyHole.prototype.onProbesetSelect = function(probesetId) { 93 this.selectedProbesetId = probesetId; 94 }; 95 96 vq.CubbyHole.prototype.draw = function(data) { 97 var that = this; 98 99 this.data = new vq.models.CubbyHoleData(data); 100 101 this._setOptionDefaults(this.data); 102 // this._visHeight = this.height() - ( 2 * this.vertical_padding()); 103 // this._visWidth = this.width() - ( 2 * this.horizontal_padding()); 104 105 var div = this.container(); 106 107 if (this.data.isDataReady()) { 108 var dataObj = this.data; 109 110 var x = dataObj.COLUMNID.x; 111 var y = dataObj.COLUMNID.y; 112 var value = dataObj.COLUMNID.value; 113 114 var data_array = dataObj.data; 115 var xScale = pv.Scale.ordinal(dataObj.sortOrderX).splitBanded(0, that.width(),0.8); 116 var yScale = pv.Scale.ordinal(dataObj.sortOrderY).splitBanded(0, that.height(),0.8); 117 var bandWidth = xScale.range().band / 2; 118 var bandHeight = yScale.range().band / 2; 119 var padHeight = bandHeight /4; 120 var padWidth = bandWidth /4; 121 122 //start protovis code 123 124 //identify selected Probeset, if passed in. 125 var selectedProbesetId; 126 127 if (this._selectedProbesetId) { 128 selectedProbesetId = this._selectedProbesetId; 129 } 130 131 var vis = new pv.Panel() 132 .width(that.width()) 133 .height(that.height()) 134 .bottom(that.vertical_padding()) 135 .top(that.vertical_padding()) 136 .left(that.horizontal_padding()) 137 .right(that.horizontal_padding()) 138 .strokeStyle("#aaa") 139 .events("all") 140 .event("mousemove", pv.Behavior.point()) 141 .canvas(div); 142 143 //y-axis ticks 144 vis.add(pv.Rule) 145 .data(yScale.domain()) 146 .bottom(function(val) { return yScale(val) + bandHeight;}) 147 .strokeStyle(function(d) { 148 return d ? "#ccc" : "#999" 149 }) 150 .anchor("left").add(pv.Label); 151 152 //y-axis label 153 vis.add(pv.Label) 154 .text(dataObj.COLUMNLABEL.y) 155 .font(that.font) 156 .textAlign("center") 157 .left(-20) 158 .bottom(this.height() / 2) 159 .textAngle(-1 * Math.PI / 2); 160 161 //x-axis ticks 162 vis.add(pv.Rule) 163 .data(xScale.domain()) 164 .left(function(val) { return xScale(val) + bandWidth;}) 165 .strokeStyle(function(d) { 166 return d ? "#ccc" : "#999" 167 }) 168 .anchor("bottom").add(pv.Label); 169 170 //x-axis label 171 vis.add(pv.Label) 172 .text(dataObj.COLUMNLABEL.x) 173 .font(that.font) 174 .textAlign("center") 175 .bottom(-30) 176 .left(this.width() / 2); 177 178 vis.add(pv.Rule) //y-axis frame 179 .data(yScale.domain()) 180 .bottom(function(val) { return yScale(val) - padHeight;}) 181 .strokeStyle('#444'); 182 vis.add(pv.Rule) //x-axis frame 183 .data(xScale.domain()) 184 .left(function(val) { return xScale(val)- padWidth;}) 185 .strokeStyle('#444'); 186 187 var panel = vis.add(pv.Panel) 188 .events('all') 189 .overflow("hidden"); 190 191 var strokeStyle = function(data) { 192 return pv.color(dataObj._strokeStyle(data)); 193 }; 194 var fillStyle = function(data) { 195 return pv.color(dataObj._fillStyle(data)); 196 }; 197 var violinPanel = panel.add(pv.Panel) 198 .strokeStyle(null) 199 .fillStyle(null); 200 201 var ln2 =Math.LN2; 202 203 var ring_number = function(index) { 204 //ring 0 is size 1 205 //ring 1 is size 4 206 //ring 2 is size 8 207 return index == 0 ? 0 : index <= 4 ? 1 : Math.floor(Math.log(index -1)/ln2); 208 }; 209 210 var ring_size = function(index) { 211 return index == 0 ? 1 : index <= 4 ? 4 : (Math.pow(2,ring_number(index)+1) - Math.pow(2,ring_number(index))); 212 }; 213 214 var theta = function(index) { 215 // theta = index / (ring + 3) in radians 216 // ring 0 217 return (index / ring_size(index) * (2 * Math.PI)) + 218 // phase angle induced by moving outward 219 ((Math.PI / 4) * (ring_number(index)%2)); 220 }; 221 222 var radial_interval = dataObj._radial_interval; 223 224 var radius = function(index) { 225 return (ring_number(index) * radial_interval); 226 }; 227 228 var x_pos =function(index) { 229 return radius(index) * Math.cos(theta(index)); 230 }; 231 var y_pos =function(index) { 232 return radius(index) * Math.sin(theta(index)); 233 }; 234 235 if(dataObj._showPoints) { 236 var dot= violinPanel.add(pv.Dot) 237 .data(data_array) 238 .def("active", -1) 239 .left(function(c) { 240 return xScale(c[x]) + bandWidth + x_pos(c.dist_index); 241 }) 242 .bottom(function(c) { 243 return yScale(c[y])+bandHeight + y_pos(c.dist_index); 244 }) 245 .shape(dataObj._shape) 246 .fillStyle(fillStyle) 247 .strokeStyle(strokeStyle) 248 .radius(dataObj._radius) 249 .event("point", function() { 250 this.active(this.index); 251 return label.render(); 252 }) 253 .event("unpoint", function() { 254 this.active(-1); 255 return label.render(); 256 }) 257 .event('click', dataObj._notifier) 258 259 var label = dot.anchor("right").add(pv.Label) 260 .visible(function() { return this.anchorTarget().active() == this.index; }) 261 .text(function(d) { return dataObj.COLUMNLABEL.value + " " + d[value]; }); 262 } 263 264 /** Update the x- and y-scale domains per the new transform. */ 265 266 vis.render(); 267 } 268 }; 269 270 271 /** 272 * 273 * @class Internal data model for cubbyhole plots. 274 * 275 * @param data {JSON Object} - Configures a cubbyhole plot. 276 * @extends vq.models.VisData 277 */ 278 279 280 vq.models.CubbyHoleData = function(data) { 281 vq.models.VisData.call(this, data); 282 this.setDataModel(); 283 if (this.getDataType() == 'vq.models.CubbyHoleData') { 284 this._build_data(this.getContents()); 285 } else { 286 console.warn('Unrecognized JSON object. Expected vq.models.CubbyHoleData object.'); 287 } 288 }; 289 vq.models.CubbyHoleData.prototype = pv.extend(vq.models.VisData); 290 291 292 vq.models.CubbyHoleData.prototype.setDataModel = function () { 293 this._dataModel = [ 294 {label: 'width', id: 'PLOT.width', cast : Number, defaultValue: 400}, 295 {label: 'height', id: 'PLOT.height', cast : Number, defaultValue: 300}, 296 {label : 'container', id:'PLOT.container', optional : true}, 297 {label: 'vertical_padding', id: 'PLOT.vertical_padding', cast : Number, defaultValue: 20}, 298 {label: 'horizontal_padding', id: 'PLOT.horizontal_padding',cast : Number, defaultValue:30}, 299 {label : 'data', id: 'data_array', defaultValue : [] }, 300 {label : 'COLUMNID.x', id: 'xcolumnid',cast : String, defaultValue : 'X'}, 301 {label : 'COLUMNID.y', id: 'ycolumnid',cast : String, defaultValue : 'Y'}, 302 {label : 'COLUMNID.value', id: 'valuecolumnid',cast : String, defaultValue : 'VALUE'}, 303 {label : 'COLUMNLABEL.x', id: 'xcolumnlabel',cast : String, defaultValue : ''}, 304 {label : 'COLUMNLABEL.y', id: 'ycolumnlabel',cast : String, defaultValue : ''}, 305 {label : 'COLUMNLABEL.value', id: 'valuecolumnlabel',cast : String, defaultValue : ''}, 306 {label : 'tooltipItems', id: 'tooltip_items', defaultValue : { 307 X : 'X', Y : 'Y', Value : 'VALUE' } }, 308 {label : '_fillStyle', id: 'fill_style',cast :vq.utils.VisUtils.wrapProperty, 309 defaultValue : function() { 310 return pv.color('steelblue').alpha(0.2); 311 }}, 312 {label : '_strokeStyle', id: 'stroke_style', 313 cast :vq.utils.VisUtils.wrapProperty, defaultValue : function() { 314 return 'steelblue'; 315 }}, 316 {label : '_radius', id: 'radius',cast :vq.utils.VisUtils.wrapProperty, defaultValue : function() { 317 return 2; }}, 318 {label : '_shape', id: 'shape',cast : vq.utils.VisUtils.wrapProperty, defaultValue : function() { 319 return 'dot'; 320 }}, 321 {label : '_radial_interval', id: 'radial_interval',cast :Number, defaultValue : 6 }, 322 {label : '_showPoints', id: 'show_points',cast :Boolean, defaultValue : true}, 323 {label : '_notifier', id: 'notifier', cast : Function, defaultValue : function() { 324 return null; 325 }} 326 ]; 327 }; 328 329 vq.models.CubbyHoleData.prototype._build_data = function(data) { 330 var that = this; 331 this._processData(data); 332 333 if (this.COLUMNLABEL.x == '') this.COLUMNLABEL.x = this.COLUMNID.x; 334 if (this.COLUMNLABEL.y == '') this.COLUMNLABEL.y = this.COLUMNID.y; 335 if (this.COLUMNLABEL.value == '') this.COLUMNLABEL.value = this.COLUMNID.value; 336 337 var x = this.COLUMNID.x; 338 var y = this.COLUMNID.y; 339 var value = this.COLUMNID.value; 340 341 this.dist = {}; 342 this.dist_index = {}; 343 344 this.data.forEach(function(point) { 345 if (that.dist[point[x]] ===undefined) { 346 that.dist[point[x]] = {}; 347 } 348 if (that.dist[point[x]][point[y]] === undefined) { 349 that.dist[point[x]][point[y]] =0; 350 } 351 point.dist_index = that.dist[point[x]][point[y]]; 352 that.dist[point[x]][point[y]]++; 353 }); 354 355 //maintain a strict ordering on the category labels 356 this.sortOrderX = pv.uniq(that.data, function(a) { return a[x];}).sort(); 357 this.sortOrderY = pv.uniq(that.data, function(a) { return a[y];}).sort(); 358 359 if (this.data.length > 0) this.setDataReady(true); 360 361 362 }; 363