Avg. Rating 5.0
Tags:



Problem

There are some examples out on the web that add trend lines to Flex charting controls, but I found them buggy and difficult to use. They also do not support both numeric axis as well as date-time axis. I would like a simple class that adds a linear trend line that supports both kinds of axis.

Solution

To implement this solution you have to add a CartesianDataCanvas to your chart and use TrendLine.as custom class. The CartesianDataCanvas lets you draw over a chart without using the dataProvider. The TrenLine class generates a trend line using the chart dataProvider and draws a line over the CartesianDataCanvas using the first and last value of the trend line.

Detailed explanation

In this example, we are going to implement an application with two line graphs. One chart uses a LinearAxis and the other uses a DateTimeAxis. To populate those graphs we have two different dataProviders. One represents the sales vs. the price of a product. The second one represents the sales of that product in different months. So we have two different data types for the horizontal axis: Numbers and dates.

The MXML code of the two graphs:

<mx:LineChart id="linearAxisChart"
                  dataProvider="{priceSalesLst}">
        <mx:annotationElements>
            <mx:CartesianDataCanvas id="priceSalesCanvas"
                                    width="100%"
                                    height="100%"
                                    includeInRanges="true"/>
        </mx:annotationElements>
        <mx:horizontalAxis>
            <mx:LinearAxis/>
        </mx:horizontalAxis>
        <mx:verticalAxis>
            <mx:LinearAxis/>
        </mx:verticalAxis>

        <mx:series>
            <mx:LineSeries yField="sales"
                           xField="price"/>
        </mx:series>
    </mx:LineChart>

    <mx:LineChart id="dateTimeAxisChart"
                  dataProvider="{dateSalesLst}">
        <mx:annotationElements>
            <mx:CartesianDataCanvas id="dateSalesCanvas"
                                    width="100%"
                                    height="100%"
                                    includeInRanges="true"/>
        </mx:annotationElements>
        <mx:horizontalAxis>
            <mx:DateTimeAxis dataUnits="months"
                             parseFunction="parseDates"/>
        </mx:horizontalAxis>
        <mx:verticalAxis>
            <mx:LinearAxis/>
        </mx:verticalAxis>

        <mx:series>
            <mx:LineSeries yField="sales"
                           xField="date"/>
        </mx:series>
    </mx:LineChart>

 

The ActionScript code to define the two dataProviders :

[Bindable]
public var priceSalesLst:ArrayCollection=new ArrayCollection([{price: 100, sales: 5000}, {price: 500, sales: 1230}, {price: 1000, sales: 530}, {price: 1500, sales: 4100}]);
[Bindable]
public var dateSalesLst:ArrayCollection=new ArrayCollection([{date: "2011/1/1", sales: 345}, {date: "2011/2/1", sales: 123}, {date: "2011/3/1", sales: 311}, {date: "2011/4/1", sales: 600}]);

 

We also need a function to format the data to be displayed on the horizontal axis of the graph dateTimeAxisChart . For this we use the function parseDates.

public function parseDates(value:String):Date {
 var tokens:Array=value.split("/");
 var date:Date=new Date(tokens[0], tokens[1] - 1, tokens[2]);
 return date;
}

 

We should also add a button that calls the function  updateTrendLine . That function creates two different trend lines for each graph, with the following parameters:

  • xField. The attribute for the horizontal axis.
  • yField. The attribute for the vertical axis.
  • dataSet. The chart dataProvider.

After that, we just call the TrendLine public method drawTrendLine, with the corresponding CartesianDataCanvas as argument.

The code of updateTrendLine function:

public function updateTrendLine(event:MouseEvent): void {
 var priceSalesTrend:TrendLine = new TrendLine();
 priceSalesTrend.xField = "price";
 priceSalesTrend.yField = "sales";
 priceSalesTrend.dataSet = priceSalesLst;
 priceSalesTrend.drawTrendLine(priceSalesCanvas);

 var dateSalesTrend:TrendLine = new TrendLine();
 dateSalesTrend.xField = "date";
 dateSalesTrend.yField = "sales";
 dateSalesTrend.dataSet = dateSalesLst;
 dateSalesTrend.drawTrendLine(dateSalesCanvas);
}

 

Finally, you have to include the TrendLine class in your Flex project.  The TrendLine class uses the linear regression formula (y=a+bx), where a is the intercept and b is the slope. The example uses Number variables for all operations. If you are going to represent bigger values, you might have some accuracy problems. In this case, use another implementation of Number instead (for example BigDecimal).

