1 /** 2 * @class Monk.component.FeaturesChartComponent 3 * @description Display features from SEASR analyses. 4 * @extends Workbench.component.Component 5 * @author Andrew 6 */ 7 8 Monk.component.FeaturesChartComponent = function(args) { 9 10 this.featuresGrid = null; 11 12 this.pageSize = 20; 13 14 this.cachedData = null; 15 this.cachedTree = null; 16 this.cachedTreeSummary=null; 17 this.cachedTreeImage = null; 18 this.chartWin=null; 19 20 this.classNames = []; 21 22 this.colourArray = [ 23 {h: 240, s: 50, v: 85}, 24 {h: 92, s: 50, v: 85}, 25 {h: 349, s: 50, v: 85}, 26 {h: 45, s: 50, v: 85} 27 ]; 28 29 Monk.component.FeaturesChartComponent.superclass.constructor.call(this, args); 30 } 31 32 Workbench.extend(Monk.component.FeaturesChartComponent, Workbench.component.Component, { 33 34 label: 'Features Chart', 35 description: 'For viewing text features.', 36 "window": this.window, 37 38 handle : function(monkEvent, data) { 39 if (monkEvent.instanceOf(Workbench.event.ComponentLoaded)) { 40 if (monkEvent.component == this) { 41 var features = Monk.component.dataManager.getFeatureList(); 42 var decisionTreeSummary = Monk.component.dataManager.getDecisionTreeSummary(); 43 var tree = Monk.component.dataManager.getDecisionTree(); 44 var treeImage = Monk.component.dataManager.getDecisionTreeImage(); 45 46 if (features.length > 0) { 47 this.cachedData = features; 48 } 49 50 51 if(decisionTreeSummary!=null){ 52 this.cachedTreeSummary= decisionTreeSummary; 53 } 54 if(decisionTreeSummary!=null){ 55 this.cachedTree= tree; 56 } 57 58 if(treeImage!=null){ 59 this.cachedTreeImage= treeImage; 60 } 61 62 //Workbench.console.info("Tree URL is: " + treeImage); 63 } 64 } 65 }, 66 67 // taken from: http://johndyer.name/lab/colorpicker/ 68 hsvToRgb: function (hsv) { 69 70 rgb = {r:0, g:0, b:0}; 71 72 var h = hsv.h; 73 var s = hsv.s; 74 var v = hsv.v; 75 76 if (s == 0) { 77 if (v == 0) { 78 rgb.r = rgb.g = rgb.b = 0; 79 } else { 80 rgb.r = rgb.g = rgb.b = parseInt(v * 255 / 100); 81 } 82 } else { 83 if (h == 360) { 84 h = 0; 85 } 86 h /= 60; 87 88 // 100 scale 89 s = s/100; 90 v = v/100; 91 92 var i = parseInt(h); 93 var f = h - i; 94 var p = v * (1 - s); 95 var q = v * (1 - (s * f)); 96 var t = v * (1 - (s * (1 - f))); 97 switch (i) { 98 case 0: 99 rgb.r = v; 100 rgb.g = t; 101 rgb.b = p; 102 break; 103 case 1: 104 rgb.r = q; 105 rgb.g = v; 106 rgb.b = p; 107 break; 108 case 2: 109 rgb.r = p; 110 rgb.g = v; 111 rgb.b = t; 112 break; 113 case 3: 114 rgb.r = p; 115 rgb.g = q; 116 rgb.b = v; 117 break; 118 case 4: 119 rgb.r = t; 120 rgb.g = p; 121 rgb.b = v; 122 break; 123 case 5: 124 rgb.r = v; 125 rgb.g = p; 126 rgb.b = q; 127 break; 128 } 129 130 rgb.r = parseInt(rgb.r * 255); 131 rgb.g = parseInt(rgb.g * 255); 132 rgb.b = parseInt(rgb.b * 255); 133 } 134 135 return rgb; 136 }, 137 138 initializeFeaturesGrid : function(initData) { 139 var data = []; 140 141 if (initData != null) { 142 data = initData; 143 } else if (this.cachedData != null) { 144 data = this.cachedData; 145 } 146 147 var json = {features: data}; 148 149 // find the min/max for feature class values 150 var minClass = null; 151 var maxClass = null; 152 for (var i = 0, len = data.length; i < len; i++) { 153 var feature = data[i]; 154 for (var j = 0, len2 = feature.classes.entry.length; j < len2; j++) { 155 var entry = parseFloat(feature.classes.entry[j]['double']); 156 if (entry < minClass || minClass == null) minClass = entry; 157 if (entry > maxClass || maxClass == null) maxClass = entry; 158 } 159 } 160 var modifier = minClass * -1; 161 minClass = minClass + modifier; 162 maxClass = maxClass + modifier; 163 164 var confidenceGradient = function(value, metadata, record, rowIndex, colIndex, store, i){ 165 var confidence = Math.floor((value + modifier) / maxClass * 100); 166 var colour = this.colourArray[i]; 167 if (!colour) { 168 var remainder = i / this.colourArray.length - Math.floor(i / this.colourArray.length); 169 i = this.colourArray.length * remainder; 170 colour = this.colourArray[i]; 171 } 172 colour.s = confidence; 173 var rgb = this.hsvToRgb(colour); 174 metadata.attr = 'style = "background-color: rgb('+rgb.r+','+rgb.g+','+rgb.b+');"'; 175 return ''; // just show the colour 176 } 177 178 var columnHeaders = [ 179 {header: 'Feature', width: 100, sortable: true, dataIndex: 'name', 180 renderer : function (val) {return val.replace(/\s+\(.*?\)/g,'').replace(/\s+/g,' ')} 181 }, 182 {header: 'POS', width: 75, sortable: true, dataIndex: 'name', renderer: function(val){ 183 var re = /\(.*?\)/g; 184 var pos = new Array(); 185 while ((match = re.exec(val)) != null) { 186 pos.push(match[0]); 187 } 188 return pos.join(' '); 189 } 190 191 }, 192 {header:'type',width:50,sortable:true,dataIndex:'type', 193 renderer: function(content, el, data) {return content;} 194 }, 195 {header: 'Avg. Freq Test', width: 125, sortable: true, dataIndex: 'avgFreqTestDocs', 196 renderer: function(content, el, data) {return '<span ext:qtip="Frequency per 10000 features in test documents">'+content+"</span>"} 197 }, 198 {header: 'Avg. Freq Training', width: 125, sortable: true, dataIndex: 'avgFreqTrainingDocs', 199 convert:function(value){var num = parseFloat(value);return num.toFixed(4)}, 200 renderer: function(content, el, data) {return '<span ext:qtip="Frequency per 10000 features in training documents">'+content+"</span>"} 201 } 202 ]; 203 var recordArray = [ 204 {name: 'name'}, 205 {name: 'pos'}, 206 {name: 'type'}, 207 {name: 'avgFreqTestDocs', type: 'float',convert:function(value){var num = parseFloat(value);return num.toFixed(4)}}, 208 {name: 'avgFreqTrainingDocs', type: 'float',convert:function(value){var num = parseFloat(value);return num.toFixed(4)}} 209 ]; 210 if (data.length > 0) { 211 var tableType = data[0].type; 212 var firstEntry = data[0].classes.entry; 213 for (var i = 0, len = firstEntry.length; i < len; i++) { 214 var className = firstEntry[i].string; 215 var trimmedClassName = className.replace('level_', '').replace('_Features', '')+" confidence"; 216 this.classNames.push(trimmedClassName); 217 columnHeaders.push({ 218 header: trimmedClassName, sortable: true, dataIndex: trimmedClassName, 219 renderer: confidenceGradient.createDelegate(this, [i], true) 220 }); 221 recordArray.push({name: trimmedClassName, mapping: 'classes.entry['+i+'].double', type: 'float'}); 222 } 223 } 224 225 var recordDef = Ext.data.Record.create(recordArray); 226 227 var reader = new Ext.data.JsonReader({ 228 root: 'features' 229 }, recordDef); 230 231 var store = new Ext.data.GroupingStore({ 232 reader: reader, 233 proxy: new Ext.ux.data.PagingMemoryProxy(json, {reader: reader}), 234 remoteSort: true, 235 groupField: 'type', 236 sortInfo:{field: 'name', direction: "ASC"} 237 }); 238 239 var viewport = new Ext.Viewport({ 240 layout: 'fit', 241 renderTo: 'gridParent', 242 items: this.featuresGrid 243 }); 244 245 Ext.EventManager.onWindowResize(function(w, h) { 246 this.featuresGrid.setHeight(h); 247 }, this); 248 249 var columnModel = new Ext.grid.ColumnModel(columnHeaders); 250 251 var selectionModel = new Ext.grid.RowSelectionModel({ 252 singleSelect: false 253 }); 254 255 this.featuresGrid = new Ext.grid.GridPanel({ 256 store: store, 257 stripeRows: true, 258 cm: columnModel, 259 sm: selectionModel, 260 view: new Ext.grid.GroupingView({ 261 forceFit:true, 262 groupTextTpl: '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "Items" : "Item"]})' 263 }), 264 viewConfig: { 265 emptyText: 'No features to display.', // TODO not working 266 forceFit: true 267 }, 268 renderTo: 'featuresGrid', 269 autoWidth: true, 270 listeners: { 271 'render': function(grid){ 272 var h = viewport.getBox().height; 273 grid.setHeight(h); 274 } 275 }, 276 bbar: new Ext.PagingToolbar({ 277 store: store, 278 pageSize: this.pageSize, 279 displayInfo: true, 280 displayMsg: 'Displaying features {0} - {1} of {2}', 281 emptyMsg: 'No features to display', 282 plugins: [new Ext.ux.PageSizePlugin()] 283 }), 284 tbar: [{ 285 iconCls : 'infoBtn', 286 text: 'Decision Tree', 287 handler: function(button, event) { 288 var result = this.cachedTree; 289 290 if(this.cachedTreeSummary==''){ 291 Monk.component.messenger.show({ title: 'Results Summary',msg: 'Decision Tree not available.', 292 width: 470, modal: false},this.window); 293 return; 294 }else{ 295 // if (this.chartWin == null) { 296 this.chartWin = new Ext.Window({ 297 title: 'Decision Tree', 298 height: 350, 299 width: 475, 300 modal: false, 301 shadow: true, 302 plain: true, 303 border: false, 304 collapsible: false, 305 layout: 'fit', 306 items: [ 307 { 308 xtype: 'tabpanel', 309 id:'tree-tabs', 310 activeTab: 0, 311 items:[ 312 { 313 xtype: 'panel', 314 title: 'Tree', 315 id:'treeTab', 316 border: false, 317 autoScroll: true, 318 html: '<div id="tree_img"></div>' 319 },{ 320 xtype:'panel', 321 title:'Summary', 322 id:'summaryTab', 323 border: false, 324 autoscroll:true, 325 html: '<div id="tree_summary"></div>' 326 } 327 ] 328 } 329 330 ] 331 }); 332 //} 333 // Workbench.console.info("tree img is: " + "/charts/c45/"+this.cachedTreeImage); 334 this.chartWin.on('show', function(){ 335 Ext.getCmp('summaryTab').show(); 336 Ext.getCmp('treeTab').show(); 337 Ext.get('tree_img').update('<img alt="Decision Tree" src="'+"/charts/c45/"+this.cachedTreeImage+'"/>'); 338 Ext.get('tree_summary').update("<pre>"+this.cachedTreeSummary+"</pre>"); 339 340 341 }, this, {single: true}); 342 343 this.chartWin.show(button.getEl()); 344 } 345 }, 346 scope: this 347 },{ 348 iconCls: 'exportBtn', 349 text: 'Export Features', 350 handler: function() { 351 var worklist = Monk.component.dataManager.getWorklist(); 352 var output = ['Feature','POS','Avg. Freq Test','Avg. Freq Training']; 353 output = output.concat(this.classNames).join("\t") + "\n"; 354 for (var i = 0, len = this.cachedData.length; i < len; i++) { 355 var record = this.cachedData[i]; 356 357 var name = record.name.replace(/\s+\(.*?\)/g,'').replace(/\s+/g,' '); 358 359 var re = /\(.*?\)/g; 360 var pos = new Array(); 361 while ((match = re.exec(record.name)) != null) { 362 pos.push(match[0]); 363 } 364 pos = pos.join(' '); 365 366 var avgCountTest = record.avgFreqTestDocs; 367 var avgCountTraining= record.avgFreqTrainingDocs; 368 369 output += [name, pos, avgCountTest, avgCountTraining].join("\t"); 370 371 var classes = []; 372 for (var j = 0, len2 = record.classes.entry.length; j < len2; j++) { 373 classes.push(record.classes.entry[j]['double']); 374 } 375 output += classes.join("\t") + "\n"; 376 } 377 Monk.component.messenger.show({ 378 title: 'Export Features as Tab Separated Values (TSV)', 379 msg: 'Select all the data below, copy, and paste directly into your favourite spreadsheet program.', 380 width: 600, 381 defaultTextHeight: 25, 382 buttons: Ext.MessageBox.OK, 383 multiline: true, 384 value: output, 385 scope: this 386 }, this.window.parent ? this.window.parent.window : this.window) 387 }, 388 scope: this 389 }] 390 }); 391 392 this.featuresGrid.on('rowclick', function(grid, rowIndex, event){ 393 var record = grid.getSelectionModel().getSelected(); 394 var featureInstance = record.get('name'); 395 var posTest = /\(.+?\)$/; 396 var featureType = "Spelling"; 397 if (featureInstance.match(posTest)) { 398 featureType = "Lemma"; 399 } 400 401 var worksetId = Monk.component.dataManager.getWorksetId(); 402 403 var params = { 404 worksetId: worksetId, 405 searchType: 'both', 406 sort: 'title_asc' 407 }; 408 409 if (featureType == 'Lemma') { 410 params.lemmaPatternCriterion = featureInstance; 411 } else if (featureType == 'Spelling') { 412 params.spellingPatternCriterion = featureInstance; 413 } else { 414 Workbench.console.info(featureType+" not supported"); 415 return; 416 //alert('unsupported feature type: '+featureType); 417 } 418 419 if (this.chunksGrid != null) { 420 this.chunksGrid.getStore().removeAll(); 421 } 422 423 // Monk.data.chunk.getChunksContainingFeatureWithinWorkset(params); 424 425 var featureParam = { 426 term: featureInstance, 427 type: featureType 428 }; 429 430 431 this.notify(new Monk.event.chunk.FeatureSelected({ 432 label: 'Text feature selected: '+'"'+featureInstance+'"' 433 }), 434 featureParam 435 ); 436 437 438 439 }, this); 440 441 var autoSizeColumns = function() { 442 for (var i = 0; i < this.colModel.getColumnCount(); i++) { 443 autoSizeColumn(i, this); 444 } 445 this.store.removeListener('load', autoSizeColumns, this); // only run this the first time 446 } 447 448 var autoSizeColumn = function(c, grid) { 449 var w = grid.view.getHeaderCell(c).firstChild.scrollWidth; 450 for (var i = 0, l = grid.store.getCount(); i < l; i++) { 451 w = Math.max(w, grid.view.getCell(i, c).firstChild.scrollWidth); 452 } 453 grid.colModel.setColumnWidth(c, w+2); 454 return w; 455 } 456 457 store.on('load', autoSizeColumns, this.featuresGrid); 458 459 store.load({params:{start:0, limit: this.pageSize}}); 460 }, 461 462 reset : function() { 463 if (this.featuresGrid != null) this.featuresGrid.getStore().removeAll(); 464 this.classNames = []; 465 this.cachedData = null; 466 this.cachedTree= null; 467 this.cachedTreeSummary=null; 468 // get rid of the terms we were storing since we're getting new ones now 469 Monk.component.dataManager.clearTermsForChunks(); 470 } 471 472 473 });