Changeset 2101

Show
Ignore:
Timestamp:
11-11-11 15:57:50 (2 years ago)
Author:
robert@…
Message:

Changed the visualization controller to be able to cope with series in charts. See VIS-32 for more information

Location:
trunk/grails-app
Files:
2 modified

Legend:

Unmodified
Added
Removed
  • trunk/grails-app/controllers/dbnp/visualization/VisualizeController.groovy

    r2099 r2101  
    168168        def columnType = determineFieldType(inputData.studyIds[0], inputData.columnIds[0]) 
    169169 
    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]]) 
     170                log.trace "Determining groupType: "+inputData.groupIds[0] 
     171                def groupType = determineFieldType(inputData.studyIds[0], inputData.groupIds[0]) 
     172                 
     173                        // Determine possible visualization- and aggregationtypes 
     174        def visualizationTypes = determineVisualizationTypes(rowType, columnType) 
     175                def aggregationTypes = determineAggregationTypes(rowType, columnType, groupType) 
     176                 
     177        log.trace  "visualization types: " + visualizationTypes + ", determined this based on "+rowType+" and "+columnType 
     178                log.trace  "aggregation   types: " + aggregationTypes + ", determined this based on "+rowType+" and "+columnType + " and " + groupType 
     179                 
     180                def fieldData = [ 'x': parseFieldId( inputData.columnIds[ 0 ] ), 'y': parseFieldId( inputData.rowIds[ 0 ] ) ]; 
     181                 
     182        return sendResults([ 
     183                        'types': visualizationTypes, 
     184                        'aggregations': aggregationTypes, 
     185                         
     186                        // TODO: Remove these ids when the view has been updated. Use xaxis.id and yaxis.id instead 
     187                        'rowIds':inputData.rowIds[0], 
     188                        'columnIds':inputData.columnIds[0], 
     189                         
     190                        'xaxis': [  
     191                                'id': fieldData.x.fieldId, 
     192                                'name': fieldData.x.name, 
     193                                'unit': fieldData.x.unit, 
     194                                'type': dataTypeString( columnType ) 
     195                        ], 
     196                        'yaxis': [ 
     197                                'id': fieldData.y.fieldId, 
     198                                'name': fieldData.y.name, 
     199                                'unit': fieldData.y.unit, 
     200                                'type': dataTypeString( rowType ) 
     201                        ], 
     202 
     203                ]) 
    175204        } 
    176205 
     
    328357                // Retrieve the data for both axes for all samples 
    329358                // TODO: handle the case of multiple fields on an axis 
    330                 def fields = [ "x": inputData.columnIds[ 0 ], "y": inputData.rowIds[ 0 ] ]; 
     359                def fields = [ "x": inputData.columnIds[ 0 ], "y": inputData.rowIds[ 0 ], "group": inputData.groupIds[ 0 ] ]; 
     360                def fieldInfo = [:] 
     361                fields.each {  
     362                        fieldInfo[ it.key ] = parseFieldId( it.value )  
     363                         
     364                        if( fieldInfo[ it.key ] ) 
     365                                fieldInfo[ it.key ].fieldType = determineFieldType( study.id, it.value ); 
     366                } 
     367                 
     368                // If the groupAxis is numerical, we should ignore it, unless a table is asked for 
     369                if( fieldInfo.group && fieldInfo.group.fieldType == NUMERICALDATA && inputData.visualizationType != "table" ) { 
     370                        fields.group = null; 
     371                        fieldInfo.group = null; 
     372                } 
     373                 
     374                // Fetch all data from the system. data will be in the format: 
     375                //              [ "x": [ 3, 6, null, 10 ], "y": [ "male", "male", "female", "female" ], "group": [ "US", "NL", "NL", "NL" ] 
     376                //      If a field is not given, the data will be NULL 
    331377                def data = getAllFieldData( study, samples, fields ); 
    332378 
    333                 // Group data based on the y-axis if categorical axis is selected 
    334         def groupedData 
    335         if(inputData.visualizationType=='horizontal_barchart'){ 
    336             groupedData = groupFieldData( inputData.visualizationType, data, "y", "x" ); // Indicate non-standard axis ordering 
    337         } else { 
    338             groupedData = groupFieldData( inputData.visualizationType, data ); // Don't indicate axis ordering, standard <"x", "y"> will be used 
    339         } 
    340         // Format data so it can be rendered as JSON 
    341         def returnData 
    342         if(inputData.visualizationType=='horizontal_barchart'){ 
    343             def valueAxisType = determineFieldType(inputData.studyIds[0], inputData.rowIds[0], groupedData["x"]) 
    344             def groupAxisType = determineFieldType(inputData.studyIds[0], inputData.columnIds[0], groupedData["y"]) 
    345             returnData = formatData( inputData.visualizationType, groupedData, fields, groupAxisType, valueAxisType , "y", "x" ); // Indicate non-standard axis ordering 
    346         } else { 
    347             def valueAxisType = determineFieldType(inputData.studyIds[0], inputData.rowIds[0], groupedData["y"]) 
    348             def groupAxisType = determineFieldType(inputData.studyIds[0], inputData.columnIds[0], groupedData["x"]) 
    349             returnData = formatData( inputData.visualizationType, groupedData, fields, groupAxisType, valueAxisType ); // Don't indicate axis ordering, standard <"x", "y"> will be used 
    350         } 
    351  
     379                // Aggregate the data based on the requested aggregation   
     380                def aggregatedData = aggregateData( data, fieldInfo, inputData.aggregation ); 
     381 
     382                // No convert the aggregated data into a format we can use 
     383                def returnData = formatData( inputData.visualizationType, aggregatedData, fieldInfo ); 
     384                 
    352385        // Make sure no changes are written to the database 
    353386        study.discard() 
     
    368401         */ 
    369402        def parseGetDataParams() { 
    370                 def studyIds, rowIds, columnIds, visualizationType; 
    371                  
    372                 studyIds = params.list( 'study' ); 
    373                 rowIds = params.list( 'rows' ); 
    374                 columnIds = params.list( 'columns' ); 
    375                 visualizationType = params.get( 'types') 
    376  
    377                 return [ "studyIds" : studyIds, "rowIds": rowIds, "columnIds": columnIds, "visualizationType": visualizationType ]; 
     403                def studyIds = params.list( 'study' ); 
     404                def rowIds = params.list( 'rows' ); 
     405                def columnIds = params.list( 'columns' ); 
     406                def groupIds = params.list( 'groups' );  
     407                def visualizationType = params.get( 'types'); 
     408                def aggregation = params.get( 'aggregation' ); 
     409 
     410                return [ "studyIds" : studyIds, "rowIds": rowIds, "columnIds": columnIds, "groupIds": groupIds, "visualizationType": visualizationType, "aggregation": aggregation ]; 
    378411        } 
    379412 
     
    383416         * @param samples       Samples for which the data should be retrieved 
    384417         * @param fields        Map with key-value pairs determining the name and fieldId to retrieve data for. Example: 
    385          *                                              [ "x": "field-id-1", "y": "field-id-3" ] 
     418         *                                              [ "x": "field-id-1", "y": "field-id-3", "group": "field-id-6" ] 
    386419         * @return                      A map with the same keys as the input fields. The values in the map are lists of values of the 
    387420         *                                      selected field for all samples. If a value could not be retrieved for a sample, null is returned. Example: 
    388          *                                              [ "x": [ 3, 6, null, 10 ], "y": [ "male", "male", "female", "female" ] ] 
     421         *                                              [ "numValues": 4, "x": [ 3, 6, null, 10 ], "y": [ "male", "male", "female", "female" ], "group": [ "US", "NL", "NL", "NL" ] ] 
    389422         */ 
    390423        def getAllFieldData( study, samples, fields ) { 
    391424                def fieldData = [:] 
     425                def numValues = 0; 
    392426                fields.each{ field -> 
    393                         fieldData[ field.key ] = getFieldData( study, samples, field.value ); 
    394                 } 
     427                        def fieldId = field.value ?: null; 
     428                        fieldData[ field.key ] = getFieldData( study, samples, fieldId ); 
     429                         
     430                        if( fieldData[ field.key ] ) 
     431                                numValues = Math.max( numValues, fieldData[ field.key ].size() ); 
     432                } 
     433                 
     434                fieldData.numValues = numValues; 
    395435                 
    396436                return fieldData; 
     
    407447        */ 
    408448        def getFieldData( study, samples, fieldId ) { 
     449                if( !fieldId ) 
     450                        return null 
     451                         
    409452                // Parse the fieldId as given by the user 
    410453                def parsedField = parseFieldId( fieldId ); 
     
    502545                return data 
    503546        } 
    504  
    505         /** 
    506          * Group the field data on the values of the specified axis. For example, for a bar chart, the values  
    507          * on the x-axis should be grouped. Currently, the values for each group are averaged, and the standard 
    508          * error of the mean is returned in the 'error' property 
    509      * @param visualizationType Some types require a different formatting/grouping of the data, such as 'table' 
    510          * @param data          Data for both group- and value axis. The output of getAllFieldData fits this input 
    511          * @param groupAxis     Name of the axis to group on. Defaults to "x" 
    512          * @param valueAxis     Name of the axis where the values are. Defaults to "y" 
    513          * @param errorName     Key in the output map where 'error' values (SEM) are stored. Defaults to "error"  
    514          * @param unknownName   Name of the group for all null groups. Defaults to "unknown" 
    515          * @return                      A map with the keys 'groupAxis', 'valueAxis' and 'errorName'. The values in the map are lists of values of the 
    516          *                                      selected field for all groups. For example, if the input is 
    517          *                                              [ "x": [ "male", "male", "female", "female", null, "female" ], "y": [ 3, 6, null, 10, 4, 5 ] ] 
    518          *                                      the output will be: 
    519          *                                              [ "x": [ "male", "female", "unknown" ], "y": [ 4.5, 7.5, 4 ], "error": [ 1.5, 2.5, 0 ] ] 
    520          * 
    521          *                                      As you can see: null values in the valueAxis are ignored. Null values in the  
    522          *                                      group axis are combined into a 'unknown' category. 
    523          */ 
    524         def groupFieldData( visualizationType, data, groupAxis = "x", valueAxis = "y", errorName = "error", unknownName = "unknown" ) { 
    525                 // TODO: Give the user the possibility to change this value in the user interface 
    526                 def showEmptyCategories = false; 
    527                  
    528                 // Create a unique list of values in the groupAxis. First flatten the list, since it might be that a 
    529                 // sample belongs to multiple groups. In that case, the group names should not be the lists, but the list 
    530                 // elements. A few lines below, this case is handled again by checking whether a specific sample belongs 
    531                 // to this group.  
    532                 // After flattening, the list is uniqued. The closure makes sure that values with different classes are 
    533                 // always treated as different items (e.g. "" should not equal 0, but it does if using the default comparator) 
    534                 def groups = data[ groupAxis ] 
    535                                                 .flatten() 
    536                                                 .unique { it == null ? "null" : it.class.name + it.toString() } 
    537                                                  
     547         
     548        /** 
     549         * Aggregates the data based on the requested aggregation on the categorical fields 
     550         * @param data                  Map with data for each dimension as retrieved using getAllFieldData. For example: 
     551         *                                                      [ "x": [ 3, 6, 8, 10 ], "y": [ "male", "male", "female", "female" ], "group": [ "US", "NL", "NL", "NL" ] ] 
     552         * @param fieldInfo             Map with field information for each dimension. For example: 
     553         *                                                      [ "x": [ id: "abc", "type": NUMERICALDATA ], "y": [ "id": "def", "type": CATEGORICALDATA ] ] 
     554         * @param aggregation   Kind of aggregation requested 
     555         * @return                              Data that is aggregated on the categorical fields 
     556         *                                                      [ "x": [ 3, 6, null, 9 ], "y": [ "male", "male", "female", "female" ], "group": [ "US", "NL", "US", "NL" ] ] 
     557         *  
     558         */ 
     559        def aggregateData( data, fieldInfo, aggregation ) { 
     560                // Determine the categorical fields 
     561                def dimensions = [ "categorical": [], "numerical": [] ]; 
     562                fieldInfo.each {  
     563                        // If fieldInfo value is NULL, the field is not requested 
     564                        if( it && it.value ) { 
     565                                if( [ CATEGORICALDATA, RELTIME, DATE ].contains( it.value.fieldType ) ) { 
     566                                        dimensions.categorical << it.key 
     567                                } else { 
     568                                        dimensions.numerical << it.key 
     569                                } 
     570                        } 
     571                } 
     572                 
     573                // Compose a map with aggregated data 
     574                def aggregatedData = [:]; 
     575                fieldInfo.each { aggregatedData[ it.key ] = [] } 
     576                 
     577                // Loop through all categorical fields and aggregate the values for each combination 
     578                if( dimensions.categorical.size() > 0 ) { 
     579                        return aggregate( data, dimensions.categorical, dimensions.numerical, aggregation, fieldInfo ); 
     580                } else { 
     581                        // No categorical dimensions. Just compute the aggregation for all values together 
     582                        def returnData = [ "count": [ data.numValues ] ]; 
     583                  
     584                        // Now compute the correct aggregation for each numerical dimension. 
     585                        dimensions.numerical.each { numericalDimension -> 
     586                                def currentData = data[ numericalDimension ]; 
     587                                returnData[ numericalDimension ] = [ computeAggregation( aggregation, currentData ).value ]; 
     588                        } 
     589                         
     590                        return returnData; 
     591                } 
     592        } 
     593         
     594        /** 
     595         * Aggregates the given data on the categorical dimensions. 
     596         * @param data                                  Initial data 
     597         * @param categoricalDimensions List of categorical dimensions to group  by 
     598         * @param numericalDimensions   List of all numerical dimensions to compute the aggregation for 
     599         * @param aggregation                   Type of aggregation requested 
     600         * @param fieldInfo                             Information about the fields requested by the user      (e.g. [ "x": [ "id": 1, "fieldType": CATEGORICALDATA ] ] ) 
     601         * @param criteria                              The criteria the current aggregation must keep (e.g. "x": "male") 
     602         * @param returnData                    Initial return object with the same keys as the data object, plus 'count'  
     603         * @return 
     604         */ 
     605        protected def aggregate( Map data, Collection categoricalDimensions, Collection numericalDimensions, String aggregation, fieldInfo, criteria = [:], returnData = null ) { 
     606                if( !categoricalDimensions ) 
     607                        return data; 
     608                         
     609                // If no returndata is given, initialize the map 
     610                if( returnData == null ) { 
     611                        returnData = [ "count": [] ] 
     612                        data.each { returnData[ it.key ] = [] } 
     613                } 
     614                 
     615                def dimension = categoricalDimensions.head(); 
     616                 
     617                // Determine the unique values on the categorical axis and sort by toString method 
     618                def unique = data[ dimension ].flatten() 
     619                                        .unique { it == null ? "null" : it.class.name + it.toString() } 
     620                                        .sort { 
     621                                                // Sort categoricaldata on its string value, but others (numerical, reltime, date)  
     622                                                // on its real value 
     623                                                switch( fieldInfo[ dimension ].fieldType ) { 
     624                                                        case CATEGORICALDATA: 
     625                                                                return it.toString() 
     626                                                        default: 
     627                                                                return it 
     628                                                }  
     629                                        }; 
     630                                         
    538631                // Make sure the null category is last 
    539                 groups = groups.findAll { it != null } + groups.findAll { it == null } 
    540                  
    541                 // Generate the output object 
    542                 def outputData = [:] 
    543                 outputData[ valueAxis ] = []; 
    544                 outputData[ errorName ] = []; 
    545                 outputData[ groupAxis ] = []; 
    546                  
    547                 // Loop through all groups, and gather the values for this group 
    548         // A visualization of type 'table' is a special case. There, the counts of two combinations of 'groupAxis' 
    549                 // and 'valueAxis' items are computed 
    550         if( visualizationType=='table' ){ 
    551             // For each 'valueAxis' item and 'groupAxis' item combination, count how often they appear together. 
    552             def counts = [:] 
     632                unique = unique.findAll { it != null } + unique.findAll { it == null } 
     633                 
     634                unique.each { el -> 
     635                        // Use this element to search on 
     636                        criteria[ dimension ] = el; 
    553637                         
    554             // The 'counts' list uses keys like this: ['item1':group, 'item2':value] 
    555             // The value will be an integer (the count) 
    556             data[ groupAxis ].eachWithIndex { group, index -> 
    557                 def value =  data[ valueAxis ][index] 
    558                 if(!counts.get(['item1':group, 'item2':value])){ 
    559                     counts[['item1':group, 'item2':value]] = 1 
    560                 } else { 
    561                     counts[['item1':group, 'item2':value]] = counts[['item1':group, 'item2':value]] + 1 
    562                 } 
    563             } 
    564              
    565                         def valueData =  data[ valueAxis ] 
    566                                         .flatten() 
    567                                         .unique { it == null ? "null" : it.class.name + it.toString() } 
    568                                                                                                          
    569                         // Now we will first check whether any of the categories is empty. If some of the rows 
    570                         // or columns are empty, don't include them in the output 
    571                         if( !showEmptyCategories ) { 
    572                                 groups.eachWithIndex { group, index -> 
    573                                         if( counts.findAll { it.key.item1 == group } ) 
    574                                                 outputData[groupAxis] << group 
     638                        // If the list of categoricalDimensions is empty after this dimension, do the real work 
     639                        if( categoricalDimensions.size() == 1 ) { 
     640                                // Search for all elements in the numericaldimensions that belong to the current group 
     641                                // The current group is defined by the criteria object 
     642                                 
     643                                // We start with all indices belonging to this group 
     644                                def indices = 0..data.numValues; 
     645                                criteria.each { criterion -> 
     646                                        // Find the indices of the samples that belong to this group. if a sample belongs to multiple groups (i.e. if 
     647                                        // the samples groupAxis contains multiple values, is a collection), the value should be used in all groups. 
     648                                        def currentIndices = data[ criterion.key ].findIndexValues { it instanceof Collection ? it.contains( criterion.value ) : it == criterion.value }; 
     649                                        indices = indices.intersect( currentIndices ); 
     650                                         
     651                                        // Store the value for the criterion in the returnData object 
     652                                        returnData[ criterion.key ] << criterion.value; 
    575653                                } 
    576654                                 
    577                                 valueData.each { value -> 
    578                                         if( counts.findAll { it.key.item2 == value } ) 
    579                                                 outputData[valueAxis] << value 
     655                                // If no numericalDimension is asked for, no aggregation is possible. For that reason, we  
     656                                // also return counts 
     657                                returnData[ "count" ] << indices.size(); 
     658                                  
     659                                // Now compute the correct aggregation for each numerical dimension. 
     660                                numericalDimensions.each { numericalDimension -> 
     661                                        def currentData = data[ numericalDimension ][ indices ];  
     662                                        returnData[ numericalDimension ] << computeAggregation( aggregation, currentData ).value; 
    580663                                } 
     664                                 
    581665                        } else { 
    582                                 outputData[groupAxis] = groups.collect { it != null ? it : unknownName } 
    583                                 ouputData[valueAxis] = valueData 
     666                                returnData = aggregate( data, categoricalDimensions.tail(), numericalDimensions, aggregation, fieldInfo, criteria, returnData ); 
    584667                        } 
    585                                                                                                                                  
    586             // Because we are collecting counts, we do not set the 'errorName' item of the 'outputData' map. 
    587             // We do however set the 'data' map to contain the counts. We set it in such a manner that it has  
    588                         // a 'table' layout with respect to 'groupAxis' and 'valueAxis'. 
    589             def rows = [] 
    590             outputData[groupAxis].each{ group -> 
    591                 def row = [] 
    592                 outputData[valueAxis].each{ value -> 
    593                     row << counts[['item1':group, 'item2':value]] 
    594                 } 
    595                 while(row.contains(null)){ 
    596                     row[row.indexOf(null)] = 0 
    597                 } // '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. 
    598                 if(row!=[]) rows << row 
    599             } 
    600             outputData['data']= rows 
    601                          
    602                         // Convert groups to group names 
    603                         outputData[ groupAxis ] = outputData[ groupAxis ].collect { it != null ? it : unknownName } 
    604         } else { 
    605             groups.each { group -> 
    606                 // Find the indices of the samples that belong to this group. if a sample belongs to multiple groups (i.e. if 
    607                 // the samples groupAxis contains multiple values, is a collection), the value should be used in all groups. 
    608                 def indices = data[ groupAxis ].findIndexValues { it instanceof Collection ? it.contains( group ) : it == group }; 
    609                 def values  = data[ valueAxis ][ indices ] 
    610  
    611                                 // The computation for mean and error will return null if no (numerical) values are found 
    612                                 // In that case, the user won't see this category 
    613                 def dataForGroup = null; 
    614                 switch( params.get( 'aggregation') ) { 
    615                                 case "average": 
    616                         dataForGroup = computeMeanAndError( values ); 
    617                         break; 
    618                     case "count": 
    619                         dataForGroup = computeCount( values ); 
    620                         break; 
    621                     case "median": 
    622                         dataForGroup = computeMedian( values ); 
    623                         break; 
    624                     case "none": 
    625                         // Currently disabled, create another function 
    626                         dataForGroup = computeMeanAndError( values ); 
    627                         break; 
    628                     case "sum": 
    629                         dataForGroup = computeSum( values ); 
    630                         break; 
    631                     default: 
    632                         // Default is "average" 
    633                         dataForGroup = computeMeanAndError( values ); 
    634                 } 
    635  
    636  
    637                                 if( showEmptyCategories || dataForGroup.value != null ) { 
    638                                         // Gather names for the groups. Most of the times, the group names are just the names, only with 
    639                                         // a null value, the unknownName must be used 
    640                                         outputData[ groupAxis ] << ( group != null ? group : unknownName ) 
    641                         outputData[ valueAxis ] << dataForGroup.value ?: 0 
    642                         outputData[ errorName ] << dataForGroup.error ?: 0 
    643                                 } 
    644             } 
    645         } 
    646                  
    647                 return outputData 
     668                } 
     669                 
     670                return returnData; 
    648671        } 
    649672         
     673        /** 
     674         * Compute the aggregation for a list of values 
     675         * @param aggregation 
     676         * @param currentData 
     677         * @return 
     678         */ 
     679        def computeAggregation( String aggregation, List currentData ) { 
     680                switch( aggregation ) { 
     681                        case "count": 
     682                                return computeCount( currentData ); 
     683                                break; 
     684                        case "median": 
     685                                return computeMedian( currentData ); 
     686                                break; 
     687                        case "sum": 
     688                                return computeSum( currentData ); 
     689                                break; 
     690                        case "average": 
     691                        default: 
     692                                // Default is "average" 
     693                                return computeMeanAndError( currentData ); 
     694                                break; 
     695                } 
     696        } 
     697 
    650698        /** 
    651699         * Formats the grouped data in such a way that the clientside visualization method  
    652700         * can handle the data correctly. 
    653701         * @param groupedData   Data that has been grouped using the groupFields method 
    654          * @param fields                Map with key-value pairs determining the name and fieldId to retrieve data for. Example: 
    655          *                                                      [ "x": "field-id-1", "y": "field-id-3" ] 
    656      * @param groupAxisType Integer, either CATEGORICAL or NUMERIACAL 
    657      * @param valueAxisType Integer, either CATEGORICAL or NUMERIACAL 
    658          * @param groupAxis             Name of the axis to with group data. Defaults to "x" 
    659          * @param valueAxis             Name of the axis where the values are stored. Defaults to "y" 
     702         * @param fieldData             Map with key-value pairs determining the name and fieldId to retrieve data for. Example: 
     703         *                                                      [ "x": { "id": ... }, "y": { "id": "field-id-3" }, "group": { "id": "field-id-6" } ] 
    660704         * @param errorName             Key in the output map where 'error' values (SEM) are stored. Defaults to "error"         *  
    661705         * @return                              A map like the following: 
     
    676720         *  
    677721         */ 
    678         def formatData( type, groupedData, fields, groupAxisType, valueAxisType, groupAxis = "x", valueAxis = "y", errorName = "error" ) { 
    679                 // We want to sort the data based on the group-axis, but keep the values on the value-axis in sync. 
    680                 // The only way seems to be to combine data from both axes. 
    681         def combined = [] 
    682         if(type=="table"){ 
    683             groupedData[ groupAxis ].eachWithIndex { group, i -> 
    684                 combined << [ "group": group, "data": groupedData[ 'data' ][ i ] ] 
    685             } 
    686             combined.sort { it.group.toString() } 
    687             groupedData[groupAxis] = renderTimesAndDatesHumanReadable(combined*.group, groupAxisType) 
    688             groupedData[valueAxis] = renderTimesAndDatesHumanReadable(groupedData[valueAxis], valueAxisType) 
    689             groupedData["data"] = combined*.data 
    690         } else { 
    691             groupedData[ groupAxis ].eachWithIndex { group, i -> 
    692                 combined << [ "group": (groupAxisType==CATEGORICALDATA ? group.toString() : group), "value": groupedData[ valueAxis ][ i ] ] 
    693             } 
    694             combined.sort { it.group } 
    695             groupedData[groupAxis] = renderTimesAndDatesHumanReadable(combined*.group, groupAxisType) 
    696             groupedData[valueAxis] = renderTimesAndDatesHumanReadable(combined*.value, valueAxisType) 
    697         } 
    698         // TODO: Handle name and unit of fields correctly 
    699         def valueAxisTypeString = (valueAxisType==CATEGORICALDATA || valueAxisType==DATE || valueAxisType==RELTIME ? "categorical" : "numerical") 
    700         def groupAxisTypeString = (groupAxisType==CATEGORICALDATA || groupAxisType==DATE || groupAxisType==RELTIME ? "categorical" : "numerical") 
    701  
    702         if(type=="table"){ 
    703             def return_data = [:] 
    704             return_data[ "type" ] = type 
    705             return_data.put("yaxis", ["title" : parseFieldId( fields[ valueAxis ] ).name, "unit" : parseFieldId( fields[ valueAxis ] ).unit, "type":valueAxisTypeString ]) 
    706             return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": parseFieldId( fields[ groupAxis ] ).unit, "type":groupAxisTypeString ]) 
    707             return_data.put("series", [[ 
    708                     "x": groupedData[ groupAxis ].collect { it.toString() }, 
    709                     "y": groupedData[ valueAxis ].collect { it.toString() }, 
    710                     "data": groupedData["data"] 
    711             ]]) 
    712             return return_data; 
    713         } else { 
    714             def xAxis = groupedData[ groupAxis ].collect { it.toString() }; 
    715             def yName = parseFieldId( fields[ valueAxis ] ).name; 
    716  
    717             def return_data = [:] 
    718             return_data[ "type" ] = type 
    719             return_data.put("yaxis", ["title" : yName, "unit" : parseFieldId( fields[ valueAxis ] ).unit, "type":valueAxisTypeString ]) 
    720             return_data.put("xaxis", ["title" : parseFieldId( fields[ groupAxis ] ).name, "unit": parseFieldId( fields[ groupAxis ] ).unit, "type":groupAxisTypeString  ]) 
    721             return_data.put("series", [[ 
    722                 "name": yName, 
    723                 "x": xAxis, 
    724                 "y": groupedData[ valueAxis ], 
    725                 "error": groupedData[ errorName ] 
    726             ]]) 
    727  
    728             return return_data; 
    729         } 
     722        def formatData( type, groupedData, fieldInfo, xAxis = "x", yAxis = "y", serieAxis = "group", errorName = "error" ) { 
     723                // Format categorical axes by setting the names correct 
     724                fieldInfo.each { field, info -> 
     725                        if( field && info ) { 
     726                                groupedData[ field ] = renderFieldsHumanReadable( groupedData[ field ], info.fieldType) 
     727                        } 
     728                } 
     729                 
     730                // TODO: Handle name and unit of fields correctly 
     731                def xAxisTypeString = dataTypeString( fieldInfo[ xAxis ]?.fieldType ) 
     732                def yAxisTypeString = dataTypeString( fieldInfo[ yAxis ]?.fieldType ) 
     733                def serieAxisTypeString = dataTypeString( fieldInfo[ serieAxis ]?.fieldType ) 
     734                 
     735                // Create a return object 
     736                def return_data = [:] 
     737                return_data[ "type" ] = type 
     738                return_data.put("xaxis", ["title" : fieldInfo[ xAxis ]?.name, "unit": fieldInfo[ xAxis ]?.unit, "type": xAxisTypeString ]) 
     739                return_data.put("yaxis", ["title" : fieldInfo[ yAxis ]?.name, "unit" : fieldInfo[ yAxis ]?.unit, "type": yAxisTypeString ]) 
     740                return_data.put("groupaxis", ["title" : fieldInfo[ serieAxis ]?.name, "unit" : fieldInfo[ serieAxis ]?.unit, "type": serieAxisTypeString ]) 
     741                 
     742                if(type=="table"){ 
     743                        // Determine the lists on both axes. The strange addition is done because the unique() method 
     744                        // alters the object itself, instead of only returning a unique list 
     745                        def xAxisData = ([] + groupedData[ xAxis ]).unique() 
     746                        def yAxisData = ([] + groupedData[ yAxis ]).unique() 
     747 
     748                        if( !fieldInfo[ serieAxis ] ) { 
     749                                // If no value has been chosen on the serieAxis, we should show the counts for only one serie 
     750                                def tableData = formatTableData( groupedData, xAxisData, yAxisData, xAxis, yAxis, "count" ); 
     751                                 
     752                                return_data.put("series", [[ 
     753                                        "name": "count", 
     754                                        "x": xAxisData, 
     755                                        "y": yAxisData, 
     756                                        "data": tableData 
     757                                ]]) 
     758                        } else if( fieldInfo[ serieAxis ].fieldType == NUMERICALDATA ) { 
     759                                // If no value has been chosen on the serieAxis, we should show the counts for only one serie 
     760                                def tableData = formatTableData( groupedData, xAxisData, yAxisData, xAxis, yAxis, serieAxis ); 
     761 
     762                                // If a numerical field has been chosen on the serieAxis, we should show the requested aggregation  
     763                                // for only one serie 
     764                                return_data.put("series", [[ 
     765                                        "name": fieldInfo[ xAxis ].name, 
     766                                        "x": xAxisData, 
     767                                        "y": yAxisData, 
     768                                        "data": groupedData[ serieAxis ] 
     769                                ]]) 
     770                        } else { 
     771                                // If a categorical field has been chosen on the serieAxis, we should create a table for each serie 
     772                                // with counts as data. That table should include all data for that serie 
     773                                return_data[ "series" ] = []; 
     774                                 
     775                                // The strange addition is done because the unique() method 
     776                                // alters the object itself, instead of only returning a unique list 
     777                                def uniqueSeries = ([] + groupedData[ serieAxis ]).unique(); 
     778                                 
     779                                uniqueSeries.each { serie ->  
     780                                        def indices = groupedData[ serieAxis ].findIndexValues { it == serie } 
     781                                         
     782                                        // If no value has been chosen on the serieAxis, we should show the counts for only one serie 
     783                                        def tableData = formatTableData( groupedData, xAxisData, yAxisData, xAxis, yAxis, "count", indices ); 
     784         
     785                                        return_data[ "series" ] << [ 
     786                                                "name": serie, 
     787                                                "x": xAxisData, 
     788                                                "y": yAxisData, 
     789                                                "data": tableData, 
     790                                        ] 
     791                                } 
     792                        } 
     793                         
     794                } else { 
     795                        // For a horizontal barchart, the two axes should be swapped 
     796                        if( type == "horizontal_barchart" ) {  
     797                                def tmp = xAxis 
     798                                xAxis = yAxis 
     799                                yAxis = tmp 
     800                        } 
     801                 
     802                        if( !fieldInfo[ serieAxis ] ) { 
     803                                // If no series field has defined, we return all data in one serie 
     804                                return_data.put("series", [[ 
     805                                        "name": "count", 
     806                                        "x": groupedData[ xAxis ], 
     807                                        "y": groupedData[ yAxis ], 
     808                                ]]) 
     809                        } else if( fieldInfo[ serieAxis ].fieldType == NUMERICALDATA ) { 
     810                                // No numerical series field is allowed in a chart.  
     811                                throw new Exception( "No numerical series field is allowed here." ); 
     812                        } else { 
     813                                // If a categorical field has been chosen on the serieAxis, we should create a group for each serie 
     814                                // with the correct values, belonging to that serie. 
     815                                return_data[ "series" ] = []; 
     816                                 
     817                                def uniqueSeries = groupedData[ serieAxis ].unique(); 
     818                                 
     819                                uniqueSeries.each { serie -> 
     820                                        def indices = groupedData[ serieAxis ].findIndexValues { it == serie } 
     821                                        return_data[ "series" ] << [ 
     822                                                "name": serie, 
     823                                                "x": groupedData[ xAxis ][ indices ], 
     824                                                "y": groupedData[ yAxis ][ indices ] 
     825                                        ] 
     826                                } 
     827                        } 
     828                } 
     829                 
     830                return return_data; 
     831        } 
     832 
     833        /** 
     834         * Formats the requested data for a table        
     835         * @param groupedData 
     836         * @param xAxisData 
     837         * @param yAxisData 
     838         * @param xAxis 
     839         * @param yAxis 
     840         * @param dataAxis 
     841         * @return 
     842         */ 
     843        def formatTableData( groupedData, xAxisData, yAxisData, xAxis, yAxis, dataAxis, serieIndices = null ) { 
     844                def tableData = [] 
     845                 
     846                xAxisData.each { x -> 
     847                        def colData = [] 
     848                         
     849                        def indices = groupedData[ xAxis ].findIndexValues { it == x } 
     850                         
     851                        // If serieIndices are given, intersect the indices 
     852                        if( serieIndices != null ) 
     853                                indices = indices.intersect( serieIndices ); 
     854                         
     855                        yAxisData.each { y -> 
     856                                def index = indices.intersect( groupedData[ yAxis ].findIndexValues { it == y } ); 
     857                                 
     858                                if( index.size() ) { 
     859                                        colData << groupedData[ dataAxis ][ (int) index[ 0 ] ] 
     860                                } 
     861                        } 
     862                        tableData << colData; 
     863                } 
     864                 
     865                return tableData; 
    730866        } 
    731867 
     
    737873     * @see determineFieldType 
    738874     */ 
    739     def renderTimesAndDatesHumanReadable(data, axisType){ 
    740         if(axisType==RELTIME){ 
    741             data = renderTimesHumanReadable(data) 
    742         } 
    743         if(axisType==DATE){ 
    744            data = renderDatesHumanReadable(data) 
    745         } 
    746         return data 
     875    def renderFieldsHumanReadable(data, axisType){ 
     876        switch( axisType ) { 
     877                        case RELTIME: 
     878                                return renderTimesHumanReadable(data) 
     879                        case DATE: 
     880                                return renderDatesHumanReadable(data) 
     881                        case CATEGORICALDATA: 
     882                                return data.collect { it.toString() } 
     883                        case NUMERICALDATA: 
     884                        default: 
     885                                return data; 
     886                } 
    747887    } 
    748888 
     
    10891229                def attrs = [:] 
    10901230 
     1231                if( !fieldId )  
     1232                        return null; 
     1233                 
    10911234                def parts = fieldId.split(",",5) 
    10921235                 
     
    10961239                        "source": new String(parts[ 2 ].decodeBase64()), 
    10971240                        "type": new String(parts[ 3 ].decodeBase64()), 
    1098             "unit": parts.length>4? new String(parts[ 4 ].decodeBase64()) : null 
     1241            "unit": parts.length>4? new String(parts[ 4 ].decodeBase64()) : null, 
     1242                        "fieldId": fieldId 
    10991243                ] 
    11001244 
    11011245        return attrs 
     1246        } 
     1247         
     1248        /** 
     1249         * Returns a string representation of the given fieldType, which can be sent to the userinterface 
     1250         * @param fieldType     CATEGORICALDATA, DATE, RELTIME, NUMERICALDATA 
     1251         * @return      String representation 
     1252         */ 
     1253        protected String dataTypeString( fieldType ) { 
     1254                return (fieldType==CATEGORICALDATA || fieldType==DATE || fieldType==RELTIME ? "categorical" : "numerical") 
    11021255        } 
    11031256         
     
    11431296        def study = Study.get(studyId) 
    11441297                def data = [] 
     1298                 
     1299                // If the fieldId is incorrect, or the field is not asked for, return  
     1300                // CATEGORICALDATA 
     1301                if( !parsedField ) 
     1302                        return CATEGORICALDATA; 
    11451303 
    11461304        try{ 
     
    13671525     */ 
    13681526    protected def determineVisualizationTypes(rowType, columnType){ 
    1369          def types = [] 
    1370         if(rowType==CATEGORICALDATA || DATE || RELTIME){ 
    1371             if(columnType==CATEGORICALDATA || DATE || RELTIME){ 
    1372                 types = [ [ "id": "table", "name": "Table"] ]; 
    1373             } 
    1374             if(columnType==NUMERICALDATA){ 
     1527                def types = [] 
     1528                 
     1529        if(rowType == CATEGORICALDATA || rowType == DATE || rowType == RELTIME){ 
     1530            if(columnType == CATEGORICALDATA || columnType == DATE || columnType == RELTIME){ 
     1531                                types = [ [ "id": "table", "name": "Table"] ]; 
     1532            } else {    // NUMERICALDATA 
    13751533                types = [ [ "id": "horizontal_barchart", "name": "Horizontal barchart"] ]; 
    13761534            } 
    1377         } 
    1378         if(rowType==NUMERICALDATA){ 
    1379             if(columnType==CATEGORICALDATA || DATE || RELTIME){ 
     1535        } else {        // NUMERICALDATA 
     1536            if(columnType == CATEGORICALDATA || columnType == DATE || columnType == RELTIME){ 
    13801537                types = [ [ "id": "barchart", "name": "Barchart"], [ "id": "linechart", "name": "Linechart"] ]; 
    1381             } 
    1382             if(columnType==NUMERICALDATA){ 
     1538            } else { 
    13831539                types = [ [ "id": "scatterplot", "name": "Scatterplot"], [ "id": "linechart", "name": "Linechart"] ]; 
    13841540            } 
     
    13861542        return types 
    13871543    } 
     1544         
     1545        /** 
     1546        * Returns the types of aggregation possible for the given two objects of either CATEGORICALDATA or NUMERICALDATA 
     1547        * @param rowType The type of the data that has been selected for the row, either CATEGORICALDATA or NUMERICALDATA 
     1548        * @param columnType The type of the data that has been selected for the column, either CATEGORICALDATA or NUMERICALDATA 
     1549        * @param groupType The type of the data that has been selected for the grouping, either CATEGORICALDATA or NUMERICALDATA 
     1550        * @return 
     1551        */ 
     1552        protected def determineAggregationTypes(rowType, columnType, groupType = null ){ 
     1553                // A list of all aggregation types. By default, every item is possible 
     1554                def types = [ 
     1555                        [ "id": "average", "name": "Average", "disabled": false ], 
     1556                        [ "id": "count", "name": "Count", "disabled": false ], 
     1557                        [ "id": "median", "name": "Median", "disabled": false ], 
     1558                        [ "id": "none", "name": "No aggregation", "disabled": false ], 
     1559                        [ "id": "sum", "name": "Sum", "disabled": false ], 
     1560                ] 
     1561 
     1562                // Normally, all aggregation types are possible, with three exceptions: 
     1563                //              Categorical data on both axes. In that case, we don't have anything to aggregate, so we can only count 
     1564                //              Grouping on a numerical field is not possible. In that case, it is ignored 
     1565                //                      Grouping on a numerical field with categorical data on both axes (table) enabled aggregation, 
     1566                //                      In that case we can aggregate on the numerical field.  
     1567                 
     1568                if(rowType == CATEGORICALDATA || rowType == DATE || rowType == RELTIME){ 
     1569                        if(columnType == CATEGORICALDATA || columnType == DATE || columnType == RELTIME){ 
     1570                                 
     1571                                if( groupType == NUMERICALDATA ) { 
     1572                                        // Disable 'none', since that can not be visualized 
     1573                                        types.each { 
     1574                                                if( it.id == "none" ) 
     1575                                                        it.disabled = true 
     1576                                        } 
     1577                                } else { 
     1578                                        // Disable everything but 'count' 
     1579                                        types.each {  
     1580                                                if( it.id != "count" )   
     1581                                                        it.disabled = true 
     1582                                        } 
     1583                                } 
     1584                        } 
     1585                } 
     1586                 
     1587                return types 
     1588   } 
    13881589} 
  • trunk/grails-app/views/visualize/index.gsp

    r2078 r2101  
    7878                                <option value="count">Count</option> 
    7979                                <option value="median">Median</option> 
    80                                 <option value="none" disabled>No aggregation</option> 
     80                                <option value="none">No aggregation</option> 
    8181                                <option value="sum">Sum</option> 
    8282                            </select>