source: trunk/grails-app/controllers/dbnp/visualization/VisualizeController.groovy @ 2001

Last change on this file since 2001 was 2001, checked in by taco@…, 8 years ago

Update for the visualization controller, first support for multiple categories on the y axis

File size: 38.8 KB
Line 
1/**
2 * Visualize Controller
3 *
4 * This controller enables the user to visualize his data
5 *
6 * @author  robert@thehyve.nl
7 * @since       20110825
8 * @package     dbnp.visualization
9 *
10 * Revision information:
11 * $Rev$
12 * $Author$
13 * $Date$
14 */
15package dbnp.visualization
16
17import dbnp.studycapturing.*;
18import grails.converters.JSON
19
20import org.dbnp.gdt.*
21
22class VisualizeController {
23        def authenticationService
24        def moduleCommunicationService
25    def infoMessage = ""
26    final int categoricalData = 0
27    final int numericalData = 1
28
29        /**
30         * Shows the visualization screen
31         */
32        def index = {
33                [ studies: Study.giveReadableStudies( authenticationService.getLoggedInUser() )]
34        }
35
36        def getStudies = {
37                def studies = Study.giveReadableStudies( authenticationService.getLoggedInUser() );
38        return sendResults(studies)
39        }
40
41        def getFields = {
42                def input_object
43                def studies
44
45                try{
46                        input_object = parseGetDataParams();
47                } catch(Exception e) {
48                        log.error("VisualizationController: getFields: "+e)
49            return returnError(400, "An error occured while retrieving the user input.")
50                }
51
52        // Check to see if we have enough information
53        if(input_object==null || input_object?.studyIds==null){
54            infoMessage = "Please select a study."
55            return sendInfoMessage()
56        } else {
57            studies = input_object.studyIds[0]
58        }
59
60                def fields = [];
61
62        /*
63         Gather fields related to this study from GSCF.
64         This requires:
65         - a study.
66         - a category variable, e.g. "events".
67         - a type variable, either "domainfields" or "templatefields".
68         */
69        // TODO: Handle multiple studies
70        def study = Study.get(studies)
71
72        if(study!=null){
73            fields += getFields(study, "subjects", "domainfields")
74            fields += getFields(study, "subjects", "templatefields")
75            fields += getFields(study, "events", "domainfields")
76            fields += getFields(study, "events", "templatefields")
77            fields += getFields(study, "samplingEvents", "domainfields")
78            fields += getFields(study, "samplingEvents", "templatefields")
79            fields += getFields(study, "assays", "domainfields")
80            fields += getFields(study, "assays", "templatefields")
81            fields += getFields(study, "samples", "domainfields")
82            fields += getFields(study, "samples", "domainfields")
83
84            /*
85            Gather fields related to this study from modules.
86            This will use the getMeasurements RESTful service. That service returns measurement types, AKA features.
87            It does not actually return measurements (the getMeasurementData call does).
88            The getFields method (or rather, the getMeasurements service) requires one or more assays and will return all measurement
89            types related to these assays.
90            So, the required variables for such a call are:
91              - a source variable, which can be obtained from AssayModule.list() (use the 'name' field)
92              - an assay, which can be obtained with study.getAssays()
93             */
94
95            def moduleFields = []
96            study.getAssays().each { assay ->
97                def list = []
98                list = getFields(assay.module.id, assay)
99                if(list!=null){
100                    if(list.size()!=0){
101                        fields += list
102                        moduleFields += list
103                    }
104                }
105            }
106
107            // Creating the option to select composite fields (fields that all occur on one module)
108            AssayModule.list().each { am ->
109                def list = []
110                moduleFields.each { mf ->
111                    if(mf.source==am.id){
112                        list << mf
113                    }
114                }
115                if(list!=[]){
116                    def compositeId = ""
117                    def compositeIdId = ""
118                    def compositeIdName = ""
119                    def compositeIdSource = ""
120                    def compositeIdType = ""
121                    def source = am.id
122                    def compositeCategory = ""
123                    def compositeName = ""
124
125                    for(int i = 0; i < list.size(); i++){
126                        def singleAssayId = list[i].id.split(',')[2]
127                        if(i==0){
128                            compositeIdType += list[i].category
129                            compositeCategory += list[i].category
130                            compositeIdSource += "compositeId"+singleAssayId
131                            compositeIdName += list[i].name
132                            compositeName += list[i].name
133                        } else {
134                            compositeIdType += "@SEP@"+list[i].category
135                            compositeCategory += "@SEP@"+list[i].category
136                            compositeIdSource += "@SEP@"+singleAssayId
137                            compositeIdName += "@SEP@"+list[i].name
138                            if(list.size()>1 && i==(list.size()-1)){
139                                compositeName += " and "+list[i].name
140                            } else {
141                                compositeName += ", "+list[i].name
142                            }
143                        }
144                    }
145                    compositeId = createFieldId(id: compositeIdId, name: compositeIdName, source: compositeIdSource, type: compositeIdType)
146                    def compositeField = ["id": compositeId, "source": source, "category": compositeCategory, "name": compositeName]
147                    fields << compositeField
148                    //println "compositeField: "+compositeField
149                }
150            }
151
152
153            // TODO: Maybe we should add study's own fields
154        } else {
155            log.error("VisualizationController: getFields: The requested study could not be found. Id: "+studies)
156            return returnError(404, "The requested study could not be found.")
157        }
158
159                return sendResults(fields)
160        }
161
162        def getVisualizationTypes = {
163        def inputData = parseGetDataParams();
164
165        if(inputData.columnIds == null || inputData.columnIds == [] || inputData.columnIds[0] == null || inputData.columnIds[0] == ""){
166            infoMessage = "Please select columndata."
167            return sendInfoMessage()
168        }
169        if(inputData.rowIds == null || inputData.rowIds == [] ||  inputData.rowIds[0] == null ||   inputData.rowIds[0] == ""){
170            infoMessage = "Please select rowdata."
171            return sendInfoMessage()
172        }
173
174
175        // TODO: handle the case of multiple fields on an axis
176        // Determine data types
177        def rowType = determineFieldType(inputData.studyIds[0], inputData.rowIds[0])
178        def columnType = determineFieldType(inputData.studyIds[0], inputData.columnIds[0])
179
180        // Determine possible visualization types
181        // TODO: Determine possible visualization types
182        def types = []
183        if(rowType==categoricalData){
184            if(columnType==categoricalData){
185                types = [ [ "id": "table", "name": "Table"] ];
186            }
187            if(columnType==numericalData){
188                types = [ [ "id": "horizontal_barchart", "name": "Horizontal barchart"] ];
189            }
190        }
191        if(rowType==numericalData){
192            if(columnType==categoricalData){
193                types = [ [ "id": "barchart", "name": "Barchart"], [ "id": "linechart", "name": "Linechart"] ];
194            }
195            if(columnType==numericalData){
196                types = [ [ "id": "scatterplot", "name": "Scatterplot"], [ "id": "linechart", "name": "Linechart"] ];
197            }
198        }
199
200        return sendResults(types)
201        }
202
203    def getFields(source, assay){
204        /*
205        Gather fields related to this study from modules.
206        This will use the getMeasurements RESTful service. That service returns measurement types, AKA features.
207        getMeasurements does not actually return measurements (the getMeasurementData call does).
208        The getFields method (or rather, the getMeasurements service) requires one or more assays and will return all measurement
209        types related to these assays.
210        So, the required variables for such a call are:
211          - a source variable, which can be obtained from AssayModule.list() (use the 'name' field)
212          - a list of assays, which can be obtained with study.getAssays()
213
214        Output is a list of items. Each item contains
215          - an 'id'
216          - a 'source', which is a module identifier
217          - a 'category', which indicates where the field can be found. When dealing with module data as we are here, this is the assay name(s)
218          - a 'name', which is the the name of the field
219         */
220        def fields = []
221        def callUrl = ""
222
223        // Making a different call for each assay
224        // TODO: Change this to one call that requests fields for all assays, when you get that to work (in all cases)
225
226        def urlVars = "assayToken="+assay.assayUUID
227        try {
228            callUrl = ""+assay.module.url + "/rest/getMeasurements/query?"+urlVars
229            def json = moduleCommunicationService.callModuleRestMethodJSON( assay.module.url /* consumer */, callUrl );
230            def collection = []
231            json.each{ jason ->
232                collection.add(jason)
233            }
234            // Formatting the data
235            collection.each { field ->
236                // For getting this field from this assay
237                fields << [ "id": createFieldId( id: field, name: field, source: assay.id, type: ""+assay.name), "source": source, "category": ""+assay.name, "name": field ]
238            }
239        } catch(Exception e){
240            //returnError(404, "An error occured while trying to collect field data from a module. Most likely, this module is offline.")
241            infoMessage = "An error occured while trying to collect field data from a module. Most likely, this module is offline."
242            log.error("VisualizationController: getFields: "+e)
243        }
244
245        return fields
246    }
247
248   def getFields(study, category, type){
249        /*
250        Gather fields related to this study from GSCF.
251        This requires:
252          - a study.
253          - a category variable, e.g. "events".
254          - a type variable, either "domainfields" or "templatefields".
255
256        Output is a list of items, which are formatted by the formatGSCFFields function.
257        */
258
259        // Collecting the data from it's source
260        def collection = []
261        def fields = []
262        def source = "GSCF"
263
264        // Gathering the data
265        if(category=="subjects"){
266            if(type=="domainfields"){
267                collection = Subject.giveDomainFields()
268            }
269            if(type=="templatefields"){
270                collection = study?.samples?.parentSubject?.template?.fields
271            }
272        }
273        if(category=="events"){
274            if(type=="domainfields"){
275                collection = Event.giveDomainFields()
276            }
277            if(type=="templatefields"){
278                collection = study?.samples?.parentEventGroup?.events?.template?.fields
279            }
280        }
281        if(category=="samplingEvents"){
282            if(type=="domainfields"){
283                collection = SamplingEvent.giveDomainFields()
284            }
285            if(type=="templatefields"){
286                collection = study?.samples?.parentEventGroup?.samplingEvents?.template?.fields
287            }
288        }
289        if(category=="samples"){
290            if(type=="domainfields"){
291                collection = Sample.giveDomainFields()
292            }
293            if(type=="templatefields"){
294                collection = study?.samples?.template?.fields
295            }
296        }
297        if(category=="assays"){
298            if(type=="domainfields"){
299                collection = Assay.giveDomainFields()
300            }
301            if(type=="templatefields"){
302                collection = study?.assays?.template?.fields
303            }
304        }
305
306        collection.unique()
307
308        // Formatting the data
309        fields += formatGSCFFields(type, collection, source, category)
310
311        return fields
312    }
313
314    def formatGSCFFields(type, inputObject, source, category){
315        /*  The formatGSCFFields function can receive both lists of fields and single fields. If it receives a list, it calls itself again for each item in the list. This way, the original formatGSCFFields call will return single fields regardless of it's input.
316
317        Output is a list of items. Each item contains
318          - an 'id'
319          - a 'source', which in this case will be "GSCF"
320          - a 'category', which indicates where the field can be found, e.g. "subjects", "samplingEvents"
321          - a 'name', which is the the name of the field
322         */
323        if(inputObject==null || inputObject == []){
324            return []
325        }
326        def fields = []
327        if(inputObject instanceof Collection){
328            // Apparently this field is actually a list of fields.
329            // We will call ourselves again with the list's elements as input.
330            // These list elements will themselves go through this check again, effectively flattening the original input
331            for(int i = 0; i < inputObject.size(); i++){
332                fields += formatGSCFFields(type, inputObject[i], source, category)
333            }
334            return fields
335        } else {
336            // This is a single field. Format it and return the result.
337            if(type=="domainfields"){
338                fields << [ "id": createFieldId( id: inputObject.name, name: inputObject.name, source: source, type: category ), "source": source, "category": category, "name": inputObject.name ]
339            }
340            if(type=="templatefields"){
341                fields << [ "id": createFieldId( id: inputObject.id, name: inputObject.name, source: source, type: category ), "source": source, "category": category, "name": inputObject.name ]
342            }
343            return fields
344        }
345    }
346
347        /**
348         * Retrieves data for the visualization itself.
349         */
350        def getData = {
351                // Extract parameters
352                // TODO: handle erroneous input data
353                def inputData = parseGetDataParams();
354
355        if(inputData.columnIds == null || inputData.rowIds == null){
356            infoMessage = "Please select row and columndata."
357            return sendInfoMessage()
358        }
359               
360                // TODO: handle the case that we have multiple studies
361                def studyId = inputData.studyIds[ 0 ];
362                def study = Study.get( studyId as Integer );
363
364                // Find out what samples are involved
365                def samples = study.samples
366
367                // Retrieve the data for both axes for all samples
368                // TODO: handle the case of multiple fields on an axis
369                def fields = [ "x": inputData.columnIds[ 0 ], "y": inputData.rowIds[ 0 ] ];
370                def data = getAllFieldData( study, samples, fields );
371
372                // Group data based on the y-axis if categorical axis is selected
373
374        // Detecting if, on the y-axis, we have categories (multiple fields on an axis)
375        // For some reason, the class of the elements will be considered null incase of categories
376        // I'd rather have filtered on being an instance of Collection, or something along those lines
377        if(data['y'][0].class==null && data['y'][0]['category']!=null){
378            // We are dealing with multiple categories
379            def groupedData = []
380            def returnData = []
381
382            data['y'].each{
383                def tmp_GFdat = groupFieldData(['x': data['x'], 'y': it['category']['data']] );
384                groupedData << tmp_GFdat
385
386                // Format data so it can be rendered as JSON
387                def tmp_Fdat = formatData( tmp_GFdat, ['x': fields['x'], 'y': "a,"+it['category']['name']+",a,a"] );
388                //println "\tmp_Fdat: "+tmp_Fdat
389                returnData << tmp_Fdat
390            }
391
392            // Format data so it can be rendered as JSON
393            def returnDataWithMultipleCategories = formatCategoryData(returnData)
394            return sendResults(returnDataWithMultipleCategories)
395        } else {
396            // We are dealing with a single category
397            def groupedData = groupFieldData( data );
398   
399            // Format data so it can be rendered as JSON
400            def returnData = formatData( groupedData, fields );
401            return sendResults(returnData)
402        }
403        }
404
405        /**
406         * Parses the parameters given by the user into a proper list
407         * @return Map with 4 keys:
408         *              studyIds:       list with studyIds selected
409         *              rowIds:         list with fieldIds selected for the rows
410         *              columnIds:      list with fieldIds selected for the columns
411         *              visualizationType:      String with the type of visualization required
412         * @see getFields
413         * @see getVisualizationTypes
414         */
415        def parseGetDataParams() {
416                def studyIds, rowIds, columnIds, visualizationType;
417               
418                def inputData = params.get( 'data' );
419                try{
420                        def input_object = JSON.parse(inputData)
421                       
422                        studyIds = input_object.get('studies')*.id
423                        rowIds = input_object.get('rows')*.id
424                        columnIds = input_object.get('columns')*.id
425                        visualizationType = "barchart"
426                } catch(Exception e) {
427            /* TODO: Find a way to handle these kinds of exceptions without breaking the user interface.
428                Doing things in this way results in the user interface getting a 400.
429                        returnError(400, "An error occured while retrieving the user input")
430            infoMessage = "An error occured while retrieving the user input."
431                        log.error("VisualizationController: parseGetDataParams: "+e)
432            return sendInfoMessage()
433            */
434                }
435
436                return [ "studyIds" : studyIds, "rowIds": rowIds, "columnIds": columnIds, "visualizationType": visualizationType ];
437        }
438
439        /**
440         * Retrieve the field data for the selected fields
441         * @param study         Study for which the data should be retrieved
442         * @param samples       Samples for which the data should be retrieved
443         * @param fields        Map with key-value pairs determining the name and fieldId to retrieve data for. Example:
444         *                                              [ "x": "field-id-1", "y": "field-id-3" ]
445         * @return                      A map with the same keys as the input fields. The values in the map are lists of values of the
446         *                                      selected field for all samples. If a value could not be retrieved for a sample, null is returned. Example:
447         *                                              [ "x": [ 3, 6, null, 10 ], "y": [ "male", "male", "female", "female" ] ]
448         */
449        def getAllFieldData( study, samples, fields ) {
450                def fieldData = [:]
451                fields.each{ field ->
452                        fieldData[ field.key ] = getFieldData( study, samples, field.value );
453                }
454               
455                return fieldData;
456        }
457       
458        /**
459        * Retrieve the field data for the selected field
460        * @param study          Study for which the data should be retrieved
461        * @param samples        Samples for which the data should be retrieved
462        * @param fieldId        ID of the field to return data for
463        * @return                       A list of values of the selected field for all samples. If a value
464        *                                       could not be retrieved for a sample, null is returned. Examples:
465        *                                               [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ]
466        */
467        def getFieldData( study, samples, fieldId ) {
468                // Parse the fieldId as given by the user
469                def parsedField = parseFieldId( fieldId );
470               
471                def data = []
472               
473                if( parsedField.source == "GSCF" ) {
474                        // Retrieve data from GSCF itself
475                        def closure = valueCallback( parsedField.type )
476                       
477                        if( closure ) {
478                                samples.each { sample ->
479                                        // Retrieve the value for the selected field for this sample
480                                        def value = closure( sample, parsedField.name );
481                                       
482                                        if( value ) {
483                                                data << value;
484                                        } else {
485                                                // Return null if the value is not found
486                                                data << null
487                                        }
488                                }
489                        } else {
490                                // TODO: Handle error properly
491                                // Closure could not be retrieved, probably because the type is incorrect
492                                data = samples.collect { return null }
493                log.error("VisualizationController: getFieldData: Requested wrong field type: "+parsedField.type+". Parsed field: "+parsedField)
494                        }
495                } else {
496                        // Data must be retrieved from a module
497                        data = getModuleData( study, samples, parsedField.source, parsedField.name );
498                }
499               
500                return data
501        }
502       
503        /**
504         * Retrieve data for a given field from a data module
505         * @param study                 Study to retrieve data for
506         * @param samples               Samples to retrieve data for
507         * @param source_module Name of the module to retrieve data from
508         * @param fieldName             Name of the measurement type to retrieve (i.e. measurementToken)
509         * @return                              A list of values of the selected field for all samples. If a value
510         *                                              could not be retrieved for a sample, null is returned. Examples:
511         *                                                      [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ]
512         */
513        def getModuleData( study, samples, assay_id, fieldName ) {
514                def data = []
515                //println "assay_id: "+assay_id+", fieldName: "+fieldName
516                // TODO: Handle values that should be retrieved from multiple assays
517        // Check if this id starts with "compositeId", which indicates that the request concerns multiple assays from the same source module
518        if(assay_id.toString().startsWith("compositeId")){
519            //println "composite data request."
520            // This assay_id is a composite, meaning that the user is requesting data from multiple assays
521            //data = [:]
522            def ids = assay_id.toString().substring("compositeId".length()).split("@SEP@")
523            def names = fieldName.split("@SEP@")
524            ids.eachWithIndex { it, i ->
525                //println "data request decomposition:"
526                data << ['category': ['name': names[i], 'data': getModuleData(study, samples, it, names[i])]]
527            }
528        } else {
529            def assay = Assay.get(assay_id);
530
531            if( assay ) {
532                // Request for a particular assay and a particular feature
533                def urlVars = "assayToken=" + assay.assayUUID + "&measurementToken="+fieldName
534                urlVars += "&" + samples.collect { "sampleToken=" + it.sampleUUID }.join( "&" );
535
536                def callUrl
537                try {
538                    callUrl = assay.module.url + "/rest/getMeasurementData"
539                    def json = moduleCommunicationService.callModuleMethod( assay.module.url, callUrl, urlVars, "POST" );
540
541                    if( json ) {
542                        // First element contains sampletokens
543                        // Second element contains the featurename
544                        // Third element contains the measurement value
545                        def sampleTokens = json[ 0 ]
546                        def measurements = json[ 2 ]
547
548                        // Loop through the samples
549                        samples.each { sample ->
550                            // Search for this sampletoken
551                            def sampleToken = sample.sampleUUID;
552                            def index = sampleTokens.findIndexOf { it == sampleToken }
553
554                            if( index > -1 ) {
555                                data << measurements[ index ];
556                            } else {
557                                data << null
558                            }
559                        }
560                    } else {
561                        // TODO: handle error
562                        // Returns an empty list with as many elements as there are samples
563                        data = samples.collect { return null }
564                    }
565
566                } catch(Exception e){
567                    log.error("VisualizationController: getFields: "+e)
568                    return returnError(404, "An error occured while trying to collect data from a module. Most likely, this module is offline.")
569                }
570            } else {
571                // TODO: Handle error correctly
572                // Returns an empty list with as many elements as there are samples
573                data = samples.collect { return null }
574            }
575        }
576
577
578        //println "\t data request: "+data
579                return data
580        }
581
582        /**
583         * Group the field data on the values of the specified axis. For example, for a bar chart, the values
584         * on the x-axis should be grouped. Currently, the values for each group are averaged, and the standard
585         * error of the mean is returned in the 'error' property
586         * @param data          Data for both group- and value axis. The output of getAllFieldData fits this input
587         * @param groupAxis     Name of the axis to group on. Defaults to "x"
588         * @param valueAxis     Name of the axis where the values are. Defaults to "y"
589         * @param errorName     Key in the output map where 'error' values (SEM) are stored. Defaults to "error"
590         * @param unknownName   Name of the group for all null groups. Defaults to "unknown"
591         * @return                      A map with the keys 'groupAxis', 'valueAxis' and 'errorName'. The values in the map are lists of values of the
592         *                                      selected field for all groups. For example, if the input is
593         *                                              [ "x": [ "male", "male", "female", "female", null, "female" ], "y": [ 3, 6, null, 10, 4, 5 ] ]
594         *                                      the output will be:
595         *                                              [ "x": [ "male", "female", "unknown" ], "y": [ 4.5, 7.5, 4 ], "error": [ 1.5, 2.5, 0 ] ]
596         *
597         *                                      As you can see: null values in the valueAxis are ignored. Null values in the
598         *                                      group axis are combined into a 'unknown' category.
599         */
600        def groupFieldData( data, groupAxis = "x", valueAxis = "y", errorName = "error", unknownName = "unknown" ) {
601                // Create a unique list of values in the groupAxis. First flatten the list, since it might be that a
602                // sample belongs to multiple groups. In that case, the group names should not be the lists, but the list
603                // elements. A few lines below, this case is handled again by checking whether a specific sample belongs
604                // to this group.
605                // After flattening, the list is uniqued. The closure makes sure that values with different classes are
606                // always treated as different items (e.g. "" should not equal 0, but it does if using the default comparator)
607                def groups = data[ groupAxis ]
608                                                .flatten()
609                                                .unique { it == null ? "null" : it.class.name + it.toString() }
610                // Make sure the null category is last
611                groups = groups.findAll { it != null } + groups.findAll { it == null }
612                // Gather names for the groups. Most of the times, the group names are just the names, only with
613                // a null value, the unknownName must be used
614                def groupNames = groups.collect { it != null ? it : unknownName }
615                // Generate the output object
616                def outputData = [:]
617                outputData[ valueAxis ] = [];
618                outputData[ errorName ] = [];
619                outputData[ groupAxis ] = groupNames;
620               
621                // Loop through all groups, and gather the values for this group
622                groups.each { group ->
623                        // Find the indices of the samples that belong to this group. if a sample belongs to multiple groups (i.e. if
624                        // the samples groupAxis contains multiple values, is a collection), the value should be used in all groups.
625                        def indices= data[ groupAxis ].findIndexValues { it instanceof Collection ? it.contains( group ) : it == group };
626                        def values = data[ valueAxis ][ indices ]
627                       
628                        def dataForGroup = computeMeanAndError( values );
629                       
630                        outputData[ valueAxis ] << dataForGroup.value
631                        outputData[ errorName ] << dataForGroup.error 
632                }
633                return outputData
634        }
635       
636        /**
637         * Formats the grouped data in such a way that the clientside visualization method
638         * can handle the data correctly.
639         * @param groupedData   Data that has been grouped using the groupFields method
640         * @param fields                Map with key-value pairs determining the name and fieldId to retrieve data for. Example:
641         *                                                      [ "x": "field-id-1", "y": "field-id-3" ]
642         * @param groupAxis             Name of the axis to with group data. Defaults to "x"
643         * @param valueAxis             Name of the axis where the values are stored. Defaults to "y"
644         * @param errorName             Key in the output map where 'error' values (SEM) are stored. Defaults to "error"         *
645         * @return                              A map like the following:
646         *
647                        {
648                                "type": "barchart",
649                                "x": [ "Q1", "Q2", "Q3", "Q4" ],
650                                "xaxis": { "title": "quarter 2011", "unit": "" },
651                                "yaxis": { "title": "temperature", "unit": "degrees C" },
652                                "series": [
653                                        {
654                                                "name": "series name",
655                                                "y": [ 5.1, 3.1, 20.6, 15.4 ],
656                                                "error": [ 0.5, 0.2, 0.4, 0.5 ]
657                                        },
658                                ]
659                        }
660         *
661         */
662        def formatData( groupedData, fields, groupAxis = "x", valueAxis = "y", errorName = "error" ) {
663                // TODO: Handle name and unit of fields correctly
664               
665                def return_data = [:]
666                return_data[ "type" ] = "barchart"
667                return_data[ "x" ] = groupedData[ groupAxis ].collect { it.toString() }
668                return_data.put("yaxis", ["title" : parseFieldId( fields[ valueAxis ] ).name, "unit" : "" ])
669                return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": "" ])
670                return_data.put("series", [[
671                        "name": "Y",
672                        "y": groupedData[ valueAxis ],
673                        "error": groupedData[ errorName ]
674                ]])
675               
676                return return_data;
677        }
678
679        /**
680         * Returns a closure for the given entitytype that determines the value for a criterion
681         * on the given object. The closure receives two parameters: the sample and a field.
682         *
683         * For example:
684         *              How can one retrieve the value for subject.name, given a sample? This can be done by
685         *              returning the field values sample.parentSubject:
686         *                      { sample, field -> return getFieldValue( sample.parentSubject, field ) }
687         * @return      Closure that retrieves the value for a field and the given field
688         */
689        protected Closure valueCallback( String entity ) {
690                switch( entity ) {
691                        case "Study":
692                        case "studies":
693                                return { sample, field -> return getFieldValue( sample.parent, field ) }
694                        case "Subject":
695                        case "subjects":
696                                return { sample, field -> return getFieldValue( sample.parentSubject, field ); }
697                        case "Sample":
698                        case "samples":
699                                return { sample, field -> return getFieldValue( sample, field ) }
700                        case "Event":
701                        case "events":
702                                return { sample, field ->
703                                        if( !sample || !sample.parentEventGroup || !sample.parentEventGroup.events || sample.parentEventGroup.events.size() == 0 )
704                                                return null
705
706                                        return sample.parentEventGroup.events?.collect { getFieldValue( it, field ) };
707                                }
708                        case "SamplingEvent":
709                        case "samplingEvents":
710                                return { sample, field -> return getFieldValue( sample.parentEvent, field ); }
711                        case "Assay":
712                        case "assays":
713                                return { sample, field ->
714                                        def sampleAssays = Assay.findByParent( sample.parent ).findAll { it.samples?.contains( sample ) };
715                                        if( sampleAssays && sampleAssays.size() > 0 )
716                                                return sampleAssays.collect { getFieldValue( it, field ) }
717                                        else
718                                                return null
719                                }
720                }
721        }
722       
723        /**
724         * Computes the mean value and Standard Error of the mean (SEM) for the given values
725         * @param values        List of values to compute the mean and SEM for. Strings and null
726         *                                      values are ignored
727         * @return                      Map with two keys: 'value' and 'error'
728         */
729        protected Map computeMeanAndError( values ) {
730                // TODO: Handle the case that one of the values is a list. In that case,
731                // all values should be taken into account.     
732                def mean = computeMean( values );
733                def error = computeSEM( values, mean );
734               
735                return [ 
736                        "value": mean,
737                        "error": error
738                ]
739        }
740       
741        /**
742         * Computes the mean of the given values. Values that can not be parsed to a number
743         * are ignored. If no values are given, the mean of 0 is returned.
744         * @param values        List of values to compute the mean for
745         * @return                      Arithmetic mean of the values
746         */
747        protected def computeMean( List values ) {
748                def sumOfValues = 0;
749                def sizeOfValues = 0;
750                values.each { value ->
751                        def num = getNumericValue( value );
752                        if( num != null ) {
753                                sumOfValues += num;
754                                sizeOfValues++
755                        } 
756                }
757
758                if( sizeOfValues > 0 )
759                        return sumOfValues / sizeOfValues;
760                else
761                        return 0;
762        }
763
764        /**
765        * Computes the standard error of mean of the given values. 
766        * Values that can not be parsed to a number are ignored. 
767        * If no values are given, the standard deviation of 0 is returned.
768        * @param values         List of values to compute the standard deviation for
769        * @param mean           Mean of the list (if already computed). If not given, the mean
770        *                                       will be computed using the computeMean method
771        * @return                       Standard error of the mean of the values or 0 if no values can be used.
772        */
773   protected def computeSEM( List values, def mean = null ) {
774           if( mean == null )
775                        mean = computeMean( values )
776           
777           def sumOfDifferences = 0;
778           def sizeOfValues = 0;
779           values.each { value ->
780                   def num = getNumericValue( value );
781                   if( num != null ) {
782                           sumOfDifferences += Math.pow( num - mean, 2 );
783                           sizeOfValues++
784                   }
785           }
786
787           if( sizeOfValues > 0 ) {
788                   def std = Math.sqrt( sumOfDifferences / sizeOfValues );
789                   return std / Math.sqrt( sizeOfValues );
790           } else {
791                   return 0;
792           }
793   }
794               
795        /**
796         * Return the numeric value of the given object, or null if no numeric value could be returned
797         * @param       value   Object to return the value for
798         * @return                      Number that represents the given value
799         */
800        protected Number getNumericValue( value ) {
801                // TODO: handle special types of values
802                if( value instanceof Number ) {
803                        return value;
804                } else if( value instanceof RelTime ) {
805                        return value.value;
806                }
807               
808                return null
809        }
810
811        /** 
812         * Returns a field for a given templateentity
813         * @param object        TemplateEntity (or subclass) to retrieve data for
814         * @param fieldName     Name of the field to return data for.
815         * @return                      Value of the field or null if the value could not be retrieved
816         */
817        protected def getFieldValue( TemplateEntity object, String fieldName ) {
818                if( !object || !fieldName )
819                        return null;
820               
821                try {
822                        return object.getFieldValue( fieldName );
823                } catch( Exception e ) {
824                        return null;
825                }
826        }
827
828        /**
829         * Parses a fieldId that has been created earlier by createFieldId
830         * @param fieldId       FieldId to parse
831         * @return                      Map with attributes of the selected field. Keys are 'name', 'id', 'source' and 'type'
832         * @see createFieldId
833         */
834        protected Map parseFieldId( String fieldId ) {
835                def attrs = [:]
836               
837                def parts = fieldId.split(",")
838               
839                attrs = [
840                        "id": parts[ 0 ],
841                        "name": parts[ 1 ],
842                        "source": parts[ 2 ],
843                        "type": parts[ 3 ]
844                ]
845        }
846       
847        /**
848         * Create a fieldId based on the given attributes
849         * @param attrs         Map of attributes for this field. Keys may be 'name', 'id', 'source' and 'type'
850         * @return                      Unique field ID for these parameters
851         * @see parseFieldId
852         */
853        protected String createFieldId( Map attrs ) {
854                // TODO: What if one of the attributes contains a comma?
855                def name = attrs.name;
856                def id = attrs.id ?: name;
857                def source = attrs.source;
858                def type = attrs.type ?: ""
859               
860                return id + "," + name + "," + source + "," + type;
861        }
862
863    protected void returnError(code, msg){
864        response.sendError(code , msg)
865    }
866
867    protected int determineFieldType(studyId, fieldId){
868        // Parse the fieldId as given by the user
869                def parsedField = parseFieldId( fieldId );
870
871        def study = Study.get(studyId)
872
873                def data = []
874
875                if( parsedField.source == "GSCF" ) {
876            if(parsedField.id.isNumber()){
877                // Templatefield
878                // ask for tf by id, ask for .type
879                try{
880                    TemplateField tf = TemplateField.get(parsedField.id)
881                    if(tf.type=="DOUBLE" || tf.type=="LONG" || tf.type=="DATE" || tf.type=="RELTIME"){
882                        return numericalData
883                    } else {
884                        return categoricalData
885                    }
886                } catch(Exception e){
887                    log.error("VisualizationController: determineFieldType: "+e)
888                    // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
889                    return categoricalData
890                }
891            } else {
892                // Domainfield or memberclass
893                try{
894                    switch( parsedField.type ) {
895                        case "Study":
896                        case "studies":
897                            return determineCategoryFromClass(Study[parsedField.name].class)
898                            break
899                        case "Subject":
900                        case "subjects":
901                            return determineCategoryFromClass(Subject[parsedField.name].class)
902                            break
903                        case "Sample":
904                        case "samples":
905                            return determineCategoryFromClass(Sample[parsedField.name].class)
906                            break
907                        case "Event":
908                        case "events":
909                            return determineCategoryFromClass(Event[parsedField.name].class)
910                            break
911                        case "SamplingEvent":
912                        case "samplingEvents":
913                            return determineCategoryFromClass(SamplingEvent[parsedField.name].class)
914                            break
915                        case "Assay":
916                        case "assays":
917                            return determineCategoryFromClass(Assay[parsedField.name].class)
918                            break
919                    }
920                } catch(Exception e){
921                    log.error("VisualizationController: determineFieldType: "+e)
922                    // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
923                    return categoricalData
924                }
925            }
926
927            // Check parsedField.id == number
928                } else {
929            data = getModuleData( study, study.getSamples(), parsedField.source, parsedField.name );
930            def cat = determineCategoryFromData(data)
931            return cat
932                }
933    }
934
935    protected int determineCategoryFromClass(inputObject){
936        if(inputObject==java.lang.String){
937            return categoricalData
938        } else {
939            return numericalData
940        }
941    }
942
943    protected int determineCategoryFromData(inputObject){
944        def results = []
945        if(inputObject instanceof Collection){
946            // This data is more complex than a single value, so we will call ourselves again so we c
947            inputObject.each {
948                results << determineCategoryFromData(it)
949            }
950        } else {
951            if(inputObject.toString().isDouble()){
952                results << numericalData
953            } else {
954                results << categoricalData
955            }
956        }
957
958        results.unique()
959
960        if(results.size()>1){
961            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
962            results[0] = categoricalData
963        }
964
965        return results[0]
966    }
967
968    protected void sendResults(returnData){
969        def results = [:]
970        if(infoMessage!=""){
971            results.put("infoMessage", infoMessage)
972            infoMessage = ""
973        }
974        results.put("returnData", returnData)
975        render results as JSON
976    }
977
978    protected void sendInfoMessage(){
979        def results = [:]
980        results.put("infoMessage", infoMessage)
981        infoMessage = ""
982        render results as JSON
983    }
984
985    /*
986        Combine several blocks of formatted data into one
987     */
988    protected def formatCategoryData(inputData){
989        def series = []
990        inputData.eachWithIndex { it, i ->
991            series << ['name': it['yaxis']['title'], 'y': it['series']['y'][0], 'error': it['series']['error'][0]]
992        }
993        def ret = [:]
994        ret.put('type', inputData[0]['type'])
995        ret.put('x', inputData[0]['x'])
996        ret.put('yaxis',['title': 'title', 'unit': ''])
997        ret.put('xaxis', inputData[0]['xaxis'])
998        ret.put('series', series)
999        return ret
1000    }
1001}
Note: See TracBrowser for help on using the repository browser.