1 /** 2 * 3 * 4 * @class An interactive violinplot. A violinplot is a graph of nominal/ordinal data vs. interval/continuous data. 5 * The data is wiggled such that the distribution for any given value on the continuous scale is more apparent. 6 * The "violin" (or stingray) shape is due to the empirical distribution calculated over the entire 7 * 8 * 9 * 10 * The JSON Object passed into the {@link ViolinPlot#draw} function as input to the visualization: 11 * 12 * 13 * <pre> { 14 * data_array : {Array}, 15 * xcolumnid : {string}, 16 * ycolumnid : {string}, 17 * valuecolumnid : {string}, 18 * xcolumnvalue : {string}, 19 * ycolumnvalue : {string}, 20 * valuecolumnvalue : {string}, 21 * tooltip_items : {Function}, 22 * fill_style : {Function} or {string}, 23 * stroke_style : {Function} or {string}, 24 * radius : {Number}, 25 * fill_style : {Function} or {string}, 26 * stroke_style : {Function} or {string}, 27 * show_points : {Boolean}, 28 * notifier : {Function}, 29 * PLOT : { 30 * width : {Number}, 31 * height : {Number}, 32 * container : {string} or {HTMLElement} 33 * vertical_padding : {Number}, 34 * horizontal_padding : {Number}, 35 * } 36 * } 37 * </pre> 38 * 39 * @extends vq.Vis 40 */ 41 42 43 vq.ViolinPlot = function() { 44 vq.Vis.call(this); 45 // private variables 46 47 this.height(300); // defaults 48 this.width(700); // defaults 49 this.vertical_padding(20); 50 this.horizontal_padding(30); 51 this.selectedProbesetId(''); 52 53 }; 54 vq.ViolinPlot.prototype = pv.extend(vq.Vis); 55 56 /** @name vq.ViolinPlot.selectedProbesetId **/ 57 58 vq.ViolinPlot.prototype 59 .property('selectedProbesetId'); 60 61 /** @private **/ 62 vq.ViolinPlot.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 vq.ViolinPlot.prototype.onProbesetSelect = function(probesetId) { 91 this.selectedProbesetId = probesetId; 92 }; 93 94 /** 95 * 96 * Constructs the ViolinPlot model and adds the SVG tags to the defined DOM element. 97 * 98 * @param {JSON Object} violinplot_object - the object defined above. 99 */ 100 101 vq.ViolinPlot.prototype.draw = function(data) { 102 var that = this; 103 104 this.data = new vq.models.ViolinPlotData(data); 105 106 this._setOptionDefaults(this.data); 107 108 var div = this.container(); 109 110 function trans() { 111 var t = this.transform().invert(); 112 var w = that.width(); 113 var h = that.height(); 114 var halfY = (showMaxY - showMinY) / 2, 115 centerY = showMaxY - halfY, 116 scaleY = 2 * halfY / h; 117 118 //xScale.domain(t.x * scaleX - (halfX) + centerX, centerX + (w * t.k + t.x) * scaleX - halfX); 119 yScale.domain(-1 * ( h * t.k + (t.y)) * scaleY + halfY + centerY, -1 * ( t.y) * scaleY + halfY + centerY); 120 vis.render(); 121 } 122 123 if (this.data.isDataReady()) { 124 var dataObj = this.data; 125 126 var x = dataObj.COLUMNID.x; 127 var y = dataObj.COLUMNID.y; 128 var value = dataObj.COLUMNID.value; 129 130 var data_array = dataObj.data; 131 var data_summary = dataObj.data_summary; 132 var summary_map = {}; 133 var highest = -9999999,lowest = 9999999; 134 if (typeof data_array[0][x] == 'number') data_array.sort(function(a,b) { return a[x]-b[x];} ); //sort numerically ascending 135 var xScale = pv.Scale.ordinal(data_array,function(val){return val[x];}).splitBanded(0, that.width(),0.8); 136 var bandWidth = xScale.range().band / 2; 137 138 data_summary.forEach(function(category) { 139 var minY = pv.min(category[y]); 140 var maxY = pv.max(category[y]); 141 var sampleCount = category[y].length; 142 143 category.bottom = minY; 144 category.top = maxY; 145 category.mean = pv.mean(category[y]); 146 if (sampleCount <=4) { 147 summary_map[category[x]] = category; 148 category.dist=[]; 149 category.bandScale=0; 150 category.setSize=1; 151 highest = maxY > highest ? maxY : highest; 152 lowest = minY < lowest ? minY : lowest; 153 return; 154 } 155 var quartiles = pv.Scale.quantile(category[y]).quantiles(4).quantiles(); 156 //Freedman-Diaconis' choice for bin size 157 var setSize = 2 * (quartiles[3] - quartiles[1]) / Math.pow(sampleCount,0.33); 158 category.dist = pv.range(minY- 3*setSize/2,maxY+ 3*setSize/2,setSize).map(function(subset) { 159 return {position : subset + setSize/2, 160 value : category[y].filter(function(val) { return val >= subset && val < subset + setSize;}).length/category[y].length}; 161 }); 162 category.bandScale = pv.Scale.linear(0,pv.max(category.dist,function(val) { return val.value;})).range(0,bandWidth); 163 highest = maxY+ 3*setSize/2 > highest ? maxY+ 3*setSize/2 : highest; 164 lowest = minY- 3*setSize/2 < lowest ? minY- 3*setSize/2 : lowest; 165 category.setSize=setSize; 166 summary_map[category[x]] = category; 167 }); 168 169 delete data_summary; 170 171 //expand plot around highest/lowest values 172 var showMinY = lowest - (highest - lowest) / 15; 173 var showMaxY = highest + (highest - lowest) /15; 174 175 //start protovis code 176 177 var yScale = pv.Scale.linear(showMinY, showMaxY).range(0, that.height()); 178 179 //identify selected Probeset, if passed in. 180 var selectedProbesetId; 181 182 if (this._selectedProbesetId) { 183 selectedProbesetId = this._selectedProbesetId; 184 } 185 186 var vis = new pv.Panel() 187 .width(that.width()) 188 .height(that.height()) 189 .top(that.vertical_padding()) 190 .bottom(that.vertical_padding()) 191 .left(that.horizontal_padding()) 192 .right(that.horizontal_padding()) 193 .strokeStyle("#aaa") 194 .events("all") 195 .event("mousemove", pv.Behavior.point()) 196 .canvas(div); 197 198 //y-axis ticks 199 vis.add(pv.Rule) 200 .data(function() { 201 return yScale.ticks() 202 }) 203 .bottom(yScale) 204 .strokeStyle(function(d) { 205 return d ? "#ccc" : "#999" 206 }) 207 .anchor("left").add(pv.Label) 208 .text(yScale.tickFormat); 209 210 //y-axis label 211 vis.add(pv.Label) 212 .text(dataObj.COLUMNLABEL.y) 213 .font(that.font) 214 .textAlign("center") 215 .left(-24) 216 .bottom(this.height() / 2) 217 .textAngle(-1 * Math.PI / 2); 218 219 //x-axis ticks 220 vis.add(pv.Rule) 221 .data(xScale.domain()) 222 .left(function(val) { return xScale(val) + bandWidth;}) 223 .strokeStyle(function(d) { 224 return d ? "#ccc" : "#999" 225 }) 226 .anchor("bottom").add(pv.Label); 227 228 //x-axis label 229 vis.add(pv.Label) 230 .text(dataObj.COLUMNLABEL.x) 231 .font(that.font) 232 .textAlign("center") 233 .bottom(-30) 234 .left(this.width() / 2); 235 236 var panel = vis.add(pv.Panel) 237 .events('all') 238 .overflow("hidden"); 239 240 var strokeStyle = function(data) { 241 return pv.color(dataObj._strokeStyle(data)); 242 }; 243 var fillStyle = function(data) { 244 return pv.color(dataObj._fillStyle(data)); 245 }; 246 var violinPanel = panel.add(pv.Panel) 247 .data(xScale.domain()) 248 .strokeStyle(null) 249 .fillStyle(null); 250 251 //mean of distribution 252 violinPanel.add(pv.Bar) 253 .left(function(c) {return xScale(c);}) 254 .visible(function(c) { return summary_map[c].dist.length;}) 255 .width(bandWidth * 2) 256 .height(2) 257 .bottom(function(label) {return yScale(summary_map[label].mean)-1;}) 258 .fillStyle('rgb(255,0,0,0.6)'); 259 260 //left side of distribution 261 violinPanel.add(pv.Line) 262 .data(function(label){return summary_map[label].dist;}) 263 .strokeStyle('black') 264 .lineWidth(1) 265 .left(function(set,label) { return xScale(label) + bandWidth - summary_map[label].bandScale(set.value);}) 266 .bottom(function(set) { return yScale(set.position);}); 267 268 //right side of distribution 269 violinPanel.add(pv.Line) 270 .data(function(label){return summary_map[label].dist;}) 271 .strokeStyle('black') 272 .lineWidth(1) 273 .left(function(set,label) { return xScale(label) + bandWidth + summary_map[label].bandScale(set.value);}) 274 .bottom(function(set) { return yScale(set.position);}); 275 276 //data points 277 if(dataObj._showPoints) { 278 violinPanel.add(pv.Dot) 279 .def("active", -1) 280 .data(data_array) 281 .left(function(c) { 282 //if only one point in distribution, just put it on the axis 283 if (summary_map[c[x]].dist.length < 1) {return xScale(c[x]) + bandWidth;} 284 //if more than one point in distribution, wiggle it around 285 var distSize = summary_map[c[x]].dist[Math.floor((c[y]-summary_map[c[x]].bottom)/summary_map[c[x]].setSize)].value; 286 var distSize2 = summary_map[c[x]].dist[Math.ceil((c[y]-summary_map[c[x]].bottom)/summary_map[c[x]].setSize)].value; 287 var average = (distSize +distSize2) / 3; 288 return xScale(c[x]) + bandWidth + summary_map[c[x]].bandScale(Math.cos(this.index%(summary_map[c[x]][y].length/3))*average); 289 }) 290 .bottom(function(c) { return yScale(c[y]);}) 291 .shape(dataObj._shape) 292 .fillStyle(fillStyle) 293 .strokeStyle(strokeStyle) 294 .radius(dataObj._radius) 295 .event("point", function() { 296 return this.active(this.index).parent; 297 }) 298 .event("unpoint", function() { 299 return this.active(-1).parent; 300 }) 301 .event('click', dataObj._notifier) 302 .anchor("right").add(pv.Label) 303 .visible(function() { return this.anchorTarget().active() == this.index; }) 304 .text(function(d) { return dataObj.COLUMNLABEL.value + " " + d[value]; }); 305 } 306 307 /* Use an invisible panel to capture pan & zoom events. */ 308 vis.add(pv.Panel) 309 .left(0) 310 .bottom(0) 311 .events("all") 312 .event("mousedown", pv.Behavior.pan()) 313 .event("mousewheel", pv.Behavior.zoom()) 314 .event("pan", trans) 315 .event("zoom", trans); 316 317 /** Update the x- and y-scale domains per the new transform. */ 318 319 vis.render(); 320 } 321 }; 322 323 324 325 /** 326 * 327 * @class Internal data model for violin plots. 328 * 329 * @param data {JSON Object} - Configures a violin plot. 330 * @extends vq.models.VisData 331 */ 332 333 vq.models.ViolinPlotData = function(data) { 334 vq.models.VisData.call(this, data); 335 this.setDataModel(); 336 if (this.getDataType() == 'vq.models.ViolinPlotData') { 337 this._build_data(this.getContents()); 338 } else { 339 console.warn('Unrecognized JSON object. Expected vq.models.ViolinPlotData object.'); 340 } 341 }; 342 vq.models.ViolinPlotData.prototype = pv.extend(vq.models.VisData); 343 344 345 vq.models.ViolinPlotData.prototype.setDataModel = function () { 346 this._dataModel = [ 347 {label: 'width', id: 'PLOT.width', cast : Number, defaultValue: 400}, 348 {label: 'height', id: 'PLOT.height', cast : Number, defaultValue: 300}, 349 {label : 'container', id:'PLOT.container', optional : true}, 350 {label: 'vertical_padding', id: 'PLOT.vertical_padding', cast : Number, defaultValue: 20}, 351 {label: 'horizontal_padding', id: 'PLOT.horizontal_padding',cast : Number, defaultValue:30}, 352 {label : 'data', id: 'data_array', defaultValue : [] }, 353 {label : 'COLUMNID.x', id: 'xcolumnid',cast : String, defaultValue : 'X'}, 354 {label : 'COLUMNID.y', id: 'ycolumnid',cast : String, defaultValue : 'Y'}, 355 {label : 'COLUMNID.value', id: 'valuecolumnid',cast : String, defaultValue : 'VALUE'}, 356 {label : 'COLUMNLABEL.x', id: 'xcolumnlabel',cast : String, defaultValue : ''}, 357 {label : 'COLUMNLABEL.y', id: 'ycolumnlabel',cast : String, defaultValue : ''}, 358 {label : 'COLUMNLABEL.value', id: 'valuecolumnlabel',cast : String, defaultValue : ''}, 359 {label : 'tooltipItems', id: 'tooltip_items', defaultValue : { 360 X : 'X', Y : 'Y', Value : 'VALUE' } }, 361 {label : '_fillStyle', id: 'fill_style',cast :vq.utils.VisUtils.wrapProperty, 362 defaultValue : function() { 363 return pv.color('steelblue').alpha(0.2); 364 }}, 365 {label : '_strokeStyle', id: 'stroke_style', 366 cast :vq.utils.VisUtils.wrapProperty, defaultValue : function() { 367 return 'steelblue'; 368 }}, 369 {label : '_radius', id: 'radius',cast :vq.utils.VisUtils.wrapProperty, defaultValue : function() { 370 return 2; 371 }}, 372 {label : '_shape', id: 'shape',cast : vq.utils.VisUtils.wrapProperty, defaultValue : function() { 373 return 'dot'; 374 }}, 375 {label : '_showPoints', id: 'show_points',cast :Boolean, defaultValue : true}, 376 {label : '_notifier', id: 'notifier', cast : Function, defaultValue : function() { 377 return null; 378 }} 379 ]; 380 }; 381 382 vq.models.ViolinPlotData.prototype._build_data = function(data) { 383 var that = this; 384 this._processData(data); 385 386 if (this.COLUMNLABEL.x == '') this.COLUMNLABEL.x = this.COLUMNID.x; 387 if (this.COLUMNLABEL.y == '') this.COLUMNLABEL.y = this.COLUMNID.y; 388 if (this.COLUMNLABEL.value == '') this.COLUMNLABEL.value = this.COLUMNID.value; 389 390 391 //aggregate categorical data 392 this.data_summary = []; 393 pv.uniq(that.data,function(val) { return val[that.COLUMNID.x];}).forEach(function(label){ 394 var obj={}; 395 var set = that.data.filter(function(a){return a[that.COLUMNID.x]==label;}); 396 obj[that.COLUMNID.x] = label; 397 obj[that.COLUMNID.y] = set.map(function(val){return val[that.COLUMNID.y];}); 398 obj[that.COLUMNID.value] = set.map(function(val){return val[that.COLUMNID.value];}); 399 that.data_summary.push(obj); 400 }); 401 if (this.data.length > 0) this.setDataReady(true); 402 }; 403