Changeset 1991
- Timestamp:
- Sep 2, 2011, 12:37:09 PM (12 years ago)
- Location:
- trunk
- Files:
-
- 2 added
- 3 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/grails-app/controllers/dbnp/visualization/VisualizeController.groovy
r1984 r1991 17 17 import dbnp.studycapturing.*; 18 18 import grails.converters.JSON 19 import groovy.lang.Closure; 20 19 21 import org.dbnp.gdt.* 20 22 … … 22 24 def authenticationService 23 25 def moduleCommunicationService 24 26 25 27 /** 26 28 * Shows the visualization screen 27 29 */ 28 30 def index = { 29 31 [ studies: Study.giveReadableStudies( authenticationService.getLoggedInUser() )] 30 32 } 31 33 32 34 def getStudies = { 33 35 def studies = Study.giveReadableStudies( authenticationService.getLoggedInUser() ); 34 36 render studies as JSON 35 37 } 36 38 37 39 def getFields = { 38 def input_object 39 def studies 40 41 try{ 42 input_object = JSON.parse(params.get('data')) 43 studies = input_object.get('studies').id 44 } catch(Exception e) { 45 // TODO: properly handle this exception 46 println e 47 } 48 49 def fields = []; 50 studies.each { 51 /* 52 Gather fields related to this study from GSCF. 53 This requires: 54 - a study. 55 - a category variable, e.g. "events". 56 - a type variable, either "domainfields" or "templatefields". 57 */ 58 def study = Study.get(it) 59 fields += getFields(study, "subjects", "domainfields") 60 fields += getFields(study, "subjects", "templatefields") 61 fields += getFields(study, "events", "domainfields") 62 fields += getFields(study, "events", "templatefields") 63 fields += getFields(study, "samplingEvents", "domainfields") 64 fields += getFields(study, "samplingEvents", "templatefields") 65 fields += getFields(study, "assays", "domainfields") 66 fields += getFields(study, "assays", "templatefields") 67 fields += getFields(study, "samples", "domainfields") 68 fields += getFields(study, "samples", "domainfields") 69 70 71 /* 72 Gather fields related to this study from modules. 73 This will use the getMeasurements RESTful service. That service returns measurement types, AKA features. 74 It does not actually return measurements (the getMeasurementData call does). 75 The getFields method (or rather, the getMeasurements service) requires one or more assays and will return all measurement 76 types related to these assays. 77 So, the required variables for such a call are: 78 - a source variable, which can be obtained from AssayModule.list() (use the 'name' field) 79 - a list of assays, which can be obtained with study.getAssays() 80 */ 81 AssayModule.list().each { module -> 82 def list = [] 83 list = getFields(module.name, study.getAssays()) 84 if(list!=null){ 85 if(list.size()!=0){ 86 fields += list 87 } 88 } 89 } 90 91 // TODO: Maybe we should add study's own fields 92 } 40 def input_object 41 def studies 42 43 try{ 44 input_object = JSON.parse(params.get('data')) 45 studies = input_object.get('studies').id 46 } catch(Exception e) { 47 // TODO: properly handle this exception 48 println e 49 } 50 51 def fields = []; 52 studies.each { 53 /* 54 Gather fields related to this study from GSCF. 55 This requires: 56 - a study. 57 - a category variable, e.g. "events". 58 - a type variable, either "domainfields" or "templatefields". 59 */ 60 def study = Study.get(it) 61 fields += getFields(study, "subjects", "domainfields") 62 fields += getFields(study, "subjects", "templatefields") 63 fields += getFields(study, "events", "domainfields") 64 fields += getFields(study, "events", "templatefields") 65 fields += getFields(study, "samplingEvents", "domainfields") 66 fields += getFields(study, "samplingEvents", "templatefields") 67 fields += getFields(study, "assays", "domainfields") 68 fields += getFields(study, "assays", "templatefields") 69 fields += getFields(study, "samples", "domainfields") 70 fields += getFields(study, "samples", "domainfields") 71 72 /* 73 Gather fields related to this study from modules. 74 This will use the getMeasurements RESTful service. That service returns measurement types, AKA features. 75 It does not actually return measurements (the getMeasurementData call does). 76 The getFields method (or rather, the getMeasurements service) requires one or more assays and will return all measurement 77 types related to these assays. 78 So, the required variables for such a call are: 79 - a source variable, which can be obtained from AssayModule.list() (use the 'name' field) 80 - a list of assays, which can be obtained with study.getAssays() 81 */ 82 AssayModule.list().each { module -> 83 def list = [] 84 list = getFields(module.name, study.getAssays()) 85 if(list!=null){ 86 if(list.size()!=0){ 87 fields += list 88 } 89 } 90 } 91 92 // TODO: Maybe we should add study's own fields 93 } 93 94 94 95 render fields as JSON 95 96 } 96 97 97 98 def getVisualizationTypes = { 98 99 def types = [ [ "id": "barchart", "name": "Barchart"] ]; 99 100 render types as JSON 100 101 } 102 103 def getFields(source, assays){ 104 /* 105 Gather fields related to this study from modules. 106 This will use the getMeasurements RESTful service. That service returns measurement types, AKA features. 107 It does not actually return measurements (the getMeasurementData call does). 108 The getFields method (or rather, the getMeasurements service) requires one or more assays and will return all measurement 109 types related to these assays. 110 So, the required variables for such a call are: 111 - a source variable, which can be obtained from AssayModule.list() (use the 'name' field) 112 - a list of assays, which can be obtained with study.getAssays() 113 */ 114 def collection = [] 115 def callUrl = "" 116 117 // Making a different call for each assay 118 // TODO: Change this to one call that requests fields for all assays, when you get that to work (in all cases) 119 assays.each { assay -> 120 def urlVars = "assayToken="+assay.assayUUID 121 AssayModule.list().each { module -> 122 if(source==module.name){ 123 try { 124 callUrl = module.url + "/rest/getMeasurements/query?"+urlVars 125 def json = moduleCommunicationService.callModuleRestMethodJSON( module.url, callUrl ); 126 json.each{ jason -> 127 collection.add(jason) 128 } 129 } catch(Exception e){ 130 // Todo: properly handle this exception 131 println "No success with\n\t"+callUrl+"\n"+e 132 return null 133 } 134 } 135 } 136 } 137 138 def fields = [] 139 // Formatting the data 140 collection.each { field -> 141 fields << [ "id": createFieldId( name: field, source: source, type: "feature" ), "source": source, "category": "feature", "name": source+" feature "+field ] 142 } 143 return fields 144 } 145 146 def getFields(study, category, type){ 147 /* 148 Gather fields related to this study from GSCF. 149 This requires: 150 - a study. 151 - a category variable, e.g. "events". 152 - a type variable, either "domainfields" or "templatefields". 153 */ 154 155 // Collecting the data from it's source 156 def collection 157 def fields = [] 158 def source = "GSCF" 159 160 // Gathering the data 161 if(category=="subjects"){ 162 if(type=="domainfields"){ 163 collection = Subject.giveDomainFields() 164 } 165 if(type=="templatefields"){ 166 collection = study.giveSubjectTemplates().fields 167 } 168 } 169 if(category=="events"){ 170 if(type=="domainfields"){ 171 collection = Event.giveDomainFields() 172 } 173 if(type=="templatefields"){ 174 collection = study.giveEventTemplates().fields 175 } 176 } 177 if(category=="samplingEvents"){ 178 if(type=="domainfields"){ 179 collection = SamplingEvent.giveDomainFields() 180 } 181 if(type=="templatefields"){ 182 collection = study.giveSamplingEventTemplates().fields 183 } 184 } 185 if(category=="samples"){ 186 if(type=="domainfields"){ 187 collection = Sample.giveDomainFields() 188 } 189 if(type=="templatefields"){ 190 collection = study.giveEventTemplates().fields 191 } 192 } 193 if(category=="assays"){ 194 if(type=="domainfields"){ 195 collection = Event.giveDomainFields() 196 } 197 if(type=="templatefields"){ 198 collection = study.giveEventTemplates().fields 199 } 200 } 201 202 // Formatting the data 203 if(type=="domainfields"){ 204 collection.each { field -> 205 fields << [ "id": createFieldId( name: field.name, source: source, type: category ), "source": source, "category": category, "name": category.capitalize()+" "+field.name ] 206 } 207 } 208 if(type=="templatefields"){ 209 collection.each { field -> 210 for(int i = 0; i < field.size(); i++){ 211 fields << [ "id": createFieldId( id: field[i].id, name: field[i].name, source: source, type: category ), "source": source, "category": category, "name": category.capitalize()+" "+field[i].name ] 212 } 213 } 214 } 215 216 return fields 217 } 218 219 /** 220 * Retrieves data for the visualization itself. 221 */ 222 def getData = { 223 // Extract parameters 224 // TODO: handle erroneous input data 225 def inputData = parseGetDataParams(); 226 227 // TODO: handle the case that we have multiple studies 228 def studyId = inputData.studyIds[ 0 ]; 229 def study = Study.get( studyId as Integer ); 230 231 // Find out what samples are involved 232 def samples = study.samples 233 234 // Retrieve the data for both axes for all samples 235 // TODO: handle the case of multiple fields on an axis 236 def fields = [ "x": inputData.columnIds[ 0 ], "y": inputData.rowIds[ 0 ] ]; 237 def data = getAllFieldData( study, samples, fields ); 238 239 // Group data based on the y-axis if categorical axis is selected 240 // TODO: handle categories and continuous data 241 def groupedData = groupFieldData( data ); 242 243 // Format data so it can be rendered as JSON 244 def returnData = formatData( groupedData, fields ); 245 246 render returnData as JSON 247 } 248 249 /** 250 * Parses the parameters given by the user into a proper list 251 * @return Map with 4 keys: 252 * studyIds: list with studyIds selected 253 * rowIds: list with fieldIds selected for the rows 254 * columnIds: list with fieldIds selected for the columns 255 * visualizationType: String with the type of visualization required 256 * @see getFields 257 * @see getVisualizationTypes 258 */ 259 def parseGetDataParams() { 260 def studyIds, rowIds, columnIds, visualizationType; 261 262 def inputData = params.get( 'data' ); 263 try{ 264 def input_object = JSON.parse(inputData) 265 266 studyIds = input_object.get('studies')*.id 267 rowIds = input_object.get('rows')*.id 268 columnIds = input_object.get('columns')*.id 269 visualizationType = "barchart" 270 } catch(Exception e) { 271 // TODO: properly handle this exception 272 println e 273 } 274 275 return [ "studyIds" : studyIds, "rowIds": rowIds, "columnIds": columnIds, "visualizationType": visualizationType ]; 276 } 277 278 /** 279 * Retrieve the field data for the selected fields 280 * @param study Study for which the data should be retrieved 281 * @param samples Samples for which the data should be retrieved 282 * @param fields Map with key-value pairs determining the name and fieldId to retrieve data for. Example: 283 * [ "x": "field-id-1", "y": "field-id-3" ] 284 * @return A map with the same keys as the input fields. The values in the map are lists of values of the 285 * selected field for all samples. If a value could not be retrieved for a sample, null is returned. Example: 286 * [ "x": [ 3, 6, null, 10 ], "y": [ "male", "male", "female", "female" ] ] 287 */ 288 def getAllFieldData( study, samples, fields ) { 289 def fieldData = [:] 290 fields.each{ field -> 291 fieldData[ field.key ] = getFieldData( study, samples, field.value ); 292 } 293 294 return fieldData; 295 } 101 296 102 def getData = { 103 println params 104 def input_object 105 def studies 106 def rows 107 def columns 108 def vizualisation_type 109 110 try{ 111 input_object = JSON.parse(params.get('data')) 112 studies = input_object.get('studies') 113 rows = input_object.get('rows') 114 columns = input_object.get('columns') 115 vizualisation_type = "barchart" 116 } catch(Exception e) { 117 // TODO: properly handle this exception 118 println e 119 } 120 121 def data = [:] 122 123 Collection row_data = [] 124 Collection column_data = [] 125 studies.each { 126 // TODO: Get rid of code duplication 127 def study = Study.get(it.id) 128 rows.eachWithIndex { r, index -> 129 println " - field "+r 130 def case_switch 131 def input_id = r.id.split(",") 132 def field_id = input_id[0] 133 def source_module = input_id[1] 134 def field_type = input_id[2] 135 def field_name = input_id[3] 136 def templatefield_source 137 if(source_module=="GSCF"){ 138 if(field_type!=TemplateField.class.toString()){ 139 case_switch = "domain" 140 } else { 141 templatefield_source = input_id[4] 142 } 143 row_data[index] = getFieldData(study, case_switch, field_id, field_name, source_module, templatefield_source, field_type) 144 } else { 145 // Grabbing field data from a module 146 row_data[index] = getFieldData(study, "", field_id, "", source_module, "", "") 147 } 148 } 149 150 columns.eachWithIndex { r, index -> 151 def case_switch 152 def input_id = r.id.split(",") 153 def field_id = input_id[0] 154 def source_module = input_id[1] 155 def field_type = input_id[2] 156 def field_name = input_id[3] 157 def templatefield_source 158 159 if(source_module=="GSCF"){ 160 if(field_type!=TemplateField.class.toString()){ 161 case_switch = "domain" 162 } else { 163 templatefield_source = input_id[4] 164 } 165 column_data[index] = getFieldData(study, case_switch, field_id, field_name, source_module, templatefield_source, field_type) 166 } else { 167 // Grabbing field data from a module 168 column_data[index] = getFieldData(study, "", field_id, "", source_module, "", "") 169 } 170 } 171 } 172 173 if(row_data.size()!=0 && column_data.size()!=0 && row_data[0].size()!=0 && column_data[0].size()!=0){ 174 // Going to build the return object now 175 def return_data = [:] 176 def series = [] 177 def possible_xaxis_title = "" 178 def possible_yaxis_title = "" 179 return_data.put("type", vizualisation_type) 180 if(vizualisation_type=='barchart'){ 181 182 // Determining what different bars we need (x-axis) 183 def list_of_row_contents = [] 184 rows.eachWithIndex { r, j -> 185 row_data[j].each { datapoint -> 186 list_of_row_contents.add(datapoint) 187 } 188 } 189 def bars = [] 190 // Make the list unique and stringify the individual objects 191 list_of_row_contents.unique().each { 192 item -> 193 bars << item.toString() 194 } 195 bars.sort() 196 return_data.put("x", bars) 197 // Determining what different bars we need (x-axis) 198 199 // Determine the different categories that datapoints can fall under 200 def categories = [] 201 columns.eachWithIndex { c, i -> 202 column_data[i].each { 203 cd -> 204 categories << cd 205 } 206 } 207 categories.unique().sort() 208 209 // Looking at the actual datapoints ... 210 columns.eachWithIndex { column, column_index -> 211 // ... for each column 212 categories.each { category -> 213 def data_per_bar = [:] // To store the datapoints contained in the current category, ordered by bar 214 // Make an entry for each of the bars that will be in the barchart 215 list_of_row_contents.each { bar -> 216 data_per_bar.put(bar, 0) 217 } 218 rows.eachWithIndex { row, row_index -> 219 // ... check for each row what it contains for each bar and category combination 220 // TODO: properly determine axis titles 221 if(possible_xaxis_title==""){ 222 possible_xaxis_title = row.id.split(',')[3] 223 } 224 list_of_row_contents.each { bar -> 225 // Check for the current bar, how many datapoints each category has 226 column_data[column_index].eachWithIndex { cd, cdi -> 227 if(bar==row_data[row_index][cdi] && cd==category){ 228 // Apparently this column contains a datapoint, whose entry in the relevant row equals the bar we are currently looking for and whose column equals the category that we are currently checking for. What this means is that the current datapoint should be included in this series 229 data_per_bar.put(bar, data_per_bar.get(bar)+1) 230 } 231 } 232 } 233 } 234 // Now add the data in the correct order (corresponding to bar order, so that the values end up in the right bar) 235 def data_per_bar_sorted = [] 236 list_of_row_contents.each { bar -> 237 data_per_bar_sorted.add(data_per_bar.get(bar)) 238 } 239 series.add(["name":category.toString(),"y":data_per_bar_sorted]) 240 } 241 } 242 243 if(possible_yaxis_title==""){ 244 // TODO: properly determine axis titles 245 possible_yaxis_title = "Amount" 246 if(possible_xaxis_title!=""){ 247 possible_yaxis_title += " of each "+possible_xaxis_title 248 } 249 } 250 return_data.put("yaxis", ["title" : possible_yaxis_title, "unit" : "..."]) 251 return_data.put("xaxis", ["title" : possible_xaxis_title, "unit": "..."]) 252 return_data.put("series", series) 253 data = return_data 254 } 255 } else { 256 // TODO: handle this exception properly 257 // We couldn't get any data to display... 258 } 259 println "\n\nReturn object: "+(data as JSON)+" ... " 260 render data as JSON 261 } 262 263 def getFieldData(study, case_switch, field_id, field_name, source, templatefield_source, field_type){ 264 if(source=="GSCF"){ 265 if(case_switch=="domain"){ 266 if(!study.getProperty(field_type)){ 267 // TODO: handle this exception properly 268 println "getFieldData: domainfield: Requested property '"+field_type+"' does not appear to exist in the study '"+study+"'." 269 return 270 } 271 def domain_objects = study.getProperty(field_type) // Simple way of getting at the relevant domain objects 272 273 if(domain_objects==null){ 274 // TODO: handle this exception properly 275 println "getFieldData: domainfield: A problem occurred... Nothing was collected." 276 } 277 278 // Get the value of the requested field out of the domain objects 279 def dat = [] 280 domain_objects.each{ 281 try{ 282 dat.add(it.getFieldValue(field_name)) 283 //println "getFieldData: domainfield: *** It appears as though we were successful" 284 } catch(Exception e){ 285 // TODO: handle this exception properly 286 println "getFieldData: domainfield: A problem occurred... "+e 287 } 288 } 289 return dat 290 } else { 291 TemplateField tf 292 try{ 293 tf = TemplateField.get(field_id) 294 } catch (Exception e){ 295 // TODO: handle this exception properly 296 println "getFieldData: templatefield: A problem occurred... "+e 297 } 298 def dat = [] 299 def collection 300 if(templatefield_source=="subjects"){ 301 collection = study.getSubjects() 302 } 303 if(templatefield_source=="assays"){ 304 collection = study.getAssays() 305 } 306 if(templatefield_source=="events"){ 307 collection = study.getEvents() 308 } 309 if(templatefield_source=="samplingEvents"){ 310 collection = study.getSamplingEvents() 311 } 312 if(templatefield_source=="samples"){ 313 collection = study.getSamples() 314 } 315 if(collection==null){ 316 // TODO: handle this exception properly 317 println "getFieldData: templatefield: A problem occurred... Nothing was collected." 318 } 319 collection.each { 320 try{ 321 dat.add(it.getFieldValue(tf.name)) 322 } catch(Exception e){ 323 // TODO: handle this exception properly 324 println "getFieldData: templatefield: A problem occurred... "+e 325 } 326 } 327 return dat 328 } 329 } else { 330 // Request for module data 331 def dat = [] 332 333 // User requested a particular feature 334 study.getAssays().each { assay -> 335 // Request for a particular assay and a particular feature 336 def urlVars = "assayToken="+assay.assayUUID+"&measurementToken="+field_id 337 def callUrl 338 AssayModule.list().each { module -> 339 if(source==module.name){ 340 try { 341 callUrl = module.url + "/rest/getMeasurementData/query?"+urlVars 342 def json = moduleCommunicationService.callModuleRestMethodJSON( module.url, callUrl ); 343 // First element contains sampletokens 344 // Second element contains the featurename 345 // Third element contains the measurement value 346 // NOTE: There is no need to couple a measurement value to a sampletoken, because that just doesn't produce interesting data 347 json[2].each { val -> 348 dat << val 349 } 350 } catch(Exception e){ 351 // TODO: handle this exception properly 352 println "No success with\n\t"+callUrl+"\n"+e 353 return null 354 } 355 } 356 } 357 } 358 return dat 359 } 360 } 361 362 def getFields(source, assays){ 363 /* 364 Gather fields related to this study from modules. 365 This will use the getMeasurements RESTful service. That service returns measurement types, AKA features. 366 It does not actually return measurements (the getMeasurementData call does). 367 The getFields method (or rather, the getMeasurements service) requires one or more assays and will return all measurement 368 types related to these assays. 369 So, the required variables for such a call are: 370 - a source variable, which can be obtained from AssayModule.list() (use the 'name' field) 371 - a list of assays, which can be obtained with study.getAssays() 372 */ 373 def collection = [] 374 def callUrl = "" 375 376 // Making a different call for each assay 377 // TODO: Change this to one call that requests fields for all assays, when you get that to work (in all cases) 378 assays.each { assay -> 379 def urlVars = "assayToken="+assay.assayUUID 380 AssayModule.list().each { module -> 381 if(source==module.name){ 382 try { 383 callUrl = module.url + "/rest/getMeasurements/query?"+urlVars 384 def json = moduleCommunicationService.callModuleRestMethodJSON( module.url, callUrl ); 385 json.each{ jason -> 386 collection.add(jason) 387 } 388 } catch(Exception e){ 389 // Todo: properly handle this exception 390 println "No success with\n\t"+callUrl+"\n"+e 391 return null 392 } 393 } 394 } 395 } 396 397 def fields = [] 398 // Formatting the data 399 collection.each { field -> 400 fields << [ "id": field+","+source+","+"feature"+","+field, "source": source, "category": "feature", "name": source+" feature "+field ] 401 } 402 return fields 403 } 404 405 def getFields(study, category, type){ 406 /* 407 Gather fields related to this study from GSCF. 408 This requires: 409 - a study. 410 - a category variable, e.g. "events". 411 - a type variable, either "domainfields" or "templatefields". 412 */ 413 414 // Collecting the data from it's source 415 def collection 416 def fields = [] 417 def source = "GSCF" 418 419 // Gathering the data 420 if(category=="subjects"){ 421 if(type=="domainfields"){ 422 collection = Subject.giveDomainFields() 423 } 424 if(type=="templatefields"){ 425 collection = study.giveSubjectTemplates().fields 426 } 427 } 428 if(category=="events"){ 429 if(type=="domainfields"){ 430 collection = Event.giveDomainFields() 431 } 432 if(type=="templatefields"){ 433 collection = study.giveEventTemplates().fields 434 } 435 } 436 if(category=="samplingEvents"){ 437 if(type=="domainfields"){ 438 collection = SamplingEvent.giveDomainFields() 439 } 440 if(type=="templatefields"){ 441 collection = study.giveSamplingEventTemplates().fields 442 } 443 } 444 if(category=="samples"){ 445 if(type=="domainfields"){ 446 collection = Sample.giveDomainFields() 447 } 448 if(type=="templatefields"){ 449 collection = study.giveEventTemplates().fields 450 } 451 } 452 if(category=="assays"){ 453 if(type=="domainfields"){ 454 collection = Event.giveDomainFields() 455 } 456 if(type=="templatefields"){ 457 collection = study.giveEventTemplates().fields 458 } 459 } 460 461 // Formatting the data 462 if(type=="domainfields"){ 463 collection.each { field -> 464 fields << [ "id": field.name+","+source+","+category+","+field.name, "source": source, "category": category, "name": category.capitalize()+" "+field.name ] 465 } 466 } 467 if(type=="templatefields"){ 468 collection.each { field -> 469 for(int i = 0; i < field.size(); i++){ 470 fields << [ "id": field[i].id+","+source+","+TemplateField.toString()+","+field[i].name+","+category, "source": source, "category": category, "name": category.capitalize()+" "+field[i].name ] 471 } 472 } 473 } 474 475 return fields 476 } 297 /** 298 * Retrieve the field data for the selected field 299 * @param study Study for which the data should be retrieved 300 * @param samples Samples for which the data should be retrieved 301 * @param fieldId ID of the field to return data for 302 * @return A list of values of the selected field for all samples. If a value 303 * could not be retrieved for a sample, null is returned. Examples: 304 * [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ] 305 */ 306 def getFieldData( study, samples, fieldId ) { 307 // Parse the fieldId as given by the user 308 def parsedField = parseFieldId( fieldId ); 309 310 def data = [] 311 312 if( parsedField.source == "GSCF" ) { 313 // Retrieve data from GSCF itself 314 def closure = valueCallback( parsedField.type ) 315 316 if( closure ) { 317 samples.each { sample -> 318 // Retrieve the value for the selected field for this sample 319 def value = closure( sample, parsedField.name ); 320 321 if( value ) { 322 data << value; 323 } else { 324 // Return null if the value is not found 325 data << null 326 } 327 } 328 } else { 329 // TODO: Handle error properly 330 // Closure could not be retrieved, probably because the type is incorrect 331 data = samples.collect { return null } 332 } 333 } else { 334 // Data must be retrieved from a module 335 data = getModuleData( study, samples, parsedField.source, parsedField.name ); 336 } 337 338 return data 339 } 340 341 /** 342 * Retrieve data for a given field from a data module 343 * @param study Study to retrieve data for 344 * @param samples Samples to retrieve data for 345 * @param source_module Name of the module to retrieve data from 346 * @param fieldName Name of the measurement type to retrieve (i.e. measurementToken) 347 * @return A list of values of the selected field for all samples. If a value 348 * could not be retrieved for a sample, null is returned. Examples: 349 * [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ] 350 */ 351 def getModuleData( study, samples, source_module, fieldName ) { 352 def data = [] 353 354 // TODO: Handle values that should be retrieved from multiple assays 355 // TODO: Use Assay ID or AssayModule ID in field-ids, instead of names 356 def assay = study.assays.find { it.module.name == source_module }; 357 358 if( assay ) { 359 // Request for a particular assay and a particular feature 360 def urlVars = "assayToken=" + assay.assayUUID + "&measurementToken="+fieldName 361 urlVars += "&" + samples.collect { "sampleToken=" + it.sampleUUID }.join( "&" ); 362 363 def callUrl 364 try { 365 callUrl = assay.module.url + "/rest/getMeasurementData" 366 def json = moduleCommunicationService.callModuleMethod( assay.module.url, callUrl, urlVars, "POST" ); 367 368 if( json ) { 369 // First element contains sampletokens 370 // Second element contains the featurename 371 // Third element contains the measurement value 372 def sampleTokens = json[ 0 ] 373 def measurements = json[ 2 ] 374 375 // Loop through the samples 376 samples.each { sample -> 377 // Search for this sampletoken 378 def sampleToken = sample.sampleUUID; 379 def index = sampleTokens.findIndexOf { it == sampleToken } 380 381 if( index > -1 ) { 382 data << measurements[ index ]; 383 } else {[ "x": inputData.columnIds[ 0 ], "y": inputData.rowIds[ 0 ] ] 384 data << null 385 } 386 } 387 } else { 388 // TODO: handle error 389 // Returns an empty list with as many elements as there are samples 390 data = samples.collect { return null } 391 } 392 393 } catch(Exception e){ 394 // TODO: handle this exception properly 395 println "No success with\n\t"+callUrl+"\n"+e 396 e.printStackTrace(); 397 398 // Returns an empty list with as many elements as there are samples 399 data = samples.collect { return null } 400 } 401 } else { 402 // TODO: Handle error correctly 403 // Returns an empty list with as many elements as there are samples 404 data = samples.collect { return null } 405 } 406 407 return data 408 409 } 410 411 /** 412 * Group the field data on the values of the specified axis. For example, for a bar chart, the values 413 * on the x-axis should be grouped. Currently, the values for each group are averaged, and the standard 414 * error of the mean is returned in the 'error' property 415 * @param data Data for both group- and value axis. The output of getAllFieldData fits this input 416 * @param groupAxis Name of the axis to group on. Defaults to "x" 417 * @param valueAxis Name of the axis where the values are. Defaults to "y" 418 * @param errorName Key in the output map where 'error' values (SEM) are stored. Defaults to "error" 419 * @param unknownName Name of the group for all null groups. Defaults to "unknown" 420 * @return A map with the keys 'groupAxis', 'valueAxis' and 'errorName'. The values in the map are lists of values of the 421 * selected field for all groups. For example, if the input is 422 * [ "x": [ "male", "male", "female", "female", null, "female" ], "y": [ 3, 6, null, 10, 4, 5 ] ] 423 * the output will be: 424 * [ "x": [ "male", "female", "unknown" ], "y": [ 4.5, 7.5, 4 ], "error": [ 1.5, 2.5, 0 ] ] 425 * 426 * As you can see: null values in the valueAxis are ignored. Null values in the 427 * group axis are combined into a 'unknown' category. 428 */ 429 def groupFieldData( data, groupAxis = "x", valueAxis = "y", errorName = "error", unknownName = "unknown" ) { 430 // Create a unique list of values in the groupAxis. First flatten the list, since it might be that a 431 // sample belongs to multiple groups. In that case, the group names should not be the lists, but the list 432 // elements. A few lines below, this case is handled again by checking whether a specific sample belongs 433 // to this group. 434 // After flattening, the list is uniqued. The closure makes sure that values with different classes are 435 // always treated as different items (e.g. "" should not equal 0, but it does if using the default comparator) 436 def groups = data[ groupAxis ] 437 .flatten() 438 .unique { it == null ? "null" : it.class.name + it.toString() } 439 440 // Make sure the null category is last 441 groups = groups.findAll { it != null } + groups.findAll { it == null } 442 443 // Gather names for the groups. Most of the times, the group names are just the names, only with 444 // a null value, the unknownName must be used 445 def groupNames = groups.collect { it != null ? it : unknownName } 446 447 // Generate the output object 448 def outputData = [:] 449 outputData[ valueAxis ] = []; 450 outputData[ errorName ] = []; 451 outputData[ groupAxis ] = groupNames; 452 453 // Loop through all groups, and gather the values for this group 454 groups.each { group -> 455 // Find the indices of the samples that belong to this group. if a sample belongs to multiple groups (i.e. if 456 // the samples groupAxis contains multiple values, is a collection), the value should be used in all groups. 457 def indices= data[ groupAxis ].findIndexValues { it instanceof Collection ? it.contains( group ) : it == group }; 458 def values = data[ valueAxis ][ indices ] 459 460 def dataForGroup = computeMeanAndError( values ); 461 462 outputData[ valueAxis ] << dataForGroup.value 463 outputData[ errorName ] << dataForGroup.error 464 } 465 466 return outputData 467 } 468 469 /** 470 * Formats the grouped data in such a way that the clientside visualization method 471 * can handle the data correctly. 472 * @param groupedData Data that has been grouped using the groupFields method 473 * @param fields Map with key-value pairs determining the name and fieldId to retrieve data for. Example: 474 * [ "x": "field-id-1", "y": "field-id-3" ] 475 * @param groupAxis Name of the axis to with group data. Defaults to "x" 476 * @param valueAxis Name of the axis where the values are stored. Defaults to "y" 477 * @param errorName Key in the output map where 'error' values (SEM) are stored. Defaults to "error" * 478 * @return A map like the following: 479 * 480 { 481 "type": "barchart", 482 "x": [ "Q1", "Q2", "Q3", "Q4" ], 483 "xaxis": { "title": "quarter 2011", "unit": "" }, 484 "yaxis": { "title": "temperature", "unit": "degrees C" }, 485 "series": [ 486 { 487 "name": "series name", 488 "y": [ 5.1, 3.1, 20.6, 15.4 ], 489 "error": [ 0.5, 0.2, 0.4, 0.5 ] 490 }, 491 ] 492 } 493 * 494 */ 495 def formatData( groupedData, fields, groupAxis = "x", valueAxis = "y", errorName = "error" ) { 496 // TODO: Handle name and unit of fields correctly 497 498 def return_data = [:] 499 return_data[ "type" ] = "barchart" 500 return_data[ "x" ] = groupedData[ groupAxis ].collect { it.toString() } 501 return_data.put("yaxis", ["title" : parseFieldId( fields[ valueAxis ] ).name, "unit" : "" ]) 502 return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": "" ]) 503 return_data.put("series", [[ 504 "name": "Y", 505 "y": groupedData[ valueAxis ], 506 "error": groupedData[ errorName ] 507 ]]) 508 509 return return_data; 510 } 511 512 /** 513 * Returns a closure for the given entitytype that determines the value for a criterion 514 * on the given object. The closure receives two parameters: the sample and a field. 515 * 516 * For example: 517 * How can one retrieve the value for subject.name, given a sample? This can be done by 518 * returning the field values sample.parentSubject: 519 * { sample, field -> return getFieldValue( sample.parentSubject, field ) } 520 * @return Closure that retrieves the value for a field and the given field 521 */ 522 protected Closure valueCallback( String entity ) { 523 switch( entity ) { 524 case "Study": 525 case "studies": 526 return { sample, field -> return getFieldValue( sample.parent, field ) } 527 case "Subject": 528 case "subjects": 529 return { sample, field -> return getFieldValue( sample.parentSubject, field ); } 530 case "Sample": 531 case "samples": 532 return { sample, field -> return getFieldValue( sample, field ) } 533 case "Event": 534 case "events": 535 return { sample, field -> 536 if( !sample || !sample.parentEventGroup || !sample.parentEventGroup.events || sample.parentEventGroup.events.size() == 0 ) 537 return null 538 539 return sample.parentEventGroup.events?.collect { getFieldValue( it, field ) }; 540 } 541 case "SamplingEvent": 542 case "samplingEvents": 543 return { sample, field -> return getFieldValue( sample.parentEvent, field ); } 544 case "Assay": 545 case "assays": 546 return { sample, field -> 547 def sampleAssays = Assay.findByParent( sample.parent ).findAll { it.samples?.contains( sample ) }; 548 if( sampleAssays && sampleAssays.size() > 0 ) 549 return sampleAssays.collect { getFieldValue( it, field ) } 550 else 551 return null 552 } 553 } 554 } 555 556 /** 557 * Computes the mean value and Standard Error of the mean (SEM) for the given values 558 * @param values List of values to compute the mean and SEM for. Strings and null 559 * values are ignored 560 * @return Map with two keys: 'value' and 'error' 561 */ 562 protected Map computeMeanAndError( values ) { 563 // TODO: Handle the case that one of the values is a list. In that case, 564 // all values should be taken into account. 565 def mean = computeMean( values ); 566 def error = computeSEM( values, mean ); 567 568 return [ 569 "value": mean, 570 "error": error 571 ] 572 } 573 574 /** 575 * Computes the mean of the given values. Values that can not be parsed to a number 576 * are ignored. If no values are given, the mean of 0 is returned. 577 * @param values List of values to compute the mean for 578 * @return Arithmetic mean of the values 579 */ 580 protected def computeMean( List values ) { 581 def sumOfValues = 0; 582 def sizeOfValues = 0; 583 values.each { value -> 584 def num = getNumericValue( value ); 585 if( num != null ) { 586 sumOfValues += num; 587 sizeOfValues++ 588 } 589 } 590 591 if( sizeOfValues > 0 ) 592 return sumOfValues / sizeOfValues; 593 else 594 return 0; 595 } 596 597 /** 598 * Computes the standard error of mean of the given values. 599 * Values that can not be parsed to a number are ignored. 600 * If no values are given, the standard deviation of 0 is returned. 601 * @param values List of values to compute the standard deviation for 602 * @param mean Mean of the list (if already computed). If not given, the mean 603 * will be computed using the computeMean method 604 * @return Standard error of the mean of the values or 0 if no values can be used. 605 */ 606 protected def computeSEM( List values, def mean = null ) { 607 if( mean == null ) 608 mean = computeMean( values ) 609 610 def sumOfDifferences = 0; 611 def sizeOfValues = 0; 612 values.each { value -> 613 def num = getNumericValue( value ); 614 if( num != null ) { 615 sumOfDifferences += Math.pow( num - mean, 2 ); 616 sizeOfValues++ 617 } 618 } 619 620 if( sizeOfValues > 0 ) { 621 def std = Math.sqrt( sumOfDifferences / sizeOfValues ); 622 return std / Math.sqrt( sizeOfValues ); 623 } else { 624 return 0; 625 } 626 } 627 628 /** 629 * Return the numeric value of the given object, or null if no numeric value could be returned 630 * @param value Object to return the value for 631 * @return Number that represents the given value 632 */ 633 protected Number getNumericValue( value ) { 634 // TODO: handle special types of values 635 if( value instanceof Number ) { 636 return value; 637 } else if( value instanceof RelTime ) { 638 return value.value; 639 } 640 641 return null 642 } 643 644 /** 645 * Returns a field for a given templateentity 646 * @param object TemplateEntity (or subclass) to retrieve data for 647 * @param fieldName Name of the field to return data for. 648 * @return Value of the field or null if the value could not be retrieved 649 */ 650 protected def getFieldValue( TemplateEntity object, String fieldName ) { 651 if( !object || !fieldName ) 652 return null; 653 654 try { 655 return object.getFieldValue( fieldName ); 656 } catch( Exception e ) { 657 return null; 658 } 659 } 660 661 /** 662 * Parses a fieldId that has been created earlier by createFieldId 663 * @param fieldId FieldId to parse 664 * @return Map with attributes of the selected field. Keys are 'name', 'id', 'source' and 'type' 665 * @see createFieldId 666 */ 667 protected Map parseFieldId( String fieldId ) { 668 def attrs = [:] 669 670 def parts = fieldId.split(",") 671 672 attrs = [ 673 "id": parts[ 0 ], 674 "name": parts[ 1 ], 675 "source": parts[ 2 ], 676 "type": parts[ 3 ] 677 ] 678 } 679 680 /** 681 * Create a fieldId based on the given attributes 682 * @param attrs Map of attributes for this field. Keys may be 'name', 'id', 'source' and 'type' 683 * @return Unique field ID for these parameters 684 * @see parseFieldId 685 */ 686 protected String createFieldId( Map attrs ) { 687 // TODO: What is one of the attributes contains a comma? 688 def name = attrs.name; 689 def id = attrs.id ?: name; 690 def source = attrs.source; 691 def type = attrs.type ?: "" 692 693 return id + "," + name + "," + source + "," + type; 694 } 695 477 696 } -
trunk/grails-app/services/dbnp/modules/ModuleCommunicationService.groovy
r1864 r1991 143 143 switch( requestMethod.toUpperCase() ) { 144 144 case "GET": 145 log.trace( "Using GET method" ); 145 146 def url = restUrl + "?" + args; 146 147 def connection = url.toURL().openConnection(); … … 150 151 break 151 152 case "POST": 153 log.trace( "Using POST method" ); 152 154 def connection = restUrl.toURL().openConnection() 153 155 connection.setRequestMethod( "POST" ); -
trunk/grails-app/views/visualize/index.gsp
r1983 r1991 12 12 <g:javascript src="jqplot/plugins/jqplot.barRenderer.min.js" /> 13 13 <g:javascript src="jqplot/plugins/jqplot.categoryAxisRenderer.min.js" /> 14 <g:javascript src="jqplot/src/plugins/jqplot.pointLabels.min.js" /> 15 <g:javascript src="jqplot/src/plugins/jqplot.canvasTextRenderer.min.js" /> 16 <g:javascript src="jqplot/src/plugins/jqplot.canvasAxisLabelRenderer.min.js" /> 14 <g:javascript src="jqplot/plugins/jqplot.pointLabels.min.js" /> 15 <g:javascript src="jqplot/plugins/jqplot.canvasTextRenderer.min.js" /> 16 <g:javascript src="jqplot/plugins/jqplot.canvasAxisLabelRenderer.min.js" /> 17 18 <g:javascript src="visualization.js" /> 19 <link rel="stylesheet" type="text/css" href="<g:resource dir='css' file='visualization.css' />" /> 17 20 18 21 <script type="text/javascript"> … … 25 28 "getData": "<g:createLink action="getData" />" 26 29 }; 27 28 function showError( message ) {29 $( '#ajaxError' ).text( message );30 $( '#ajaxError' ).show();31 }32 33 /**34 * Gathers data for the given request type from the form elements on the page35 * @param type String Can be 'getStudies', 'getFields', 'getVisualizationType' or 'getData'36 * @return Object Object with the data to be sent to the server37 */38 function gatherData( type ) {39 var data = {};40 41 // different types of request require different data arrays42 // However, some data is required for all types. For that reason,43 // the fallthrough option in the switch statement is used.44 switch( type ) {45 case "getData":46 var typeElement = $( '#type' );47 data[ "type" ] = { "id": typeElement.val() };48 case "getVisualizationTypes":49 var rowsElement = $( '#rows' );50 var columnsElement = $( '#columns' );51 data[ "rows" ] = [52 { "id": rowsElement.val() }53 ];54 data[ "columns" ] = [55 { "id": columnsElement.val() }56 ];57 case "getFields":58 var studyElement = $( '#study' );59 data[ "studies" ] = [60 { "id": studyElement.val() }61 ];62 63 case "getStudies":64 }65 66 return data;67 }68 69 /**70 * Executes an ajax call in a standardized way. Retrieves data to be sent with gatherData71 * The ajaxParameters map will be sent to the $.ajax call72 * @param action Name of the action to execute. Is also given to the gatherData method73 * as a parameter and the url will be determined based on this parameter.74 * @param ajaxParameters Hashmap with parameters that are sent to the $.ajax call. The entries75 * url, data and dataType are set by this method.76 * An additional key 'errorMessage' can be given, with the message that will be77 * shown if an error occurrs in this method. In that case, the 'error' method from78 * the ajaxParameters method will be overwritten.79 * @see visualizationUrls80 * @see jQuery.ajax81 */82 function executeAjaxCall( action, ajaxParameters ) {83 var data = gatherData( action );84 85 // If no parameters are given, create an empty map86 if( !ajaxParameters )87 ajaxParameters = {}88 89 if( ajaxParameters[ "errorMessage" ] ) {90 var message = ajaxParameters[ "errorMessage" ];91 ajaxParameters[ "error" ] = function( jqXHR, textStatus, errorThrown ) {92 // An error occurred while retrieving fields from the server93 showError( "An error occurred while retrieving variables from the server. Please try again or contact a system administrator." );94 }95 96 // Remove the error message97 delete ajaxParameters[ "errorMessage" ];98 }99 100 // Retrieve a new list of fields from the controller101 // based on the study we chose102 $.ajax($.extend({103 url: visualizationUrls[ action ],104 data: "data=" + JSON.stringify( data ),105 dataType: "json",106 }, ajaxParameters ) );107 }108 109 function changeStudy() {110 executeAjaxCall( "getFields", {111 "errorMessage": "An error occurred while retrieving variables from the server. Please try again or contact a system administrator.",112 "success": function( data, textStatus, jqXHR ) {113 // Remove all previous entries from the list114 $( '#rows, #columns' ).empty();115 116 // Add all fields to the lists117 $.each( data, function( idx, field ) {118 $( '#rows, #columns' ).append( $( "<option>" ).val( field.id ).text( field.name ) );119 });120 121 $( "#step2" ).show();122 $( "#step3" ).hide();123 }124 });125 }126 127 function changeFields() {128 executeAjaxCall( "getVisualizationTypes", {129 "errorMessage": "An error occurred while retrieving visualization types from the server. Please try again or contact a system administrator.",130 "success": function( data, textStatus, jqXHR ) {131 // Remove all previous entries from the list132 $( '#types' ).empty();133 134 // Add all fields to the lists135 $.each( data, function( idx, field ) {136 $( '#types' ).append( $( "<option>" ).val( field.id ).text( field.name ) );137 });138 139 $( "#step3" ).show();140 }141 });142 }143 144 function visualize() {145 executeAjaxCall( "getData", {146 "errorMessage": "An error occurred while retrieving data from the server. Please try again or contact a system administrator.",147 "success": function( data, textStatus, jqXHR ) {148 /*149 Data expected:150 {151 "type": "barchart",152 "x": [ "Q1", "Q2", "Q3", "Q4" ],153 "xaxis": { "title": "quarter 2011", "unit": "" },154 "yaxis": { "title": "temperature", "unit": "degrees C" },155 "series": [156 {157 "name": "series name",158 "y": [ 5.1, 3.1, 20.6, 15.4 ],159 "error": [ 0.5, 0.2, 0.4, 0.5 ]160 },161 ]162 }163 */164 165 // TODO: error handling if incorrect data is returned166 167 // Retrieve the datapoints from the json object168 var dataPoints = [];169 var series = [];170 171 $.each(data.series, function(idx, element ) {172 dataPoints[ dataPoints.length ] = element.y;173 series[ series.length ] = { "label": element.name };174 });175 176 // TODO: create a chart based on the data that is sent by the user and the type of chart177 // chosen by the user178 chart = $.jqplot('visualization', dataPoints, {179 // Tell the plot to stack the bars.180 stackSeries: true,181 captureRightClick: true,182 seriesDefaults:{183 renderer:$.jqplot.BarRenderer,184 rendererOptions: {185 // Put a 30 pixel margin between bars.186 barMargin: 30,187 // Highlight bars when mouse button pressed.188 // Disables default highlighting on mouse over.189 highlightMouseDown: true190 },191 pointLabels: {show: true}192 },193 series: series,194 axes: {195 xaxis: {196 renderer: $.jqplot.CategoryAxisRenderer,197 ticks: data.x,198 label: data[ "xaxis" ].title + " (" + data[ "xaxis" ].unit + ")",199 labelRenderer: $.jqplot.CanvasAxisLabelRenderer200 },201 yaxis: {202 // Don't pad out the bottom of the data range. By default,203 // axes scaled as if data extended 10% above and below the204 // actual range to prevent data points right on grid boundaries.205 // Don't want to do that here.206 padMin: 0,207 label: data[ "yaxis" ].title + " (" + data[ "yaxis" ].unit + ")",208 labelRenderer: $.jqplot.CanvasAxisLabelRenderer209 }210 },211 legend: {212 show: true,213 location: 'e',214 placement: 'outside'215 }216 });217 218 $( "#visualization" ).show();219 },220 });221 }222 30 </script> 223 <style type="text/css">224 /* #step2, #step3 { display: none; } */225 #ajaxError {226 display: none;227 border: 1px solid #f99; /* #006dba; */228 margin-bottom: 10px;229 margin-top: 10px;230 231 background: #ffe0e0 url(${fam.icon( name: 'error' )}) 10px 10px no-repeat;232 padding: 10px 10px 10px 33px;233 }234 235 label { display: inline-block; zoom: 1; *display: inline; width: 110px; margin-top: 10px; }236 237 #visualizationForm { position: relative; margin: 10px 0; font-size: 11px; }238 #visualizationForm h3 { font-size: 13px; }239 #visualizationForm h3 .nummer { display: inline-block; zoom: 1; *display: inline; width: 25px; }240 241 #visualizationForm p { margin-left: 25px; }242 243 table.jqplot-table-legend { width: 100px; }244 </style>245 31 </head> 246 32 <body>
Note: See TracChangeset
for help on using the changeset viewer.