I've been doing a lot of work lately with Flex
Charts and part of my requirements involve being able to take an unknown
amount of data received from a web service and convert that data into a usable
experience for my clients. To make things a little more complicated,
there is no way to know how many series of data I am going to need until
runtime. I addressed a way to create this data in another post here where
you process unique values of your data into a new data provider which serves as
the provider for your chart. Now that this is completed, I want to be
able to draw trend lines for each grouping of out categories that may be
incongruent. In other words, not every category may have a value within
each series.
I've been able to find lots of basic charting examples, but few of them
address more complicated business requirements. I realize this is not the
only way to achieve the required result, but it rather one way.
An example of this, for your visualization pleasure, could be a circumstance
where you are selling fruit. Each type of fruit is your category(which
becomes the grouping along the horizontal axis), and the series is represented
by individual fiscal years. I want all of the years to be visible in my
legend by year, but I want all types of fruit to be placed together on the
graph so I can see a comparison between them across multiple years.
So let's get started by looking at what I want
to see:
So in this example, we have a company that possibly sells different types of
fruit each year. For the types of fruit that have been sold longer than a
single year, we want to be able to visually see a trend over time between like
items. We will never know exactly how many years are going to be
represented, so all of the series have to be created at runtime from the data
returned. We also will never know the number of categories and will have
to create those groupings at runtime based upon the data.
Now the real question here is that we want to create a trend comparison
between items across different series, but keep in mind that the items in each
series may not always match up across the categories. This makes things a
little more difficult.
So lets write some code:
We are going to assume that you have already
read my previous post on how to group the series into categories and that you
now have a arrayCollection or XMLListCollection being used as your chart
component data provider. Inside the chart.series property, you will find
a set of objects listed that are the individual series for your chart. We
are going to use these to create our trend line.
In my example, I don't want to be able to see the trending line permanently,
but rather want to click a checkbox to turn it on and off. In order to
draw the trend line, I will be using a CartesianDataCanvas placed in my annotation
elements.
<mx:ColumnChart id="chart" width="100%"
height="100%" showDataTips="true" >
<mx:annotationElements>
<mx:CartesianDataCanvas id="cnvOverlay"
includeInRanges="true">
</mx:CartesianDataCanvas>
</mx:annotationElements>
</mx:ColumnChart>
<mx:CheckBox id="chkTrend" label="Show Trending"
change="{trendChangeHandler(event)}"/>
In the event handler for the change event on my checkbox, I will do the
following:
protected function trendChangeHandler(event:Event):void
{
if(event.currentTarget.selected)
{
for each(var
item:Object in hAxis.dataProvider)
{
createTrending(String(item));
}
}
else
{
cnvOverlay.clear();
}
}
Since my horizontal axis is not numerical data but rather a CategoryAxis, I can access each unique value
in the axis by looking at it's dataProvider property. This will show me a
collection of objects that I can then use to check my ColumnSeries against. Here is the
heavy lifter:
protected function createTrending(cat:String):void
{
try
{
cnvOverlay.lineStyle(4, 0x0066FF, .5,
true, LineScaleMode.NORMAL, CapsStyle.ROUND, JointStyle.MITER, 2);
var ser:Array =
chart.series;
var selectedData:Array = [];
// Iterate over each series to find
identical categories
for (var iIndex:int=0;
iIndex<ser.length; iIndex++)
{
var items:Array =
ser[iIndex].items;
// Iterate over each
item in the series
for (var jIndex:int=0;
jIndex<items.length; jIndex++) {
if (items[jIndex].item.Tag == cat)
{
selectedData.push(items[jIndex] as ColumnSeriesItem);
}
}
}
// Start with the first item in
the grouping and draw the trending line
if(selectedData.length > 1)
{
for(var
kIndex:int = 0; kIndex < selectedData.length; kIndex++)
{
var currentItem:ColumnSeriesItem =
ColumnSeriesItem(selectedData[kIndex]);
var nextItem:ColumnSeriesItem =
ColumnSeriesItem(selectedData[kIndex+1]);
if(nextItem != null)
{
var c1:String = new String();
c1 = currentItem.item.Category;
var v1:Number = new Number();
v1 = currentItem.yValue as Number;
cnvOverlay.moveTo(c1, v1);
var c2:String = new String();
c2 = nextItem.item.Category;
var v2:Number = new Number();
v2 = nextItem.yValue as Number;
cnvOverlay.lineTo(c2, v2);
}
else
{
break;
}
}
}
}
catch(err:Error)
{
throw new Error("Error creating trend
line: "+err.message);
trace(err);
}
}
Now there is a problem with this code.
Since Im using my category as my xValue, Flex uses the center of the category
(not the column) as the xValue. the result is a vertical line. Not
at all what I want. I want the trend line to start and end at the center
of each column. What to do, what to do??? Instead of setting the x
object point by using the method used in the example from the flex docs here, we have to figure out a different
way to handle this. In the example, there is only one columnItem per
horizontal axis value. We have n-number of columnItems per category. I'm going to do some more work on this today and see what i can figure out to solve this data space versus pixel space issue within a given category tag.
I found one solution here that I am going to look at today.