Changeset 2101


Ignore:
Timestamp:
Nov 11, 2011, 3:57:50 PM (5 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 edited

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>
Note: See TracChangeset for help on using the changeset viewer.