Saturday, February 6, 2010

Column Labels Redux

My work life is a series of Roseanne Roseannadanna moments.  ("Well, Jane, it just goes to show you, it's always something.")  I discovered that my code to force labels to render outside columns will not work when there are negative values.  This code, however, should work:


private function adjustAxes(pLengthDict:Dictionary, vPosDict:Dictionary, vNegDict:Dictionary, clearance:Number):void {

    var axis:LinearAxis
    var axisLength:Number
    var compChartMax:Number;
    var key:Object;
    var maxNegValue:Number;
    var maxPosValue:Number;
    var obj:Object
    var percentPositive:Number;
    var refChartMax:Number;
    var referenceAxis:LinearAxis;
    var referencePercentPositive:Number;

    for (key in vPosDict){

        axis = key as LinearAxis;
        axisLength = pLengthDict[key];
        maxPosValue = vPosDict[key];
        maxNegValue = vNegDict[key];
        
        obj = calculateMaxMin(maxPosValue, maxNegValue, axisLength, clearance);
        
        if(maxPosValue > 0){
            axis.maximum = obj.max;
        }
        else {
            axis.maximum = 0;
        }

        if(maxNegValue > 0){
            axis.minimum = obj.min;
        }
        else {
            axis.minimum = 0;
        }
        
        percentPositive = axis.maximum / (axis.maximum - axis.minimum);
        
        if(isNaN(referencePercentPositive)){
            referenceAxis = axis;
            referencePercentPositive = percentPositive;
        }
        else if(Math.abs(percentPositive - 0.5) < Math.abs(referencePercentPositive - 0.5)){
            referenceAxis = axis;
            referencePercentPositive = percentPositive;
        }

    } 
    
    //If there is more than one axis, Flex doesn't line up the zero 
    //value of each axis, so the data looks weird.  This forces the
    //zero values to line up.
    for (key in vPosDict){

        axis = key as LinearAxis;
        axisLength = pLengthDict[key];

        if(axis == referenceAxis){
            continue;
        }

        if(axis.baseAtZero){

            //we need to match the positive/negative ratio of the
            //reference chart so that the zero values will line up
            percentPositive = axis.maximum / (axis.maximum - axis.minimum);
            
            if(Math.abs(percentPositive - referencePercentPositive) >= 0.5){
                //The difference in axes is great.  Make them both 50/50
                //positive/negative.
                compChartMax = Math.max(axis.maximum, Math.abs(axis.minimum));
                obj = calculateMaxMin(compChartMax, compChartMax, axisLength, clearance);

                axis.maximum = obj.max;
                axis.minimum = obj.min;
                
                refChartMax = Math.max(referenceAxis.maximum, Math.abs(referenceAxis.minimum));
                obj = calculateMaxMin(refChartMax, refChartMax, pLengthDict[referenceAxis], clearance);

                referenceAxis.maximum = obj.max;
                referenceAxis.minimum = obj.min;
                referencePercentPositive = 0.5;
                 
            }
            else if(percentPositive < referencePercentPositive){
                axis.maximum = -axis.minimum * referencePercentPositive / (1 - referencePercentPositive);
            }
            else if(percentPositive > referencePercentPositive){
                axis.minimum = axis.maximum - (axis.maximum / referencePercentPositive);
            }
        }//end if(axis.baseAtZero)
    }//end iteration over axes

}

private function calculateMaxMin(maxPosValue:Number, maxNegValue:Number, 
        axisLength:Number, clearance:Number):Object {
    
    var useableAxisLength:Number = axisLength - clearance * 2;
    var unitsPerPixel:Number = Math.ceil((maxPosValue + maxNegValue)/useableAxisLength);

    var retObj:Object = new Object();
    retObj.max = clearance * unitsPerPixel + maxPosValue;
    retObj.min = -1 * (clearance * unitsPerPixel + maxNegValue);
          
    return retObj;

}

