1 /**
  2 *
  3 *
  4 * @class  An interactive brush-link plot.
  5 *
  6 *
  7 * The JSON Object passed into the {@link BrushLink#draw} function as input to the visualization:
  8 *  
  9 * 	<pre> {
 10 *     	  data_array : {Array},
 11 *	  columns : {Array}, 
 12 *	  tooltipItems : {Function},
 13 *	  notifier : {Function},
 14 *	  PLOT : {
 15 *		width : {int},
 16 *		height : {int},
 17 *		horizontal_padding : {int},
 18 *		vertical_padding : {int},
 19 *		container : {HTMLElement or string},
 20 *		symmetric : {Boolean}
 21 *		},
 22 * 	  CONFIGURATION : {
 23 *			multiple_id : {string},
 24 *			color_id : {string},
 25 *          		show_legend : {Boolean},
 26 *          		AXES: {
 27 *				label_font : {string},
 28 *				}
 29 *			}
 30 *	}
 31 *  </pre>
 32 * @extends vq.Vis
 33 */
 34 vq.BrushLink = function(){
 35     vq.Vis.call(this);
 36 
 37     //set option variables to useful values before options are set.
 38     this.height(500);     // defaults
 39     this.width(500);     // defaults
 40     this.vertical_padding(10);
 41     this.horizontal_padding(10);
 42     this.symmetric(false);
 43     this.selection([]);
 44 };
 45 vq.BrushLink.prototype = pv.extend(vq.Vis);
 46 
 47 /** @name vq.BrushLink.selection **/
 48 
 49 /** @name vq.BrushLink.symmetric **/
 50 vq.BrushLink.prototype
 51         .property('selection')
 52         .property('symmetric', Boolean);
 53 
 54 /**
 55 * 
 56 *
 57 * @type number
 58 * @name sectionVerticalPadding
 59 */
 60 
 61 
 62 /**
 63 *  @private set optional parameters passed in at draw
 64 * @param options JSON object containing the passed in options
 65 */
 66 
 67 vq.BrushLink.prototype._setOptionDefaults = function(options) {
 68 
 69     if (options.height != null) { this.height(options.height); }
 70 
 71      if (options.width != null) { this.width(options.width); }
 72 
 73      if (options.container) { this.container(options.container); }
 74 
 75      if (options.vertical_padding != null) { this.vertical_padding(options.vertical_padding); }
 76 
 77      if (options.horizontal_padding != null) { this.horizontal_padding(options.horizontal_padding); }
 78 
 79     if (options.symmetric != null) { this.symmetric(options.symmetric); }
 80 
 81 };
 82 
 83 /**
 84 *  Renders the tool to the browser window using the designated
 85 * options and configurations.
 86  * data :
 87  * @see vq.BrushLinkData
 88  *  </pre>
 89  * options :
 90  * <pre>
 91  *      {
 92  *      container : HTMLDivElement,
 93  *      plotHeight  : number,
 94  *      plotWidth : number,
 95  *      verticalPadding : number,
 96  *      horizontalPadding : number,
 97  *      symmetric : Boolean
 98  * }
 99  * </pre>
100 *
101 * @param data JSON object containing data and Configuration
102 * @param options JSON object containing visualization options
103 */
104 
105 vq.BrushLink.prototype.draw = function(data) {
106 
107 
108 
109     this._bl_data = new vq.models.BrushLinkData(data);
110     if (this._bl_data.isDataReady()) {
111         this._data = this._bl_data._data;
112         this._setOptionDefaults(this._bl_data);
113         this.render();
114     }
115 };
116 
117 /** @private renders the visualization as SVG model to the document DOM */
118 
119 vq.BrushLink.prototype.render = function() {
120 
121     var that = this;
122     var columns = that._bl_data._columns;
123     var listener = that._bl_data._notifier;
124     var tooltip = that._bl_data._tooltipFormat;
125     var color_scale =  that._bl_data.color_scale;
126     var shape_map = that._bl_data.shape_map;
127     var grey = pv.rgb(144, 144, 144, .2),
128             red = pv.rgb(255,144,144,.7),
129             color = pv.colors(
130                     "rgba(50%, 0%, 0%, .5)",
131                     "rgba(0%, 50%, 0%, .5)",
132                     "rgba(0%, 0%, 50%, .5)");
133 
134     var s;
135     var columns_map = pv.numerate(columns);
136 
137     var legend_height = that._bl_data.show_legend ? 25 : 0;
138 
139     var visibleWidth = (this.width() + 2 * this.horizontal_padding()) * columns.length ,
140         visibleHeight = (this.height() + this.vertical_padding() * 2) * columns.length + legend_height,
141         posX = pv.dict(columns, function(c) { return pv.Scale.linear(that._data, function(d){
142 	                return d[c];}).range(0,that.width()).nice();}),
143 	posY = pv.dict(columns, function(c) { return pv.Scale.linear(that._data, function(d){
144         		return d[c];}).range(0,that.height()).nice();});
145 
146     var vis = new pv.Panel()
147             .width(visibleWidth)
148             .height(visibleHeight)
149             .left(that.horizontal_padding())
150             .top(that.vertical_padding())
151             .canvas(that.container());
152 
153     var	cell = vis.add(pv.Panel)
154             .data(columns)
155             .top(function(){return this.index * (that.height() + 2 * that.vertical_padding()) + that.vertical_padding(); })
156             .height(that.height())
157           .add(pv.Panel)
158             .data(function(y) {return columns.map(function(x) { return ({px:x,py:y});});})
159             .left(function() { return this.index * (that.width() + 2 * that.horizontal_padding()) + that.horizontal_padding(); } )
160             .width(that.width());
161 
162     var plot  = cell.add(pv.Panel)
163             .events('all')
164             .strokeStyle("#aaa");
165 
166     var xtick = plot.add(pv.Rule)
167             .data(function(t) { return posX[t.px].ticks(5);})
168             .left(function(d,t) {return posX[t.px](d);})
169             .strokeStyle("#eee");
170 
171     /* Y-axis ticks. */
172     var ytick = plot.add(pv.Rule)
173             .data(function(t) {return posY[t.py].ticks(5);})
174             .bottom(function(d, t) {return posY[t.py](d);})
175             .strokeStyle("#eee");
176 
177    if (this.symmetric()) {
178         plot.visible(function(t) { return t.px != t.py;});
179 
180        xtick.anchor("bottom").add(pv.Label)
181             .visible(function() { return (cell.parent.index == columns.length -1) && !(cell.index & 1);})
182             .text(function(d,t) {return posX[t.px].tickFormat(d);});
183 
184     xtick.anchor("top").add(pv.Label)
185             .visible(function() { return (cell.parent.index == 0) && !(cell.index & 1);})
186             .text(function(d,t) {return posX[t.px].tickFormat(d);});
187 
188     /* Left label. */
189     ytick.anchor("left").add(pv.Label)
190             .visible(function() {return (cell.index == 0) && (cell.parent.index & 1);})
191             .text(function(d, t) {return posY[t.py].tickFormat(d);});
192 
193     /* Right label. */
194     ytick.anchor("right").add(pv.Label)
195             .visible(function() {return (cell.index == columns.length - 1) && !(cell.parent.index & 1);})
196             .text(function(d, t) { return posY[t.py].tickFormat(d);});
197    } else {
198         plot.visible(function(t) { return columns_map[t.px] < columns_map[t.py];});
199 
200        xtick.anchor("bottom").add(pv.Label)
201             .text(function(d,t) {return posX[t.px].tickFormat(d);});
202 
203     ytick.anchor("left").add(pv.Label)
204             .text(function(d, t) {return posY[t.py].tickFormat(d);});
205     }
206 
207     /* Interaction: new selection and display and drag selection */
208     var select_panel = plot.add(pv.Panel);
209 
210     /* Frame and dot plot. */
211 //    var dot = plot.add(pv.Dot)
212 //            .events('all')
213 //            .data(that._data)
214 //            .left(function(d, t) {return posX[t.px](d[t.px]);})
215 //            .bottom(function(d, t) {return posY[t.py](d[t.py]);})
216 //            .size(10)
217 //            .shape(shape_map)
218 //            .fillStyle(grey)
219 //            .strokeStyle(function() { return this.fillStyle();})
220 //            .cursor('pointer')
221 ////            .event('mouseover',pv.Behavior.flextip({include_footer : false, self_hover : true,
222 ////                                                     data_config:that._bl_data.tooltipItems}))
223 //            .event('click',listener);
224 
225     function filtered_data() {
226         var filtered = [];
227         if (!s) { return that._data;}
228         if ( that.new_update) {
229             that.new_update = false;
230         filtered =that._data.filter(function(d) {return  !((d[s.px] < s.x1) || (d[s.px] > s.x2)
231             || (d[s.py] < s.y1) || (d[s.py] > s.y2));});
232         that.selection(filtered);
233         //return filtered;
234         } else {
235             //return that.selection();
236         }
237 
238     }
239 
240     function visible_data(point) {
241         return  s ? !((point[s.px] < s.x1) || (point[s.px] > s.x2)
242             || (point[s.py] < s.y1) || (point[s.py] > s.y2)) : true;
243     }
244 
245     function active_color(point) {
246         return visible_data(point) ? color_scale(point) : grey;
247     }
248 
249    var dot_panel = plot.add(pv.Panel);
250 
251     var highlighted_dot = dot_panel.add(pv.Dot)
252 //            .data(filtered_data)
253             .data(that._data)
254             .left(function(d, t) {return posX[t.px](d[t.px]);})
255             .bottom(function(d, t) {return posY[t.py](d[t.py]);})
256             .size(10)
257             .fillStyle(active_color)
258             .strokeStyle(function() { return this.fillStyle();})
259             .shape(shape_map)
260             .events('none')
261             .cursor('pointer')
262             .events('painted')
263             .event('mouseover',pv.Behavior.hovercard({include_footer : false, self_hover : true,
264                                                      data_config:that._bl_data.tooltipItems}))
265             .event('click',listener);
266 
267     select_panel
268             .data([{x:20, y:20, dx:80, dy:80}])
269             .events('all')
270             .cursor("crosshair")
271             .event("mousedown", pv.Behavior.select())
272             .event("selectstart", function() {return (s = null,
273             highlighted_dot.context(null, 0, function() {return this.render();}),
274         select_panel.context(null, 0, function() {return this.render();}));})
275             .event("select", update)
276             .event('selectend', filtered_data)
277          .add(pv.Bar)
278             .visible(function(d, k, t) { return s && s.px == t.px && s.py == t.py; })
279             .left(function(d) { return d.x;} )
280             .top(function(d) { return d.y;} )
281             .width(function(d) { return d.dx;} )
282             .height(function(d) { return d.dy;} )
283             .fillStyle("rgba(0,0,0,.15)")
284             .strokeStyle("white")
285             .cursor("move")
286             .event("mousedown", pv.Behavior.drag())
287             .event("dragend", filtered_data)
288             .event("drag", update);
289 
290     /* Labels along the diagonal. */
291     cell.anchor("center").add(pv.Label)
292             .visible(function(t) { return t.px == t.py;} )
293             .font(that._bl_data.axes_label_font)
294             .text(function(t) { return t.px.replace(/([WL])/, " $1").toLowerCase();});
295 
296     if(this._bl_data.show_legend) {
297        var legend_panel =  vis.add(pv.Panel)
298                                 .bottom(0)
299                                 .height(legend_height)
300                                 .strokeStyle('#222')
301                                 .width(that.width() * (that._bl_data._columns.length - 1))
302                                 .left(that.horizontal_padding())
303                                 .lineWidth(1);
304 
305        var color_panel = legend_panel.add(pv.Panel)
306                                 .data(that._bl_data.unique_color_ids)
307                                 .strokeStyle(null)
308                                 .bottom(legend_height /2)
309                                 .height(legend_height /2 - 2)
310                                 .left(function() { return 30 + (this.index * 40);})
311                                 .width(40);
312 
313             color_panel.add(pv.Bar)
314                         .left(2)
315                         .width(15)
316                         .bottom(0)
317                         .fillStyle(function(id) {var  a = {}; a[that._bl_data.color_id] = id; return color_scale(a); });
318 
319             color_panel.anchor('right').add(pv.Label)
320                         .font('16px');
321 
322 
323        if (that._bl_data.multiple_id) {
324 
325             var shape_panel = legend_panel.add(pv.Panel)
326                         .data(that._bl_data.unique_multiple_ids)
327                         .bottom(0)
328                         .height(legend_height / 2)
329                         .strokeStyle(null)
330                         .left(function() { return 30 + (this.index * 40);})
331                         .width(40);
332 
333             shape_panel.add(pv.Dot)
334                              .left(12)
335                              .strokeStyle('#222')
336                              .fillStyle(function() { return this.strokeStyle();})
337                              .radius(legend_height /4 -2)
338                              .shape(function(id) {var  a = {}; a[that._bl_data.multiple_id] = id; return shape_map(a); });
339 
340             shape_panel.anchor('right').add(pv.Label)
341                         .font('16px');
342 
343         }
344 
345     }
346 
347     vis.render();
348 
349     /* Interaction: update selection. */
350     function update(d, t) {
351         if (d.dx < 5 && d.dy < 5) {s = null;}
352         else {
353         s = d;
354         s.px = t.px;
355         s.py = t.py;
356         s.x1 = posX[t.px].invert(d.x);
357         s.x2 = posX[t.px].invert(d.x + d.dx);
358         s.y1 = posY[t.py].invert(that.height() - d.y - d.dy);
359         s.y2 = posY[t.py].invert(that.height() - d.y);
360         that.new_update=true;
361         }
362         highlighted_dot.context(null, 0, function() {return this.render();});
363     }
364 
365 };
366 
367 /**
368 * Constructs the data model used in the BrushLink visualization
369 * @class Represents the data, custom configuration, and behavioral functions
370 *
371 *
372 * @extends vq.models.VisData
373  * @see vq.BrushLink
374  * @param object An object that configures a vq.BrushLink visualization
375 */
376 
377 vq.models.BrushLinkData = function(data) {
378     vq.models.VisData.call(this,data);
379 
380     this.setDataModel();
381 
382     if (this.getDataType() == 'vq.models.BrushLinkData') {
383         this._build_data(this.getContents());
384     } else {
385         console.warn('Unrecognized JSON object.  Expected vq.models.BrushLinkData object.');
386     }
387 };
388 vq.models.BrushLinkData.prototype = pv.extend(vq.models.VisData);
389 
390 /**
391  * @private
392  *
393  */
394 
395 vq.models.BrushLinkData.prototype.setDataModel = function () {
396     this._dataModel = [
397         {label: 'width', id: 'PLOT.width', cast : Number, defaultValue: 400},
398         {label: 'height', id: 'PLOT.height', cast : Number, defaultValue: 400},
399         {label : 'container', id:'PLOT.container', optional : true},
400         {label: 'vertical_padding', id: 'PLOT.vertical_padding', cast : Number, defaultValue: 0},
401         {label: 'horizontal_padding', id: 'PLOT.horizontal_padding',cast : Number,  defaultValue: 0},
402         {label : 'symmetric', id:'PLOT.symmetric', cast : Boolean, defaultValue : false},
403         {label : '_data', id: 'data_array', defaultValue : [] },
404         {label : '_columns', id: 'columns', defaultValue : [] },
405         {label : '_tooltipFormat', id: 'tooltipFormat', cast: vq.utils.VisUtils.wrapProperty, defaultValue : null },
406         {label : 'tooltipItems', id: 'tooltip_items', defaultValue : [] },
407         {label : 'color_id', id: 'CONFIGURATION.color_id', cast: String, defaultValue : null },
408         {label : 'multiple_id', id: 'CONFIGURATION.multiple_id', cast: String, defaultValue : null},
409         {label : 'show_legend', id: 'CONFIGURATION.show_legend', cast: Boolean, defaultValue : false },
410         {label : 'axes_label_font', id: 'CONFIGURATION.AXES.label_font', cast: String, defaultValue : "bold 14px sans-serif" },
411         {label : '_notifier', id: 'notifier', cast : Function, defaultValue : function() {return null;}}
412     ];
413 };
414 
415 /**
416  * @private
417  * @param data
418  */
419 
420 vq.models.BrushLinkData.prototype._build_data = function(data) {
421 	var that = this;
422     this.color_scale = vq.utils.VisUtils.wrapProperty(pv.color('red').alpha(0.8));
423     var shape_map=['cross','triangle','square','cross','diamond','bar','tick'];
424     var default_shape = 'circle';
425 
426     this._processData(data);
427 
428     if (this._data.length > 0) {
429         this.setDataReady(true);
430     }
431 
432     if (this.color_id) {
433             this.unique_color_ids = pv.uniq(that._data,function(row) { return row[that.color_id];});
434             var cat10 =  pv.Colors.category10();
435             this.color_scale = this.unique_color_ids.length > 1 ?
436                     function(c) { return cat10(c[that.color_id]).alpha(0.8);} :
437                     this.color_scale;
438     }
439 
440     if (this.multiple_id) {
441             this.unique_multiple_ids = pv.uniq(that._data,function(row) { return row[that.multiple_id];});
442             var map = pv.dict(that.unique_multiple_ids, function(id) { return shape_map[this.index];})
443             this.shape_map = that.unique_multiple_ids.length > 1 ?
444                     function(feature) { return map[feature[that.multiple_id]]; }
445                     :  function() { return default_shape;};
446     }
447 
448 };
449