Odds On Flex

Flex Sparkline

February 8th, 2010

Sparklines are described by their inventor, Edward Tufte, as “data-intense, design-simple, word-sized graphics”. They are an ideal tool for displaying trends for single entries within large data sets, for example, stock prices. As well as the standard miniature line series, Tufte’s original specification introduces a ternary sparkline, a.k.a. win-loss sparkline, where small columns are used to indicate “win” or “loss” and no column to indicate “draw”.

This article is intended to present, and distribute, Flex 3 implementations of sparklines, win-loss sparklines and another common variation, column sparklines. The code and documentation can be obtained from here. The following is an example showing some of the variations available:

Sparkline Examples

The Sparkline, TernarySparkline and ColumnSparkline classes are all implemented using core Flex functionality, i.e. there are no dependencies. They all implement IListItemRenderer and IDropInListItemRenderer interfaces, so can readily be used as standalone components or as item renderers in a DataGrid or other list control. Based on the original specification, the Sparkline allows the user to optionally show a normal range, x-axis min and max points and y-axis min and max points. All of these features and the line itself can be styled. Similarly, the TernarySparkline and ColumnSparkline allow the user to specify the “zero” value, and style positive, negative and zero styles independently. A final feature of the sparklines is that they will update automatically based on dataProvider updates, as shown in the following example, which plots a constantly-updating, randomly generated series of numbers:

Updating sparkline example.

It is worth noting that there are a number of alternative Flex implementations available. However, they all have further dependencies, potentially making them more heavyweight. For example, Tom Gonzalez and Andrew Trice both present implementations using the Degrafa graphics framework. Because of their use of Degrafa, these implementations inherently allow users to easily use any number of extravagant styling. Although potentially initially appealing, this goes against the good data visualisation practices suggested by experts such as Stephen Few and Tufte himself; “eloquence through simplicity”[ref]. Lucas Pereira and Sherlock Informatics both present implementations using the Flex Data Visualization library. The charts contained in the library are wonderfully feature-rich and flexible. However, this means that they are also rather heavyweight components and, therefore, using a large number of these as item renderers in an application is likely to use a lot of memory and/or make the application non-performant. A final potential alternative are the micro-charts available in the BirdEye visualisation library. Much like the examples using the Flex Data Visualization library, these can be perfect in situations where small numbers of instances are present, but again they introduce features that cause them to be more heavyweight than necessary.

The components are being made available under the GNU General Public License and can be obtained from here. The zip also includes some documentation and an swc of components, using the namespace http://www.scottlogic.com/sparkline.

Missing values in Flex charts

April 14th, 2009

An oddness in default behaviour that can throw those new to Flex Charting is that segments in charts that should correspond to a data point are missing. By this I mean charts like those in the following example:
You must have Adobe Flash Player 9 installed to view this application.
When what is actually desired is the following:
You must have Adobe Flash Player 9 installed to view this application.
This is achieved by setting the filterData property on the Series to false, for example:

<mx:LineSeries filterData="false" />

However, it is worth understanding the functionality that ties in with this property to appreciate the potential consequences. Adobe states:

When possible, set the filterData property to false. In the transformation from data to screen coordinates, the various series types filter the incoming data to remove any missing values that are outside the range of the chart; missing values would render incorrectly if drawn to the screen. For example, a chart that represents vacation time for each week in 2003 might not have a value for the July fourth weekend because the company was closed. If you know your data model will not have any missing values at run time, or values that fall outside the chart’s data range, you can instruct a series to explicitly skip the filtering step by setting its filterData property to false. [ref]

What is actually happening under the covers is that the series a collection of “all the data points” and a collection of “all the data points I think I want to draw”. When filterData is true, i.e. by default, the series creates the collection of “all the data points I think I want to draw” by iterating through the collection of “all the data points” and throwing away any that are null or NaN or that fall outside the axes minimum and maximum values. By contrast, when filterData is false, the two collections are identical. Knowing this it is obvious why the segments are missing in the example above: with the LineSeries if a data point is not to be drawn then the line segments to that data point will not be drawn; with the ColumnSeries if a data point is not to be drawn then the column representing that data point will not be drawn.

Performance-wise, it is worth noting that performance gained by the removal of data point filtering step by setting the filterData property to false will be offset against the performance lost by drawing data points that have no bearing on the view of the chart, i.e. data points that are one or two data points removed from those around the axes maximums. As such, the performance gain/loss and appearance gain/loss should be considered on a case by case basis.

An approach that can be used to get the best of both worlds is to extend the applicable series class and override the updateFilter method to ensure that data points that have no bearing on the view of the chart are filtered out, but any others aren’t. This could, for example, amount to filtering out any data points that do not lie in the range of the x-axis and that are not adjacent in the x-axis to a data point that does lie in the range of the x-axis.

An unfortunate note about the filterData property is that there is a bug when setting it to false on a LineSeries no data tips will show on the series (as can be seen in the example above). The bug has been filed with Adobe and has recently been marked fixed, however, the fix has not yet been released. The bug report details the relatively simple but somewhat nasty workaround that can be used in the meantime.

At Scott Logic the functionality relating to the filterData property as well as the property itself have been carefully manipulated to produce the various zooming and panning capabilities in our Hindsight application and the research charts available through our Market Overview pages.

The source code is now available.

Custom data tips in stacked Flex charts

March 24th, 2009

