Milos Zikic - Personal site, sharing thoughts about startups, products and engineering

Using different fill colors for positive and negative values in AreaChart

Recently I have been playing with Flex AreaChart component and wanted to have different color fills for positive and negative values. This is not available out of the box and in order to achieve this new areaRenderer needs to be created.


I found great forum thread that tackles similar thing (http://forums.adobe.com/message/3005273) and this component needed few improvements to have good stroke colors on negative side as well. 


The biggest challenge was the part where line drops from positive to negative value and should change colors in between values and vice versa. Luckily there is renderedBase property available that holds info where is the zero point and with a little bit of trigonometry we can find out where are the important points where we need to split the line and draw different colors.


And at the end here is the code:


AreaChartRenderer:

package 
{

import flash.display.Graphics;
import flash.geom.Rectangle;

import mx.charts.chartClasses.GraphicsUtilities;
import mx.charts.renderers.AreaRenderer;
import mx.graphics.IFill;
import mx.graphics.IStroke;
import mx.graphics.SolidColor;
import mx.graphics.SolidColorStroke;

public class AreaChartRenderer extends AreaRenderer
{

private var strokePositive:uint = 0x127625;
private var strokeNegative:uint = 0xCC2800;

private var fillPositive:uint = 0x127625;
private var alphaPositive:Number = 0.2;
private var fillNegative:uint = 0xCC2800;
private var alphaNegative:Number = 0.2;


private static var noStroke:SolidColorStroke = new SolidColorStroke(0, 0, 0);

/**
* @private
*/
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
if (!data)
{
return;
}


var fill:IFill = null;
var stroke:IStroke = null;

var form:String = getStyle("form");

var g:Graphics = graphics;
g.clear();



var boundary:Array /* of Object */ = data.filteredCache;
var n:int = boundary.length;
if (n == 0)
{
return;
}

var xMin:Number;
var xMax:Number = xMin = boundary[0].x;
var yMin:Number;
var yMax:Number = yMin = boundary[0].y;

var v:Object;

// loop through items and prepare fills
for (var i:int = 0; i < n; i++)
{
v = boundary[i];

xMin = Math.min(xMin, v.x);
yMin = Math.min(yMin, v.y);
xMax = Math.max(xMax, v.x);
yMax = Math.max(yMax, v.y);

if (!isNaN(v.min))
{
yMin = Math.min(yMin, v.min);
yMax = Math.max(yMax, v.min);
}

//middle values
var x1:Number, y1:Number;

if (boundary[i + 1] != null && boundary[i].item.value > 0 && boundary[i + 1].item.value < 0)
{
// we have a transtion from positive to negative. We need to break this down to 2 areas for filling

// determine middle point

// y middle point

y1 = data.renderedBase;

x1 = Math.sqrt(Math.pow((boundary[i + 1].x - boundary[i].x), 2) * Math.pow((y1 - boundary[i].y), 2) / Math.pow((boundary[i + 1].y - boundary[i].y), 2)) + boundary[i].x;

// color positive
fill = new SolidColor(fillPositive, alphaPositive);
stroke = new SolidColorStroke(strokePositive);


fill.begin(g, new Rectangle(xMin, yMin, x1 - xMin, y1 - yMin), null);
GraphicsUtilities.drawPolyLine(g, boundary, i, i + 2, "x", "y", noStroke, form);
g.lineTo(x1, data.renderedBase);
g.lineTo(boundary[i].x, data.renderedBase);
g.endFill();

// positive stroke
drawStroke(g, boundary, i, stroke, form, x1, y1);

// color negative
fill = new SolidColor(fillNegative, alphaNegative);
stroke = new SolidColorStroke(strokeNegative);

fill.begin(g, new Rectangle(x1, y1, xMax - x1, yMax - y1), null);
GraphicsUtilities.drawPolyLine(g, boundary, i, i + 2, "x", "y", noStroke, form);
g.lineTo(boundary[i + 1].x, data.renderedBase);
g.lineTo(x1, data.renderedBase);
g.endFill();

drawStroke(g, boundary, i, stroke, form, boundary[i + 1].x, boundary[i + 1].y, x1, y1);


}
else if (boundary[i + 1] != null && boundary[i].item.value < 0 && boundary[i + 1].item.value > 0)
{
// we have a transtion from negative to positive. We need to break this down to 2 areas for filling

// determine middle point

// y middle point

y1 = data.renderedBase;

x1 = Math.sqrt(Math.pow((boundary[i + 1].x - boundary[i].x), 2) * Math.pow((y1 - boundary[i].y), 2) / Math.pow((boundary[i + 1].y - boundary[i].y), 2)) + boundary[i].x;


// color negative
fill = new SolidColor(fillNegative, alphaNegative);
stroke = new SolidColorStroke(strokeNegative);

fill.begin(g, new Rectangle(xMin, yMin, x1 - xMin, y1 - yMin), null);
GraphicsUtilities.drawPolyLine(g, boundary, i, i + 2, "x", "y", noStroke, form);
g.lineTo(x1, data.renderedBase);
g.lineTo(boundary[i].x, data.renderedBase);
g.endFill();

drawStroke(g, boundary, i, stroke, form, x1, y1);

// color positive
fill = new SolidColor(fillPositive, alphaPositive);
stroke = new SolidColorStroke(strokePositive);


fill.begin(g, new Rectangle(x1, y1, xMax - x1, yMax - y1), null);
GraphicsUtilities.drawPolyLine(g, boundary, i, i + 2, "x", "y", noStroke, form);
g.lineTo(boundary[i + 1].x, data.renderedBase);
g.lineTo(x1, data.renderedBase);
g.endFill();

drawStroke(g, boundary, i, stroke, form, boundary[i + 1].x, boundary[i + 1].y, x1, y1);
}
else
{

if (boundary[i].item.value > 0)
{
fill = new SolidColor(fillPositive, alphaPositive);
stroke = new SolidColorStroke(strokePositive);
}
else
{
fill = new SolidColor(fillNegative, alphaNegative);
stroke = new SolidColorStroke(strokeNegative);
}


if (i < n - 1)
{
fill.begin(g, new Rectangle(xMin, yMin, xMax - xMin, yMax - yMin), null);
GraphicsUtilities.drawPolyLine(g, boundary, i, i + 2, "x", "y", noStroke, form);
g.lineTo(boundary[i + 1].x, data.renderedBase);
g.lineTo(boundary[i].x, data.renderedBase);
g.endFill();


drawStroke(g, boundary, i, stroke, form);
}
}
}



}

/**
* Draws a line with a given stroke between 2 points. It either uses boundary point array or start and end points
*
*
*/

private function drawStroke(g:Graphics, boundary:Array, i:int, stroke:IStroke, form:String, endX:Number = NaN, endY:Number = NaN, startX:Number = NaN, startY:Number = NaN):void
{

if (isNaN(startX))
{
g.moveTo(boundary[i].x, boundary[i].y);
} else {
g.moveTo(startX, startY);
}

var colorStroke:SolidColorStroke = stroke as SolidColorStroke;
g.lineStyle(colorStroke.weight, colorStroke.color, colorStroke.alpha);

if (boundary[i].element.minField != null && boundary[i].element.minField != "")
{
g.lineTo(boundary[i + 1].x, boundary[i + 1].min);

GraphicsUtilities.drawPolyLine(g, boundary, i + 1, i, "x", "min", noStroke, form, false);
}
else
{
if (isNaN(endX))
{
g.lineTo(boundary[i + 1].x, boundary[i + 1].y);
}
else
{
g.lineTo(endX, endY);
}
}

g.endFill();
}
}
}


