@Override
protected String renderSummary(Map<String, Number> data, List<ColumnData> cs) {
List<ColumnData> csCopy = new ArrayList<ColumnData>();
// The totals row should never be blue. Strip out any background-color
// style.
for (ColumnData cdOrig : cs) {
ColumnData cd = copyColumnData(cdOrig);
csCopy.add(cd);
if (cd.style != null) {
int bgColorIndex = cd.style.indexOf("background-color");
if (bgColorIndex > -1) {
int endIndex = cd.style.indexOf(";", bgColorIndex);
StringBuilder sb = new StringBuilder(cd.style);
sb.delete(bgColorIndex, endIndex + 1);
cd.style = sb.toString();
}
}
}
return super.renderSummary(data, csCopy);
}
private ColumnData copyColumnData(ColumnData cdOrig) {
ColumnData cdCopy = new ColumnData();
cdCopy.id = cdOrig.id;
cdCopy.name = cdOrig.name;
cdCopy.style = cdOrig.style;
cdCopy.cellAttr = cdOrig.cellAttr;
cdCopy.css = cdOrig.css;
cdCopy.renderer = cdOrig.renderer;
return cdCopy;
}
Tuesday, July 10, 2012
Style Problem with Summary Row in Ext GWT (GXT) 2.5.5
I ran into a problem with styles in a SummaryColumnConfig. I was giving editable cells a blue background. When it got to the summary row, however, that cell was also being colored blue. The cell seems to use the last ColumnData.style value, so I needed a way to remove the background color on that cell. However, I had another section of grid after that summary row, and if I removed the color, the rest of the editable cells wouldn't have the blue background.
My solution was to override the renderSummary method in GroupSummaryView, copying the ColumnData so that the style I set would only apply to the summary row and wouldn't carry through to the next section:
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
}
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?
Tuesday, December 29, 2009
Problem Rendering a Horizontal Chart Legend in Flex
A big frustration I have with Flex is getting components laid out the way I intended. Usually the problem is with me setting improper values for height, width or what have you. By placing different colored borders around my components, I can often see where I went wrong.
Sometimes, though, Flex just won't let me have things my way. I almost always want a chart legend to be centered and the legend items rendered horizontally, so I'll wrap a Legend control in an HBox like this:
This has worked for me 99% of the time. The other 1% of the time, no matter how I configured my mxml, Flex would render my chart legend vertically. The problem took a while to figure out, but the solution was simple.
In the Legend class, calcColumnWidthsForWidth gets called twice, once with preferredWidth and again with unscaledWidth. For some reason, the unscaledWidth is Math.floor(preferredWidth). The problem with that is calcColumnWidthsForWidth has calculated the layout for the larger value, and now we've just taken away up to 0.99 pixels of the space it was counting on. For example, Flex might calculate that it needs a width of 914.32 pixels to lay out the legend items horizontally, but in another calculation, it thinks only 914 is available, so it will lay out the legend items vertically instead.
To get around this, extend the Legend class, override the measure function and round up the preferredWidth. Since we don't have access to preferredWidth, do this to measuredWidth to achieve the same effect:
Sometimes, though, Flex just won't let me have things my way. I almost always want a chart legend to be centered and the legend items rendered horizontally, so I'll wrap a Legend control in an HBox like this:
<mx:HBox width="100%" horizontalAlign="center" >
<mx:Legend dataProvider="{theChart}" direction="horizontal" />
</mx:HBox>
This has worked for me 99% of the time. The other 1% of the time, no matter how I configured my mxml, Flex would render my chart legend vertically. The problem took a while to figure out, but the solution was simple.
In the Legend class, calcColumnWidthsForWidth gets called twice, once with preferredWidth and again with unscaledWidth. For some reason, the unscaledWidth is Math.floor(preferredWidth). The problem with that is calcColumnWidthsForWidth has calculated the layout for the larger value, and now we've just taken away up to 0.99 pixels of the space it was counting on. For example, Flex might calculate that it needs a width of 914.32 pixels to lay out the legend items horizontally, but in another calculation, it thinks only 914 is available, so it will lay out the legend items vertically instead.
To get around this, extend the Legend class, override the measure function and round up the preferredWidth. Since we don't have access to preferredWidth, do this to measuredWidth to achieve the same effect:
override protected function measure():void {
super.measure();
measuredWidth = Math.ceil(measuredWidth);
}
Subscribe to:
Posts (Atom)
