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