root/trunk/grails-app/controllers/dbnp/visualization/VisualizeController.groovy @ 2065

Revision 2065, 56.8 KB (checked in by tjeerd@…, 3 years ago)

VIS-42, changed the way the valueAxis is sorted. Also made the padding in the graphs larger

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.toString(), 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                    data << value;
423                                }
424                        } else {
425                                // TODO: Handle error properly
426                                // Closure could not be retrieved, probably because the type is incorrect
427                                data = samples.collect { return null }
428                log.error("VisualizationController: getFieldData: Requested wrong field type: "+parsedField.type+". Parsed field: "+parsedField)
429                        }
430                } else {
431                        // Data must be retrieved from a module
432                        data = getModuleData( study, samples, parsedField.source, parsedField.name );
433                }
434               
435                return data
436        }
437       
438        /**
439         * Retrieve data for a given field from a data module
440         * @param study                 Study to retrieve data for
441         * @param samples               Samples to retrieve data for
442         * @param source_module Name of the module to retrieve data from
443         * @param fieldName             Name of the measurement type to retrieve (i.e. measurementToken)
444         * @return                              A list of values of the selected field for all samples. If a value
445         *                                              could not be retrieved for a sample, null is returned. Examples:
446         *                                                      [ 3, 6, null, 10 ] or [ "male", "male", "female", "female" ]
447         */
448        def getModuleData( study, samples, assay_id, fieldName ) {
449                def data = []
450               
451                // TODO: Handle values that should be retrieved from multiple assays
452        def assay = Assay.get(assay_id);
453
454        if( assay ) {
455            // Request for a particular assay and a particular feature
456            def urlVars = "assayToken=" + assay.assayUUID + "&measurementToken="+fieldName
457            urlVars += "&" + samples.collect { "sampleToken=" + it.sampleUUID }.join( "&" );
458
459            def callUrl
460            try {
461                callUrl = assay.module.url + "/rest/getMeasurementData"
462                def json = moduleCommunicationService.callModuleMethod( assay.module.url, callUrl, urlVars, "POST" );
463
464                if( json ) {
465                    // First element contains sampletokens
466                    // Second element contains the featurename
467                    // Third element contains the measurement value
468                    def sampleTokens = json[ 0 ]
469                    def measurements = json[ 2 ]
470
471                    // Loop through the samples
472                    samples.each { sample ->
473                        // Search for this sampletoken
474                        def sampleToken = sample.sampleUUID;
475                        def index = sampleTokens.findIndexOf { it == sampleToken }
476
477                        if( index > -1 ) {
478                            data << measurements[ index ];
479                        } else {
480                            data << null
481                        }
482                    }
483                } else {
484                    // TODO: handle error
485                    // Returns an empty list with as many elements as there are samples
486                    data = samples.collect { return null }
487                }
488
489            } catch(Exception e){
490                log.error("VisualizationController: getFields: "+e)
491                //return returnError(404, "An error occured while trying to collect data from a module. Most likely, this module is offline.")
492                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.")
493            }
494        } else {
495            // TODO: Handle error correctly
496            // Returns an empty list with as many elements as there are samples
497            data = samples.collect { return null }
498        }
499
500        //println "\t data request: "+data
501                return data
502        }
503
504        /**
505         * Group the field data on the values of the specified axis. For example, for a bar chart, the values
506         * on the x-axis should be grouped. Currently, the values for each group are averaged, and the standard
507         * error of the mean is returned in the 'error' property
508     * @param visualizationType Some types require a different formatting/grouping of the data, such as 'table'
509         * @param data          Data for both group- and value axis. The output of getAllFieldData fits this input
510         * @param groupAxis     Name of the axis to group on. Defaults to "x"
511         * @param valueAxis     Name of the axis where the values are. Defaults to "y"
512         * @param errorName     Key in the output map where 'error' values (SEM) are stored. Defaults to "error"
513         * @param unknownName   Name of the group for all null groups. Defaults to "unknown"
514         * @return                      A map with the keys 'groupAxis', 'valueAxis' and 'errorName'. The values in the map are lists of values of the
515         *                                      selected field for all groups. For example, if the input is
516         *                                              [ "x": [ "male", "male", "female", "female", null, "female" ], "y": [ 3, 6, null, 10, 4, 5 ] ]
517         *                                      the output will be:
518         *                                              [ "x": [ "male", "female", "unknown" ], "y": [ 4.5, 7.5, 4 ], "error": [ 1.5, 2.5, 0 ] ]
519         *
520         *                                      As you can see: null values in the valueAxis are ignored. Null values in the
521         *                                      group axis are combined into a 'unknown' category.
522         */
523        def groupFieldData( visualizationType, data, groupAxis = "x", valueAxis = "y", errorName = "error", unknownName = "unknown" ) {
524                // TODO: Give the user the possibility to change this value in the user interface
525                def showEmptyCategories = false;
526               
527                // Create a unique list of values in the groupAxis. First flatten the list, since it might be that a
528                // sample belongs to multiple groups. In that case, the group names should not be the lists, but the list
529                // elements. A few lines below, this case is handled again by checking whether a specific sample belongs
530                // to this group.
531                // After flattening, the list is uniqued. The closure makes sure that values with different classes are
532                // always treated as different items (e.g. "" should not equal 0, but it does if using the default comparator)
533                def groups = data[ groupAxis ]
534                                                .flatten()
535                                                .unique { it == null ? "null" : it.class.name + it.toString() }
536                                               
537                // Make sure the null category is last
538                groups = groups.findAll { it != null } + groups.findAll { it == null }
539               
540                // Generate the output object
541                def outputData = [:]
542                outputData[ valueAxis ] = [];
543                outputData[ errorName ] = [];
544                outputData[ groupAxis ] = [];
545               
546                // Loop through all groups, and gather the values for this group
547        // A visualization of type 'table' is a special case. There, the counts of two combinations of 'groupAxis'
548                // and 'valueAxis' items are computed
549        if( visualizationType=='table' ){
550            // For each 'valueAxis' item and 'groupAxis' item combination, count how often they appear together.
551            def counts = [:]
552                       
553            // The 'counts' list uses keys like this: ['item1':group, 'item2':value]
554            // The value will be an integer (the count)
555            data[ groupAxis ].eachWithIndex { group, index ->
556                def value =  data[ valueAxis ][index]
557                if(!counts.get(['item1':group, 'item2':value])){
558                    counts[['item1':group, 'item2':value]] = 1
559                } else {
560                    counts[['item1':group, 'item2':value]] = counts[['item1':group, 'item2':value]] + 1
561                }
562            }
563           
564                        def valueData =  data[ valueAxis ]
565                                        .flatten()
566                                        .unique { it == null ? "null" : it.class.name + it.toString() }
567                                                                                                       
568                        // Now we will first check whether any of the categories is empty. If some of the rows
569                        // or columns are empty, don't include them in the output
570                        if( !showEmptyCategories ) {
571                                groups.eachWithIndex { group, index ->
572                                        if( counts.findAll { it.key.item1 == group } )
573                                                outputData[groupAxis] << group
574                                }
575                               
576                                valueData.each { value ->
577                                        if( counts.findAll { it.key.item2 == value } )
578                                                outputData[valueAxis] << value
579                                }
580                        } else {
581                                outputData[groupAxis] = groups.collect { it != null ? it : unknownName }
582                                ouputData[valueAxis] = valueData
583                        }
584                                                                                                                               
585            // Because we are collecting counts, we do not set the 'errorName' item of the 'outputData' map.
586            // We do however set the 'data' map to contain the counts. We set it in such a manner that it has
587                        // a 'table' layout with respect to 'groupAxis' and 'valueAxis'.
588            def rows = []
589            outputData[groupAxis].each{ group ->
590                def row = []
591                outputData[valueAxis].each{ value ->
592                    row << counts[['item1':group, 'item2':value]]
593                }
594                while(row.contains(null)){
595                    row[row.indexOf(null)] = 0
596                } // '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.
597                if(row!=[]) rows << row
598            }
599            outputData['data']= rows
600                       
601                        // Convert groups to group names
602                        outputData[ groupAxis ] = outputData[ groupAxis ].collect { it != null ? it : unknownName }
603        } else {
604            groups.each { group ->
605                // Find the indices of the samples that belong to this group. if a sample belongs to multiple groups (i.e. if
606                // the samples groupAxis contains multiple values, is a collection), the value should be used in all groups.
607                def indices = data[ groupAxis ].findIndexValues { it instanceof Collection ? it.contains( group ) : it == group };
608                def values  = data[ valueAxis ][ indices ]
609
610                                // The computation for mean and error will return null if no (numerical) values are found
611                                // In that case, the user won't see this category
612                def dataForGroup = null;
613                switch( params.get( 'aggregation') ) {
614                                case "average":
615                        dataForGroup = computeMeanAndError( values );
616                        break;
617                    case "count":
618                        dataForGroup = computeCount( values );
619                        break;
620                    case "median":
621                        dataForGroup = computeMedian( values );
622                        break;
623                    case "none":
624                        // Currently disabled, create another function
625                        dataForGroup = computeMeanAndError( values );
626                        break;
627                    case "sum":
628                        dataForGroup = computeSum( values );
629                        break;
630                    default:
631                        // Default is "average"
632                        dataForGroup = computeMeanAndError( values );
633                }
634
635
636                                if( showEmptyCategories || dataForGroup.value != null ) {
637                                        // Gather names for the groups. Most of the times, the group names are just the names, only with
638                                        // a null value, the unknownName must be used
639                                        outputData[ groupAxis ] << ( group != null ? group : unknownName )
640                        outputData[ valueAxis ] << dataForGroup.value ?: 0
641                        outputData[ errorName ] << dataForGroup.error ?: 0
642                                }
643            }
644        }
645               
646                return outputData
647        }
648       
649        /**
650         * Formats the grouped data in such a way that the clientside visualization method
651         * can handle the data correctly.
652         * @param groupedData   Data that has been grouped using the groupFields method
653         * @param fields                Map with key-value pairs determining the name and fieldId to retrieve data for. Example:
654         *                                                      [ "x": "field-id-1", "y": "field-id-3" ]
655     * @param groupAxisType Integer, either CATEGORICAL or NUMERIACAL
656     * @param valueAxisType Integer, either CATEGORICAL or NUMERIACAL
657         * @param groupAxis             Name of the axis to with group data. Defaults to "x"
658         * @param valueAxis             Name of the axis where the values are stored. Defaults to "y"
659         * @param errorName             Key in the output map where 'error' values (SEM) are stored. Defaults to "error"         *
660         * @return                              A map like the following:
661         *
662                        {
663                                "type": "barchart",
664                                "xaxis": { "title": "quarter 2011", "unit": "" },
665                                "yaxis": { "title": "temperature", "unit": "degrees C" },
666                                "series": [
667                                        {
668                                                "name": "series name",
669                                                "y": [ 5.1, 3.1, 20.6, 15.4 ],
670                        "x": [ "Q1", "Q2", "Q3", "Q4" ],
671                                                "error": [ 0.5, 0.2, 0.4, 0.5 ]
672                                        },
673                                ]
674                        }
675         *
676         */
677        def formatData( type, groupedData, fields, groupAxisType, valueAxisType, groupAxis = "x", valueAxis = "y", errorName = "error" ) {
678                // We want to sort the data based on the group-axis, but keep the values on the value-axis in sync.
679                // The only way seems to be to combine data from both axes.
680        def combined = []
681        if(type=="table"){
682            groupedData[ groupAxis ].eachWithIndex { group, i ->
683                combined << [ "group": group, "data": groupedData[ 'data' ][ i ] ]
684            }
685            combined.sort { it.group.toString() }
686            groupedData[groupAxis] = renderTimesAndDatesHumanReadable(combined*.group, groupAxisType)
687            groupedData[valueAxis] = renderTimesAndDatesHumanReadable(groupedData[valueAxis], valueAxisType)
688            groupedData["data"] = combined*.data
689        } else {
690            groupedData[ groupAxis ].eachWithIndex { group, i ->
691                combined << [ "group": (groupAxisType==CATEGORICALDATA ? group.toString() : group), "value": groupedData[ valueAxis ][ i ] ]
692            }
693            combined.sort { it.group }
694            groupedData[groupAxis] = renderTimesAndDatesHumanReadable(combined*.group, groupAxisType)
695            groupedData[valueAxis] = renderTimesAndDatesHumanReadable(combined*.value, valueAxisType)
696        }
697        // TODO: Handle name and unit of fields correctly
698        def valueAxisTypeString = (valueAxisType==CATEGORICALDATA || valueAxisType==DATE || valueAxisType==RELTIME ? "categorical" : "numerical")
699        def groupAxisTypeString = (groupAxisType==CATEGORICALDATA || groupAxisType==DATE || groupAxisType==RELTIME ? "categorical" : "numerical")
700
701        if(type=="table"){
702            def return_data = [:]
703            return_data[ "type" ] = type
704            return_data.put("yaxis", ["title" : parseFieldId( fields[ valueAxis ] ).name, "unit" : "", "type":valueAxisTypeString ])
705            return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": "", "type":groupAxisTypeString ])
706            return_data.put("series", [[
707                    "x": groupedData[ groupAxis ].collect { it.toString() },
708                    "y": groupedData[ valueAxis ].collect { it.toString() },
709                    "data": groupedData["data"]
710            ]])
711            return return_data;
712        } else {
713            def xAxis = groupedData[ groupAxis ].collect { it.toString() };
714            def yName = parseFieldId( fields[ valueAxis ] ).name;
715
716            def return_data = [:]
717            return_data[ "type" ] = type
718            return_data.put("yaxis", ["title" : yName, "unit" : "", "type":valueAxisTypeString ])
719            return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": "", "type":groupAxisTypeString  ])
720            return_data.put("series", [[
721                "name": yName,
722                "x": xAxis,
723                "y": groupedData[ valueAxis ],
724                "error": groupedData[ errorName ]
725            ]])
726
727            return return_data;
728        }
729        }
730
731    /**
732     * 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.
733     * @param data  The list of items that needs to be checked/converted
734     * @param axisType As determined by determineFieldType
735     * @return The input variable 'data', with it's date and time elements converted.
736     * @see determineFieldType
737     */
738    def renderTimesAndDatesHumanReadable(data, axisType){
739        if(axisType==RELTIME){
740            data = renderTimesHumanReadable(data)
741        }
742        if(axisType==DATE){
743           data = renderDatesHumanReadable(data)
744        }
745        return data
746    }
747
748    /**
749     * Takes a one-dimensional list, returns the list with the appropriate items converted to a human readable string
750     * @param data
751     * @return
752     */
753    def renderTimesHumanReadable(data){
754        def tmpTimeContainer = []
755        data. each {
756            if(it instanceof Number) {
757                try{
758                    tmpTimeContainer << new RelTime( it ).toPrettyString()
759                } catch(IllegalArgumentException e){
760                    tmpTimeContainer << it
761                }
762            } else {
763                tmpTimeContainer << it // To handle items such as 'unknown'
764            }
765        }
766        return tmpTimeContainer
767    }
768
769    /**
770     * Takes a one-dimensional list, returns the list with the appropriate items converted to a human readable string
771     * @param data
772     * @return
773     */
774    def renderDatesHumanReadable(data) {
775        def tmpDateContainer = []
776        data. each {
777            if(it instanceof Number) {
778                try{
779                    tmpDateContainer << new java.util.Date( (Long) it ).toString()
780                } catch(IllegalArgumentException e){
781                    tmpDateContainer << it
782                }
783            } else {
784                tmpDateContainer << it // To handle items such as 'unknown'
785            }
786        }
787        return tmpDateContainer
788    }
789        /**
790         * Returns a closure for the given entitytype that determines the value for a criterion
791         * on the given object. The closure receives two parameters: the sample and a field.
792         *
793         * For example:
794         *              How can one retrieve the value for subject.name, given a sample? This can be done by
795         *              returning the field values sample.parentSubject:
796         *                      { sample, field -> return getFieldValue( sample.parentSubject, field ) }
797         * @return      Closure that retrieves the value for a field and the given field
798         */
799        protected Closure valueCallback( String entity ) {
800                switch( entity ) {
801                        case "Study":
802                        case "studies":
803                                return { sample, field -> return getFieldValue( sample.parent, field ) }
804                        case "Subject":
805                        case "subjects":
806                                return { sample, field -> return getFieldValue( sample.parentSubject, field ); }
807                        case "Sample":
808                        case "samples":
809                                return { sample, field -> return getFieldValue( sample, field ) }
810                        case "Event":
811                        case "events":
812                                return { sample, field ->
813                                        if( !sample || !sample.parentEventGroup || !sample.parentEventGroup.events || sample.parentEventGroup.events.size() == 0 )
814                                                return null
815
816                                        return sample.parentEventGroup.events?.collect { getFieldValue( it, field ) };
817                                }
818                        case "SamplingEvent":
819                        case "samplingEvents":
820                                return { sample, field -> return getFieldValue( sample.parentEvent, field ); }
821                        case "Assay":
822                        case "assays":
823                                return { sample, field ->
824                                        def sampleAssays = Assay.findByParent( sample.parent ).findAll { it.samples?.contains( sample ) };
825                                        if( sampleAssays && sampleAssays.size() > 0 )
826                                                return sampleAssays.collect { getFieldValue( it, field ) }
827                                        else
828                                                return null
829                                }
830                }
831        }
832       
833        /**
834        * Returns the domain object that should be used with the given entity string
835        *
836        * For example:
837        *               What object should be consulted if the user asks for "studies"
838        *               Response: Study
839        * @return       Domain object that should be used with the given entity string
840        */
841   protected def domainObjectCallback( String entity ) {
842           switch( entity ) {
843                   case "Study":
844                   case "studies":
845                           return Study
846                   case "Subject":
847                   case "subjects":
848                           return Subject
849                   case "Sample":
850                   case "samples":
851                           return Sample
852                   case "Event":
853                   case "events":
854                        return Event
855                   case "SamplingEvent":
856                   case "samplingEvents":
857                           return SamplingEvent
858                   case "Assay":
859                   case "assays":
860                                return Assay
861           }
862   }
863
864    /**
865    * Returns the objects within the given study that should be used with the given entity string
866    *
867    * For example:
868    *           What object should be consulted if the user asks for "samples"
869    *           Response: study.samples
870    * @return   List of domain objects that should be used with the given entity string
871    */
872    protected def templateObjectCallback( String entity, Study study ) {
873      switch( entity ) {
874          case "Study":
875          case "studies":
876              return study
877          case "Subject":
878          case "subjects":
879              return study?.subjects
880          case "Sample":
881          case "samples":
882              return study?.samples
883          case "Event":
884          case "events":
885               return study?.events
886          case "SamplingEvent":
887          case "samplingEvents":
888              return study?.samplingEvents
889          case "Assay":
890          case "assays":
891                  return study?.assays
892      }
893    }
894       
895        /**
896         * Computes the mean value and Standard Error of the mean (SEM) for the given values
897         * @param values        List of values to compute the mean and SEM for. Strings and null
898         *                                      values are ignored
899         * @return                      Map with two keys: 'value' and 'error'
900         */
901        protected Map computeMeanAndError( values ) {
902                // TODO: Handle the case that one of the values is a list. In that case,
903                // all values should be taken into account.     
904                def mean = computeMean( values );
905                def error = computeSEM( values, mean );
906               
907                return [
908                        "value": mean,
909                        "error": error
910                ]
911        }
912       
913        /**
914         * Computes the mean of the given values. Values that can not be parsed to a number
915         * are ignored. If no values are given, null is returned.
916         * @param values        List of values to compute the mean for
917         * @return                      Arithmetic mean of the values
918         */
919        protected def computeMean( List values ) {
920                def sumOfValues = 0;
921                def sizeOfValues = 0;
922                values.each { value ->
923                        def num = getNumericValue( value );
924                        if( num != null ) {
925                                sumOfValues += num;
926                                sizeOfValues++
927                        }
928                }
929
930                if( sizeOfValues > 0 )
931                        return sumOfValues / sizeOfValues;
932                else
933                        return null;
934        }
935
936        /**
937        * Computes the standard error of mean of the given values.
938        * Values that can not be parsed to a number are ignored.
939        * If no values are given, null is returned.
940        * @param values         List of values to compute the standard deviation for
941        * @param mean           Mean of the list (if already computed). If not given, the mean
942        *                                       will be computed using the computeMean method
943        * @return                       Standard error of the mean of the values or 0 if no values can be used.
944        */
945    protected def computeSEM( List values, def mean = null ) {
946       if( mean == null )
947            mean = computeMean( values )
948
949       def sumOfDifferences = 0;
950       def sizeOfValues = 0;
951       values.each { value ->
952           def num = getNumericValue( value );
953           if( num != null ) {
954               sumOfDifferences += Math.pow( num - mean, 2 );
955               sizeOfValues++
956           }
957       }
958
959       if( sizeOfValues > 0 ) {
960           def std = Math.sqrt( sumOfDifferences / sizeOfValues );
961           return std / Math.sqrt( sizeOfValues );
962       } else {
963           return null;
964       }
965    }
966
967    /**
968         * Computes the median of the given values. Values that can not be parsed to a number
969         * are ignored. If no values are given, null is returned.
970         * @param values        List of values to compute the median for
971         * @return                      Median of the values
972         */
973        protected def computeMedian( List values ) {
974                def listOfValues = [];
975                values.each { value ->
976                        def num = getNumericValue( value );
977                        if( num != null ) {
978                                listOfValues << num;
979                        }
980                }
981
982        listOfValues.sort();
983
984        def listSize = listOfValues.size();
985
986        def objReturn = null;
987
988                if( listSize > 0 ) {
989            def listHalf = (int) Math.abs(listSize/2);
990            if(listSize%2==0) {
991                // If the list is of an even size, take the mean of the middle two value's
992                objReturn = (listOfValues.get(listHalf)+listOfValues.get(listHalf-1))/2;
993            } else {
994                // If the list is of an odd size, take the middle value
995                objReturn = listOfValues.get(listHalf);
996            }
997        }
998
999                return ["value": objReturn];
1000        }
1001
1002    /**
1003         * Computes the count of the given values. Values that can not be parsed to a number
1004         * are ignored. If no values are given, null is returned.
1005         * @param values        List of values to compute the count for
1006         * @return                      Count of the values
1007         */
1008        protected def computeCount( List values ) {
1009                def sumOfValues = 0;
1010                def sizeOfValues = 0;
1011                values.each { value ->
1012                        def num = getNumericValue( value );
1013                        if( num != null ) {
1014                                sumOfValues += num;
1015                                sizeOfValues++
1016                        }
1017                }
1018
1019                if( sizeOfValues > 0 )
1020                        return ["value": sizeOfValues];
1021                else
1022                        return ["value": null];
1023        }
1024
1025    /**
1026         * Computes the sum of the given values. Values that can not be parsed to a number
1027         * are ignored. If no values are given, null is returned.
1028         * @param values        List of values to compute the sum for
1029         * @return                      Arithmetic sum of the values
1030         */
1031        protected def computeSum( List values ) {
1032                def sumOfValues = 0;
1033                def sizeOfValues = 0;
1034                values.each { value ->
1035                        def num = getNumericValue( value );
1036                        if( num != null ) {
1037                                sumOfValues += num;
1038                                sizeOfValues++
1039                        }
1040                }
1041
1042                if( sizeOfValues > 0 )
1043                        return ["value": sumOfValues];
1044                else
1045                        return ["value": null];
1046        }
1047   
1048        /**
1049         * Return the numeric value of the given object, or null if no numeric value could be returned
1050         * @param       value   Object to return the value for
1051         * @return                      Number that represents the given value
1052         */
1053        protected Number getNumericValue( value ) {
1054                // TODO: handle special types of values
1055                if( value instanceof Number ) {
1056                        return value;
1057                } else if( value instanceof RelTime ) {
1058                        return value.value;
1059                }
1060               
1061                return null
1062        }
1063
1064        /**
1065         * Returns a field for a given templateentity
1066         * @param object        TemplateEntity (or subclass) to retrieve data for
1067         * @param fieldName     Name of the field to return data for.
1068         * @return                      Value of the field or null if the value could not be retrieved
1069         */
1070        protected def getFieldValue( TemplateEntity object, String fieldName ) {
1071                if( !object || !fieldName )
1072                        return null;
1073               
1074                try {
1075                        return object.getFieldValue( fieldName );
1076                } catch( Exception e ) {
1077                        return null;
1078                }
1079        }
1080
1081        /**
1082         * Parses a fieldId that has been created earlier by createFieldId
1083         * @param fieldId       FieldId to parse
1084         * @return                      Map with attributes of the selected field. Keys are 'name', 'id', 'source' and 'type'
1085         * @see createFieldId
1086         */
1087        protected Map parseFieldId( String fieldId ) {
1088                def attrs = [:]
1089               
1090                def parts = fieldId.split(",")
1091               
1092                attrs = [
1093                        "id": parts[ 0 ],
1094                        "name": parts[ 1 ],
1095                        "source": parts[ 2 ],
1096                        "type": parts[ 3 ]
1097                ]
1098        }
1099       
1100        /**
1101         * Create a fieldId based on the given attributes
1102         * @param attrs         Map of attributes for this field. Keys may be 'name', 'id', 'source' and 'type'
1103         * @return                      Unique field ID for these parameters
1104         * @see parseFieldId
1105         */
1106        protected String createFieldId( Map attrs ) {
1107                // TODO: What if one of the attributes contains a comma?
1108                def name = attrs.name;
1109                def id = attrs.id ?: name;
1110                def source = attrs.source;
1111                def type = attrs.type ?: ""
1112               
1113                return id + "," + name + "," + source + "," + type;
1114        }
1115
1116    /**
1117     * Set the response code and an error message
1118     * @param code HTTP status code
1119     * @param msg Error message, string
1120     */
1121    protected void returnError(code, msg){
1122        response.sendError(code , msg)
1123    }
1124
1125    /**
1126     * Determines what type of data a field contains
1127     * @param studyId An id that can be used with Study.get/1 to retrieve a study from the database
1128     * @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
1129     * @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
1130     * @return Either CATEGORICALDATA, NUMERICALDATA, DATE or RELTIME
1131     */
1132    protected int determineFieldType(studyId, fieldId, inputData = null){
1133                def parsedField = parseFieldId( fieldId );
1134        def study = Study.get(studyId)
1135                def data = []
1136
1137        try{
1138            if( parsedField.source == "GSCF" ) {
1139                if(parsedField.id.isNumber()){
1140                        return determineCategoryFromTemplateFieldId(parsedField.id)
1141                } else { // Domainfield or memberclass
1142                    def callback = domainObjectCallback( parsedField.type )
1143
1144                    // 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
1145                    if(callback?.giveDomainFields().name.contains(parsedField.name.toString())){
1146                        // Use the associated templateField to determine the field type
1147                        return determineCategoryFromTemplateField(
1148                                callback?.giveDomainFields()[
1149                                    callback?.giveDomainFields().name.indexOf(parsedField.name.toString())
1150                                ]
1151                        )
1152                    }
1153                    // Apparently it is not a templatefield as well as a memberclass
1154
1155                    def field = callback?.declaredFields.find { it.name == parsedField.name };
1156                    if( field ) {
1157                        return determineCategoryFromClass( field.getType() )
1158                    } else {
1159                        // TODO: how do we communicate this to the user? Do we allow the process to proceed?
1160                        log.error( "The user asked for field " + parsedField.type + " - " + parsedField.name + ", but it doesn't exist." );
1161                    }
1162                }
1163            } else {
1164                if(inputData == null){ // If we did not get data, we need to request it from the module first
1165                    data = getModuleData( study, study.getSamples(), parsedField.source, parsedField.name );
1166                    return determineCategoryFromData(data)
1167                } else {
1168                    return determineCategoryFromData(inputData)
1169                }
1170            }
1171        } catch(Exception e){
1172            log.error("VisualizationController: determineFieldType: "+e)
1173            e.printStackTrace()
1174            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
1175            return CATEGORICALDATA
1176        }
1177    }
1178
1179    /**
1180     * Determines a field category, based on the input parameter 'classObject', which is an instance of type 'class'
1181     * @param classObject
1182     * @return Either CATEGORICALDATA of NUMERICALDATA
1183     */
1184    protected int determineCategoryFromClass(classObject){
1185        log.trace "Determine category from class: "+classObject+", of class: "+classObject?.class
1186        if(classObject==java.lang.String){
1187            return CATEGORICALDATA
1188        } else {
1189            return NUMERICALDATA
1190        }
1191    }
1192
1193    /**
1194     * 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.
1195     * @param inputObject Either a single item, or a collection of items
1196     * @return Either CATEGORICALDATA of NUMERICALDATA
1197     */
1198    protected int determineCategoryFromData(inputObject){
1199        def results = []
1200               
1201        if(inputObject instanceof Collection){
1202            // This data is more complex than a single value, so we will call ourselves again so we c
1203            inputObject.each {
1204                                if( it != null )
1205                        results << determineCategoryFromData(it)
1206            }
1207        } else {
1208                        // Unfortunately, the JSON null object doesn't resolve to false or equals null. For that reason, we
1209                        // exclude those objects explicitly here.
1210                        if( inputObject != null && inputObject?.class != org.codehaus.groovy.grails.web.json.JSONObject$Null ) {
1211                    if(inputObject.toString().isDouble()){
1212                        results << NUMERICALDATA
1213                    } else {
1214                        results << CATEGORICALDATA
1215                    }
1216                        }
1217        }
1218
1219        results.unique()
1220
1221        if(results.size() > 1) {
1222            // If we cannot figure out what kind of a datatype a piece of data is, we treat it as categorical data
1223            results[0] = CATEGORICALDATA
1224        } else if( results.size() == 0 ) {
1225                        // If the list is empty, return the numerical type. If it is the only value, if will
1226                        // be discarded later on. If there are more entries (e.g part of a collection)
1227                        // the values will be regarded as numerical, if the other values are numerical 
1228                        results[ 0 ] = NUMERICALDATA
1229        }
1230
1231                return results[0]
1232    }
1233
1234    /**
1235     * Determines a field category, based on the TemplateFieldId of a Templatefield
1236     * @param id A database ID for a TemplateField
1237     * @return Either CATEGORICALDATA of NUMERICALDATA
1238     */
1239    protected int determineCategoryFromTemplateFieldId(id){
1240        TemplateField tf = TemplateField.get(id)
1241        return determineCategoryFromTemplateField(tf)
1242    }
1243
1244    /**
1245     * Determines a field category, based on the TemplateFieldType of a Templatefield
1246     * @param id A database ID for a TemplateField
1247     * @return Either CATEGORICALDATA of NUMERICALDATA
1248     */
1249    protected int determineCategoryFromTemplateField(tf){
1250        if(tf.type==TemplateFieldType.DOUBLE || tf.type==TemplateFieldType.LONG){
1251            log.trace "GSCF templatefield: NUMERICALDATA ("+NUMERICALDATA+") (based on "+tf.type+")"
1252            return NUMERICALDATA
1253        }
1254        if(tf.type==TemplateFieldType.DATE){
1255            log.trace "GSCF templatefield: DATE ("+DATE+") (based on "+tf.type+")"
1256            return DATE
1257        }
1258        if(tf.type==TemplateFieldType.RELTIME){
1259            log.trace "GSCF templatefield: RELTIME ("+RELTIME+") (based on "+tf.type+")"
1260            return RELTIME
1261        }
1262        log.trace "GSCF templatefield: CATEGORICALDATA ("+CATEGORICALDATA+") (based on "+tf.type+")"
1263        return CATEGORICALDATA
1264    }
1265    /**
1266     * 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.
1267     * @param returnData The object containing the data
1268     * @return results A JSON object
1269     */
1270    protected void sendResults(returnData){
1271        def results = [:]
1272        if(infoMessage.size()!=0){
1273            results.put("infoMessage", infoMessage)
1274            infoMessage = []
1275        }
1276        results.put("returnData", returnData)
1277        render results as JSON
1278    }
1279
1280    /**
1281     * Properly formats an informational message that will be returned to the client. Resets the informational message to the empty String.
1282     * @param returnData The object containing the data
1283     * @return results A JSON object
1284     */
1285    protected void sendInfoMessage(){
1286        def results = [:]
1287        results.put("infoMessage", infoMessage)
1288        infoMessage = []
1289        render results as JSON
1290    }
1291
1292    /**
1293     * Adds a new message to the infoMessage
1294     * @param message The information that needs to be added to the infoMessage
1295     */
1296    protected void setInfoMessage(message){
1297        infoMessage.add(message)
1298        log.trace "setInfoMessage: "+infoMessage
1299    }
1300
1301    /**
1302     * Adds a message to the infoMessage that gives the client information about offline modules
1303     */
1304    protected void setInfoMessageOfflineModules(){
1305        infoMessageOfflineModules.unique()
1306        if(infoMessageOfflineModules.size()>0){
1307            String message = "Unfortunately"
1308            infoMessageOfflineModules.eachWithIndex{ it, index ->
1309                if(index==(infoMessageOfflineModules.size()-2)){
1310                    message += ', the '+it+' and '
1311                } else {
1312                    if(index==(infoMessageOfflineModules.size()-1)){
1313                        message += ' the '+it
1314                    } else {
1315                        message += ', the '+it
1316                    }
1317                }
1318            }
1319            message += " could not be reached. As a result, we cannot at this time visualize data contained in "
1320            if(infoMessageOfflineModules.size()>1){
1321                message += "these modules."
1322            } else {
1323                message += "this module."
1324            }
1325            setInfoMessage(message)
1326        }
1327        infoMessageOfflineModules = []
1328    }
1329
1330    /**
1331     * Combine several blocks of formatted data into one. These blocks have been formatted by the formatData function.
1332     * @param inputData Contains a list of maps, of the following format
1333     *          - a key 'series' containing a list, that contains one or more maps, which contain the following:
1334     *            - a key 'name', containing, for example, a feature name or field name
1335     *            - a key 'y', containing a list of y-values
1336     *            - a key 'error', containing a list of, for example, standard deviation or standard error of the mean values,
1337     */
1338    protected def formatCategoryData(inputData){
1339        // NOTE: This function is no longer up to date with the current inputData layout.
1340        def series = []
1341        inputData.eachWithIndex { it, i ->
1342            series << ['name': it['yaxis']['title'], 'y': it['series']['y'][0], 'error': it['series']['error'][0]]
1343        }
1344        def ret = [:]
1345        ret.put('type', inputData[0]['type'])
1346        ret.put('x', inputData[0]['x'])
1347        ret.put('yaxis',['title': 'title', 'unit': ''])
1348        ret.put('xaxis', inputData[0]['xaxis'])
1349        ret.put('series', series)
1350        return ret
1351    }
1352
1353    /**
1354     * Given two objects of either CATEGORICALDATA or NUMERICALDATA
1355     * @param rowType The type of the data that has been selected for the row, either CATEGORICALDATA or NUMERICALDATA
1356     * @param columnType The type of the data that has been selected for the column, either CATEGORICALDATA or NUMERICALDATA
1357     * @return
1358     */
1359    protected def determineVisualizationTypes(rowType, columnType){
1360         def types = []
1361        if(rowType==CATEGORICALDATA || DATE || RELTIME){
1362            if(columnType==CATEGORICALDATA || DATE || RELTIME){
1363                types = [ [ "id": "table", "name": "Table"] ];
1364            }
1365            if(columnType==NUMERICALDATA){
1366                types = [ [ "id": "horizontal_barchart", "name": "Horizontal barchart"] ];
1367            }
1368        }
1369        if(rowType==NUMERICALDATA){
1370            if(columnType==CATEGORICALDATA || DATE || RELTIME){
1371                types = [ [ "id": "barchart", "name": "Barchart"], [ "id": "linechart", "name": "Linechart"] ];
1372            }
1373            if(columnType==NUMERICALDATA){
1374                types = [ [ "id": "scatterplot", "name": "Scatterplot"], [ "id": "linechart", "name": "Linechart"] ];
1375            }
1376        }
1377        return types
1378    }
1379}
Note: See TracBrowser for help on using the browser.