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