public function padAxes():void {
    
    if(theChart.series == null){
        return;
    }

    var clearance:Number;
    var i:int;
    var items:Array;
    var j:int;
    var label:Label
    var labelContainer:Sprite;

    // key for all Dictionary objects is an IAxis object, referencing a 
    //particular axis ( primary or secondary )

    //holds maximum positive value (NOT pixels) of data associated with a 
    //particular axis
    var vPosDict:Dictionary = new Dictionary();

    //holds the absolute value of the minimum negative value (NOT pixels) of 
    //data associated with a particular axis
    var vNegDict:Dictionary = new Dictionary();
    
    //holds the length, in pixels, of a particular axis
    var pLengthDict:Dictionary = new Dictionary();
    
    if(theChart is BarChart){

        var labelWidth:Number = 0;

        for(i = 0; i < theChart.series.length; i++){
            
            if(! theChart.series is BarSeries){
                continue;
            } 

            for(j = 0; j < theChart.horizontalAxisRenderers.length; j++){
                if(theChart.horizontalAxisRenderers[j].axis == theChart.series[i].horizontalAxis){
                    pLengthDict[theChart.series[i].horizontalAxis] = theChart.horizontalAxisRenderers[j].length;
                    break;
                }
            }

            labelContainer = theChart.series[i].labelContainer;
            items = theChart.series[i].items;

            if(items == null || labelContainer == null || items.length != labelContainer.numChildren){
                continue;
            }

            if(isNaN(vPosDict[theChart.series[i].horizontalAxis])){
                vPosDict[theChart.series[i].horizontalAxis] = 0;
            }
            
            if(isNaN(vNegDict[theChart.series[i].horizontalAxis])){
                vNegDict[theChart.series[i].horizontalAxis] = 0;
            }

            var bsi:BarSeriesItem;
            var tmpLabel:Label = new Label();
            
            for(j = 0; j < items.length; j++){
                
                bsi = items[j];
                label = labelContainer.getChildAt(j) as Label;

                //we use a textField to get an immediate measure of the width
                var textField:TextField = new TextField();
                textField.text = label.text;

                labelWidth = Math.max(labelWidth, textField.textWidth);

                var xValue:Number = new Number(bsi.xValue);
                
                if(xValue < 0){
                    vNegDict[theChart.series[i].horizontalAxis] = Math.max(vNegDict[theChart.series[i].horizontalAxis], Math.abs(xValue));
                }
                else {
                    vPosDict[theChart.series[i].horizontalAxis] = Math.max(vPosDict[theChart.series[i].horizontalAxis], xValue);
                }

            }//loop through bar series items
        }//loop through bar series

        clearance = theChart.computedGutters.left + theChart.getStyle("paddingLeft") + labelWidth;
        
        adjustAxes(pLengthDict, vPosDict, vNegDict, clearance);

        //Flex draws a new line for zero, but still keeps the old baseline.
        //This will remove the old baseline.     
        theChart.backgroundElements[0].setStyle("verticalShowOrigin", false);
        
    }
    else if(theChart is ColumnChart){

        var labelHeight:Number = 0;

        for(i = 0; i < theChart.series.length; i++){
            
            if(! theChart.series is ColumnSeries){
                continue;
            } 

            for(j = 0; j < theChart.verticalAxisRenderers.length; j++){
                if(theChart.verticalAxisRenderers[j].axis == theChart.series[i].verticalAxis){
                    pLengthDict[theChart.series[i].verticalAxis] = theChart.verticalAxisRenderers[j].length;
                    break;
                }
            }

            labelContainer = theChart.series[i].labelContainer;
            items = theChart.series[i].items;

            if(items == null || labelContainer == null || items.length != labelContainer.numChildren){
                continue;
            }

            if(isNaN(vPosDict[theChart.series[i].verticalAxis])){
                vPosDict[theChart.series[i].verticalAxis] = 0;
            }
            
            if(isNaN(vNegDict[theChart.series[i].verticalAxis])){
                vNegDict[theChart.series[i].verticalAxis] = 0;
            }

            var csi:ColumnSeriesItem;
            
            for(j = 0; j < items.length; j++){
                
                csi = items[j];
                label = labelContainer.getChildAt(j) as Label;
                labelHeight = Math.max(labelHeight, label.height);

                var yValue:Number = new Number(csi.yValue);
                
                if(yValue < 0){
                    vNegDict[theChart.series[i].verticalAxis] = Math.max(vNegDict[theChart.series[i].verticalAxis], Math.abs(yValue));
                }
                else {
                    vPosDict[theChart.series[i].verticalAxis] = Math.max(vPosDict[theChart.series[i].verticalAxis], yValue);
                }

            }//loop through column series items
        }//loop through column series
        
        clearance = theChart.computedGutters.top + theChart.getStyle("paddingTop") + labelHeight;

        adjustAxes(pLengthDict, vPosDict, vNegDict, clearance);

        //Flex draws a new line for zero, but still keeps the old baseline.
        //This will remove the old baseline.     
        theChart.backgroundElements[0].setStyle("horizontalShowOrigin", false);

    }//end if ColumnChart
}