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

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

visualization/VisualizeController.groovy, Fields on the 'groupAxis' that according to their fieldId contain a 'startTime', 'endTime' or 'duration' are now rendered using the 'RelTime?' class's 'toPrettyString' function.

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