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

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

visualization/VisualizeController.groovy, 'getData' function. If the user is requesting data that concerns only subjects, then make sure those subjects appear only once

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