And here is the final result:

Happy charting!

Comments

Miloš Žikić
Hi there,
Complete source code for the renderer is available in the blog post. Copy the class and apply it as your itemRenderer.

A little bit about chart item renderers can be found at Adobe help pages:

http://help.adobe.com/en_US/flex/using/WS2db454920e96a9e51e63e3d11c0bf69084-7c52.html
Anonymous
Hi,

Could you please post your example with source code so that we can run on flex builder.

Thanks in advanced
Miloš Žikić
This way it is more fun :)

Since lines are continuous when having 2 lines on the chart (negative and positive) when they are not bellow or above the zero line they will be running through the zero line axis which will not look good.
Anonymous
Hi

Why not just use the yValue to determine the profit and loss values and create a seperate profitArray and lossArray. Draw two seperate polyLine with different color and stroke. Wouldn't that be a cleaner solution?

Regards
Ashwin
Miloš Žikić
Hi there,

I think that you can do the same as I did with AreaRenderer extend the BarCharts BoxItemRendeder (if you are using this) and in similar fashion overriide updateDisplayList method.

http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/mx/charts/renderers/BoxItemRenderer.html#protectedMethodSummary

I suppose that you can use the same check to see if you are above or bellow some value :

if (boundary[i + 1] != null && boundary[i].item.value > 0 && boundary[i + 1].item.value < 0)

Let me know if you managed it!

Cheers,
Miloš
Miloš Žikić
This comment has been removed by the author.
Anonymous
Hi,

I have a similar problem. I have a bar series to be plotted. I use a itemrenderer to colour the bar either red or green depending on the xvalue is greater than zero or less than zero. I need every single bar in that bar chart(both green and red) to have two different shades. If the bar crosses a cutoff limit then it should be indicated using a slightly different red or green on both negative and positive side. Any solution for this? Thanks

Share this post