package {
    import flash.display.CapsStyle;
    import flash.display.JointStyle;
    import flash.display.LineScaleMode;

    import mx.charts.chartClasses.CartesianCanvasValue;
    import mx.charts.chartClasses.CartesianDataCanvas;
    import mx.collections.ArrayCollection;

    public class TrendLine {

        public function TrendLine() {
        }

        private var _dataSet:ArrayCollection;

        public function set dataSet(value:ArrayCollection):void {
            this._dataSet=value;
            calculateTrendData();
        }

        /**
         * The chart data set.
         * */
        public function get dataSet():ArrayCollection {
            return this._dataSet;
        }

        private var _xField:String;

        public function set xField(value:String):void {
            this._xField=value;
            calculateTrendData();
        }

        /**
         * Identifies the x field attribute in the dataSet.
         * */
        public function get xField():String {
            return this._xField;
        }

        private var _yField:String;

        public function set yField(value:String):void {
            this._yField=value;
            calculateTrendData();
        }

        /**
         * Identifies the y field attribute in the dataSet.
         * */
        public function get yField():String {
            return this._yField;
        }

        /* The trend data */
        /**
         * First value of the trend line.
         * */
        public var firstTrendValue:*;

        /**
         * Last value of the trend line.
         * */
        public var lastTrendValue:*;
        /* (end) The trend data */

        /**
         * The intercept point of the regression line and the y axis.
         * */
        public var intercept:Number;

        /**
         * The slope of the regression line.
         * */
        public var slope:Number;

        /**
         * Generates the first and last value of the regression line used as trend line.
         * */
        protected function calculateTrendData():void {
            if (dataSet != null && dataSet.length > 1 && xField != null && yField != null) {
                var n:Number=dataSet.length;

                var xArray:Array=getFieldArray(xField);
                var yArray:Array=getFieldArray(yField);

                var sumX:Number=sumArrayValues(xArray);
                var sumY:Number=sumArrayValues(yArray);

                var xxProduct:Number=xyProduct(xArray, xArray);
                var xyProduct:Number=xyProduct(xArray, yArray);

                slope=((n * xyProduct) - (sumX * sumY)) / ((n * xxProduct) - (Math.pow(sumX, 2)));
                intercept=(sumY - (slope * sumX)) / n;

                //The first value of the trend line.
                var dataSetFirstXValue:*=(dataSet.getItemAt(0))[xField];
                firstTrendValue=getTrendFirstValue(dataSetFirstXValue);

                //The last value of the trend line.
                var dataSetLastXValue:*=(dataSet.getItemAt(dataSet.length - 1))[xField];
                lastTrendValue=getTrendLastValue(dataSetLastXValue);

            }
        }

        /**
         * Returns an object with the first value of the trend line, using the calculated intercept and scope values.
         * */
        protected function getTrendFirstValue(xValue:*):Object {
            var resultObject:Object=new Object();
            resultObject[xField]=xValue;
            if (isNaN(xValue)) {
                resultObject[yField]=intercept + (slope * 1);
            } else {
                resultObject[yField]=intercept + (slope * xValue);
            }

            return resultObject;

        }

        /**
         * Returns an object with the last value of the trend line, using the calculated intercept and scope values.
         * */
        protected function getTrendLastValue(xValue:*):Object {
            var resultObject:Object=new Object();
            resultObject[xField]=xValue;
            if (isNaN(xValue)) {
                resultObject[yField]=intercept + (slope * dataSet.length);
            } else {
                resultObject[yField]=intercept + (slope * xValue);
            }

            return resultObject;

        }

        /**
         * Returns an object with the xField and yField value in the trend line from a xField given value.
         * */
        protected function getTrendValueFromX(xValue:Number):Object {
            var resultObject:Object=new Object();
            resultObject[xField]=xValue;
            resultObject[yField]=intercept + (slope * xValue);

            return resultObject;
        }

        /**
         * Creates an array composed of the axis (field) values. If the axis values are not numbers (dates or strings), we use their position in the array instead.
         * */
        protected function getFieldArray(field:String):Array {
            var result:Array=new Array();
            var i:int;
            var currentValue:Number;
            for (i=0; i < dataSet.length; i++) {
                currentValue=(dataSet.getItemAt(i)[field]);
                if (isNaN(currentValue)) {
                    currentValue=i + 1;
                }
                result[i]=currentValue;
            }
            return result;
        }

        /**
         * Returns the sum of all the elements of the array.
         * */
        protected function sumArrayValues(values:Array):Number {
            var sumResult:Number=0;
            var i:int;

            for (i=0; i < values.length; i++) {
                sumResult+=values[i];
            }

            return sumResult;
        }

        /**
         * Returns the dot product between two arrays.
         * */
        protected function xyProduct(xValues:Array, yValues:Array):Number {
            var sumResult:Number=0;
            var i:int;

            for (i=0; i < xValues.length; i++) {
                sumResult+=(xValues[i] * yValues[i]);
            }

            return sumResult;
        }

        /**
         * Draws a trend line from the firts to the last generated values (firstTrendValue & LastTrendValue).
         * */
        public function drawTrendLine(canvas:CartesianDataCanvas):void {
            if (firstTrendValue != null && lastTrendValue != null) {
                drawContinousLine(canvas, firstTrendValue[xField], firstTrendValue[yField], lastTrendValue[xField], lastTrendValue[yField]);
            }
        }

        /**
         * Draws a continuos line over the canvas.
         * */
        protected static function drawContinousLine(canvas:CartesianDataCanvas, firstXValue:*, firstYValue:Number, lastXValue:*, lastYValue:Number):void {
            //You can customize your trend line style here.
            canvas.lineStyle(2, 0xFFAAFF, .75, true, LineScaleMode.NORMAL, CapsStyle.SQUARE, JointStyle.MITER, 2);

            var firstCCV:CartesianCanvasValue=new CartesianCanvasValue(firstXValue);
            var lastCCV:CartesianCanvasValue=new CartesianCanvasValue(lastXValue);

            canvas.moveTo(firstCCV, firstYValue);
            canvas.lineTo(lastCCV, lastYValue);
        }
    }
}


+
This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License. Permissions beyond the scope of this license, pertaining to the examples of code included within this work are available at Adobe.

Report abuse

Related recipes