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
}
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:
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:
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:
That padAxesMaximums function looks something like this:
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.
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.
Iteration over Arrays in ActionScript
I always need reminding that for each....in iterates over the values of an associative array:
...while for....in iterates over the keys of an associative array:
for each (var value:* in object){
trace(value);
}
...while for....in iterates over the keys of an associative array:
for (var key:String in object){
trace(key + ": " + object[key]); // object[key] is value
}
Flex -- Problem with LinearAxis
Error messages were popping up when I hovered my mouse over a chart. I discovered that this was due to a bug (or just poor coding) in NumericAxis. The formatDataTip function in the series was calling the formatForScreen function in NumericAxis. This function calls toString on whatever object gets passed in. If the object is null (as it could be if a data point is missing), a null pointer exception will result. The easy solution was to extend NumericAxis and override the formatForScreen function:
public override function formatForScreen(value:Object):String {
if(value == null){
return "";
}
return value.toString();
}
Wednesday, January 6, 2010
Flex -- Label Rotation in ColumnSeries
If you want the label displayed by a ColumnSeries to be rotated, you don't set a "labelRotation" property like you would on an AxisRenderer. Instead, you set the showLabelVertically property....on the ColumnChart. I don't know why it's there and not on the series. It would be nice if I could configure label rotation individually for each series. More importantly, though, if you use a ColumnSeries, you MUST use a ColumnChart. (You can add a LineSeries to a ColumnChart, but you cannot add a ColumnSeries to a LineChart.)
In ColumnSeries' updateTransform function, there's this line:
If the chart is anything but a ColumnChart, you would get an exception because the cast would be invalid.
The series is merely asking the chart for the value of that showLabelVertically property, so again, why not have it on the series?
In ColumnSeries' updateTransform function, there's this line:
if(chart && !(ColumnChart(chart).showLabelVertically))
If the chart is anything but a ColumnChart, you would get an exception because the cast would be invalid.
The series is merely asking the chart for the value of that showLabelVertically property, so again, why not have it on the series?
Subscribe to:
Posts (Atom)