The number of frustrating decisions in Flex’s charting API is minimal, but high up on my list is a strange decision that prevents developers from accessing information that is frequently desirable for custom data tips in stacked area, bar and column charts. The default data tips for stacked charts display, amongst other things, the total of the values for all the series at a specific point along the x-axis and the percentage the highlighted segment forms of this total, as shown in the following example (source code):
You must have Adobe Flash Player 9 installed to view this application.
If you wanted to customise the data tips and still include information such as the total and percentage values then you would be out of luck. This why I consider the decision to give the necessary information protected scope in the relevant classes in the charting API as strange. Fortunately circumnavigating this issue only requires a little bit of dirty work…

But first, a brief diversion to provide a little background…
The code used to create a stacked column chart generally takes the following form:

<mx:ColumnChart type="stacked">
    <mx:series>
        <mx:ColumnSeries />
        ...
    </mx:series>
</mx:ColumnChart>

This is in fact a shortcut syntax. The ColumnChart automatically wraps the array of series into a ColumnSet with type “stacked”, as it is the ColumnSet class that takes care of the stacking and associated behaviour rather than the ColumnChart class. The same pattern occurs in stacked AreaChart or BarChart. Consequently, the following MXML produces the same end result:

<mx:ColumnChart>
    <mx:series>
        <mx:ColumnSet type="stacked">
            <mx:ColumnSeries />
            ...
        </mx:ColumnSet>
    </mx:series>
</mx:ColumnChart>

When wanting to create more complex combinations of stacking, clustering and overlaying in charts, this can only be achieved by explicitly introducing (and nesting) sets – as in the second code snippet. For further details on this see Adobe’s Stacking Charts article.
Diversion over…

To be able to display customised data tips that include information relating to the stacking in a stacked ColumnChart, BarChart or AreaChart we can take one of two approaches. Both approaches involve extending the ColumnSet, BarSet or AreaSet class (depending on the desired chart type) as it is the protected-scoped negTotalsByPrimaryAxis, posTotalsByPrimaryAxis, stackedMaximum and stackedMinimum properties that contain the interesting information. The first approach is to override the formatDataTip method in the extended class, resulting in something along the following lines:

/**
 * ColumnSet extension to introduce custom data tips.
 */
public class ColumnSet extends mx.charts.series.ColumnSet
{
    /**
     * @inheritDoc
     */
    override protected function formatDataTip(hd:HitData):String
    {
        // build up the custom data tip
        var tip:String = "";
        ...
        return tip;
    }
}

However, I think that this approach is slightly short-sighted because it means that for every customisation of the data tips you would create a new extension. The other, and in my opinion more reusable, approach is to expose the aforementioned properties publicly in extensions to the ColumnSet, BarSet and AreaSet classes. By creating these extensions, the standard approach to customising data tips can be used, i.e. a method with the appropriate signature is passed to the chart’s dataTipFunction property. The classes resulting from this approach would be something along the following lines:

/**
 * ColumnSet extension to expose the stack totals for public 
 * use, e.g. in a data tip function.
 */
public class ColumnSet extends mx.charts.series.ColumnSet
{        
    /**
     * @see StackedSeries.posTotalsByPrimaryAxis
     */
    public function get positiveTotalsByAxis():Dictionary
    {
        return posTotalsByPrimaryAxis;
    }
 
    ...
}

The minor downside to this approach is that the shortcut syntax for achieving stacked series, as used in the first example and explained above, cannot be used. The explicit syntax must be used to ensure that the extended version of the relevant StackedSeries extension is used.

The following example uses the approach presented above to provide customised data tips in the chart. The source code for the example includes the necessary extensions to the ColumnSet, BarSet and AreaSet classes, so feel free to use them.
You must have Adobe Flash Player 9 installed to view this application.

Save Flex chart as image

March 4th, 2009

The ability to allow a user to save a Flex chart, or in fact any Flex UI component, as an image has popped up on my radar several times over the last few years.  Solutions to the problem have generally involved producing a pop-up window with the UI component as an image that the user can then save, either by bouncing the information off a server (James Ward – RIA Cowboy and Flex Cookbook) or interacting with JavaScript (Doug McCune).  However, additions made to the framework in Flex 3 combined with new features of Flash Player 10 have made these cumbersome techniques redundant.  It is now possible to provide this functionality directly from your Flex application in two simple steps.

The first step involves capturing the UI component’s bitmap information.  The Flex 3 API introduced the ImageSnapshot class specifically to simplify this process.  The following line of code is sufficient to capture the image data:

ImageSnapshot.captureImage(myChart);

However, we are able to control the image capturing more precisely by using some of the method’s optional parameters. These allow us to specify the target resolution in dots per inch and the image encoder to use (the Flex 3 API provides a PNGEncoder and a JPEGEncoder).  So, for example, the following line of code would capture a chart as a PNG image at a  resolution of 300dpi:

ImageSnapshot.captureImage(myChart, 300, new PNGEncoder());

Now that we have captured the image data all that remains is the second, and last, step: saving the image data to the user’s file-system. Flash Player 10 introduced a number of changes to its security sandbox, principally the ability to programmatically prompt the user
to save a file to their file-system. This is done using the FileReference class, as shown in the following lines of code:

var file:FileReference = new FileReference();
file.save(image.data, "chart.png");

So, putting the steps together results in a method along the lines of the following code snippet:

/**
 * Attempts to save the chart to the user's file-system.
 */
private function saveChart():void
{
    var image:ImageSnapshot = ImageSnapshot.captureImage(myChart, 300, new PNGEncoder());
    var file:FileReference = new FileReference();
    file.save(image.data, "chart.png");
}

The application below shows this code in action. The values in the data grid can be changed,
with the changes reflected in the chart (just to show that I’m not cheating).
You must have Adobe Flash Player 10 installed to view this application.

The source code is now available.