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