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!