Wednesday, January 27, 2010

Forcing Flex to Render Labels OUTSIDE for a Column Series

When I ask Flex to render labels outside the column for a column series, I want them all to render outside the column.  According to the Flex 3.4 API, "Flex checks if any of the elements displayed in the chart require extra padding to display properly (for example, for labels). It adjusts the values of the minimum and maximum properties accordingly." 

This isn't happening.  Instead, the maximum value of the axis is set to the value of the largest column.  That column then takes up 100% of the available space.  Since there is no room for a label outside the column, Flex moves it inside.

The code in ColumnChart that does this is:
if(v.labelIsHorizontal)
{
    if(v.y < (isNaN(v.min) ? base : v.min))
    {
        v.labelY = v.y - v.labelHeight;
        if(v.labelY < this.dataRegion.top)
            v.labelY = v.y;
    }
        
    else
    {
        v.labelY = v.y;
        if(v.labelY > this.dataRegion.bottom)
            v.labelY = v.y - v.labelHeight;
        
    }
    v.labelX = v.x - columnSeries.seriesRenderData.renderedHalfWidth +columnSeries.seriesRenderData.renderedXOffset;
}


I had an unbending requirement that all labels must render outside the column, so if I had a bar or column chart with labelPosition = "outside", I'd have to pad the axis/axes maximum values to provide enough space for the outside label.

My component was a base cartesian chart that could dynamically render series.  When generating the series or after a user re-sized the chart, I would check if I needed padding, and would call:
if(padAxes){
    callLater(padAxesMaximums);
}

That padAxesMaximums function looks something like this:
private function padAxesMaximums():void {
    
    if(!_padAxesRequestPending){
        return;
    }
    
    _padAxesRequestPending = false;
    
    var axisLength:Number;
    var axisMax:Number;
    var clearance:Number;
    var i:int;
    var j:int;
    var k:int;
    var items:Array;
    var label:Label;
    var labelContainer:Sprite;
    var pad:Number;
    var tmpNum:Number;
    
    // Map
    // key = IAxis object - references a particular axis ( primary or secondary )
    // value = maximum axis value ( NOT pixels )
    var maxAxisSuggestion:Dictionary = new Dictionary();
    
    if(theChart.series == null){
        return;
    }
    
    if(theChart is BarChart){

        for(i = 0; i < theChart.series.length; i++){

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

            labelContainer = theChart.series[i].labelContainer;
            pad = theChart.getStyle("paddingRight");
            clearance = theChart.computedGutters.right + pad;
            items = theChart.series[i].items;
            
            if(items == null || labelContainer == null || items.length != labelContainer.numChildren){
                continue;
            }

            axisMax = 0;

            for(j = 0; j < items.length; j++){
                
                var bsi:BarSeriesItem = items[j];
                label = labelContainer.getChildAt(j) as Label;
                
                if((bsi.x + label.textWidth + clearance) > axisLength){
                    
                    //we have a label stuck inside, yearning to be outside
                    tmpNum = Math.ceil(theChart.series[i].horizontalAxis.computedMaximum * axisLength / (axisLength - clearance - label.textWidth));
                    
                    // take the maximum of the current running value, or the newly calcualted value.
                    axisMax = Math.max(tmpNum, axisMax);

                }
                
                // after looping through all of the labels and seeing
                // what the max would be - then set the max
                if( isNaN( maxAxisSuggestion[theChart.series[i].horizontalAxis] ) ) {
                    maxAxisSuggestion[theChart.series[i].horizontalAxis] = 0;
                }
                maxAxisSuggestion[theChart.series[i].horizontalAxis] = Math.max(maxAxisSuggestion[theChart.series[i].horizontalAxis], axisMax);
                
            }//end iteration over BarSeriesItems
        }//end iteration over BarSeries

    }
    else if(theChart is ColumnChart){
        
        for(i = 0; i < theChart.series.length; i++){

            labelContainer = theChart.series[i].labelContainer;
            pad = theChart.getStyle("paddingTop");
            clearance = theChart.computedGutters.top + pad;
            items = theChart.series[i].items;
            
            if(items == null || labelContainer == null || items.length != labelContainer.numChildren){
                continue;
            }

            axisMax = 0;
            
            for(j = 0; j < items.length; j++){
                
                var csi:ColumnSeriesItem = items[j];
                label = labelContainer.getChildAt(j) as Label;
                
                if(csi.y < (clearance + label.height)){
                    //we have a label stuck inside, yearning to be outside
                    tmpNum = Math.ceil(theChart.series[i].verticalAxis.computedMaximum * csi.min / (csi.min - clearance - label.height));

                    // take the maximum of the current running value, or the newly calcualted value.
                    axisMax = Math.max(tmpNum, axisMax);
                }
            }   
            // after looping through all of the labels and seeing
            // what the max would be - then set the max
            if( isNaN( maxAxisSuggestion[theChart.series[i].verticalAxis] ) ) {
                maxAxisSuggestion[theChart.series[i].verticalAxis] = 0;
            }
            
            maxAxisSuggestion[theChart.series[i].verticalAxis] = Math.max(maxAxisSuggestion[theChart.series[i].verticalAxis], axisMax);
            
        }
        
    }

    for ( var key:Object in maxAxisSuggestion ) {
        var keyAxis:LinearAxis = key as LinearAxis;
        if( keyAxis != null ) {
            var value:Number = maxAxisSuggestion[key] as Number;
            keyAxis.maximum = value;
        }
    } 
    
}


Padding the column chart was easiest, because the length of the axis was available as the "min" parameter on the ColumnSeriesItem.  For the bar chart, I had to iterate through the horizontalAxisRenderers to find the length.

No comments:

Post a Comment