Search Flex Samples

Flex Easy Custom Charts

This is kind of a long post, so if you’re not the reading type, I’ll give you the goods first. But come back and read the post to understand what’s so special about it.
DataDrawingCanvas samples
The Background
When I first sat down to write the Flex Charting components, I started by looking at as many different examples of embedded charts as I could find, both flash and otherwise. It didn’t take long to realize that charts are like snowflakes: A) No two are the same, and B) they make your tongue feel funny if you can catch one on it. At a high level, many seem to convey the same information — they’re variations on a bar chart, or an area chart…but almost every single one has some little extra bit of information they need to display, some little extra customization, that makes them unique. The customer will tell you that all they want is ‘a simple line chart,’ but when you get down to details, it’s almost a guarantee that it’s going to be fairly different from the last ’simple line chart’ you had to create.
The lesson to be learned was that we weren’t going to be successful building a set of charts that we thought might solve everyone’s problems. And we probably weren’t even going to come close with the amount we could get done in the first couple of releases. So instead, we took the platform approach: rather than building a set of charts, we would first build a platform for data visualization, and build the specific chart types on top.
And judging from the number of questions I see on the flex lists every day, it looks like we were right. Many posts come through every day, and while a few of them ask about basic features or unfortunate bugs in the prebuilt chart types, I find myself answering many of them with the same answer: ‘the charts don’t support that out of the box, but you can build a custom renderer/series/chart to do that!’
The Problem
Which is all nice and good. But the truth is, that building a custom chart element is not necessarily a simple task. With power comes complexity, and to build even a simple chart extension, there’s a fair amount of learning to be done about how the journey is made from data values to pictures on the screen.
A lot of the time, the complexity in the code reflects a real complexity in the task at hand…building charts that ‘do the right thing’ in all cases, with all kinds of data, in a reasonably fast way, is not an easy thing. But sometimes a developer is just looking to draw a line at value 37, or add a label next to the monthly sales total for ‘DVD players.’ But even in a simple case, the developer must make a non-trivial learning investment to figure out where exactly to draw on screen.
The Revelation
A few days ago I saw one of these posts come by. A developer had tried simply drawing into the chart, using the flash graphics API, and was confused about how they were supposed to deal with changes to the min/max values of the chart. I crafted a typical reply, that went something like this:
“Hi Mr. Developer. to get the effect you’re trying to achieve, you’ll want to create a custom chart element. The problem with just drawing into the chart is that you’re drawing in pixel coordinates, when really you care about the data values, not the pixel values. In essence, you want to draw in data space. So you need to convert data coordinates into pixel coordinates, using the axes of the chart. Whenever you want to ‘draw in data coordinates.’ you need to wirte a custom chart element.”
OK, nice, well written, friendly, and informative. But reading back over the response, I was struck by what I had said: all the developer wanted to do was draw into data space. Why not just let him?
The Solution
And hence, this little experiment was born. I’ve spent my nights over the past week or so working on a new custom chart component that for now I’m calling the DataDrawingCanvas.
The idea with this component is to let you manipulate the contents of a chart using all the basic functionality you get with the flash display list, but to do it with data coordinates instead of x and y pixel coordinates. So you can moveTo, and you can lineTo. you can drawRect, beginFill, and even curveTo. You can add new children to the chart…sprites, shapes, and bitmaps…buttons, checkboxes, even other charts…In theory, anything you can do with either the display list API or the graphics API, you can now do in a chart.
So how does it work?
Let’s use lineTo as an example. The flash graphics object has a function called lineTo that looks something like this:public function lineTo(x:Number, y:Number):void;
which you would call, passing the x/y pixel values you want to draw the line to. The DataDrawingCanvas, in turn, has a function called lineTo that looks like this:public function lineTo(hDataValue:*, vDataValue:*):void;
which you also call to draw a line to the passed in values. But in this case, the hDataValue and vDataValue parameters are mapped against the two axes of the chart and converted into pixel values before being drawn. All the other graphics APIs are there as well, with similar parameter mappings.
Now, like in the flash graphics API, the DataDrawingCanvas is a retained mode graphics API. That means that once you’ve drawn a line into the canvas, it will stay there on screen until you call clear(). The difference is, this line stays at the data coordinates rather than the screen coordinates…if the min/max values of the chart changes, the line will move to the correct mapped screen location.
Now what kind of values should pass in to the lineTo function? Well, just as with the values in a chart series, that really depends on the type of Axes in your chart. If your verticalAxis is a default LinearAxis, then your vDataValue parameter should be a number. But if your horizontalAxis is a category axis, then you can pass in the name of a category as your horizontal value, and it will get mapped correctly. Which means you can tell the DataDrawingCanvas to draw a box from “California,120″ to “Massachusets,270″!
Adding children
Adding components or other display objects to the DataDrawingCanvas works similarly. You add them in the usual way, by calling addChild() (or specifying them in MXML). But once they’re there, you want to position them in data space rather than screen space. The DataDrawingCanvas uses a constraint system similar to the Canvas container. But rather than setting the constraints directly on the component, you set them by calling the updateDataChild function on the DataDrawingCanvas:
function updateDataChild(child:DisplayObject, dDataConstraints:Object):void;
That second parameter is a set of name/value pairs with the constraint values you’d like applied to the component. The same ones work as with normal constrains: you can set left,right,top,bottom,horizontalCenter, or verticalCenter. Again, these are data coordiantes, so to bind the top right corner of a label to the position “California”,270, you would do the following:var l:Label = new Label();
l.text = "Last Month";
dataCanvas.addChild(l);
dataCanvas.updateDataChild(l, { top: 270, right: "California" } );
In theory at least, it’s that easy.
Pixel Padding
Now sometimes it can get a little more complicated than that. Imagine you want to use the drawing API to draw a 10 pixel radius ellipse centered at the point “Wyoming”,150. You can’t just subtract 10 from the center of the circle to tell it where to draw, because you want the center to be specified in data values. How do you subtract 10 from “Wyoming”?
In order to do that, you need to subtract 10 pixels from “Wyoming” after it’s been converted to pixel coordinates. But the whole point of this experiment is to allow you, the developer, to not have to think about when and how to convert to pixel coordiantes. So we need something a little more sophisticated.
So here’s how the DataDrawingCanvas handles that. Every API that takes a data coordinate, can also take a data coordinate/pixel offset pair. In this case, you would call the drawEllipse function like so:dataCanvas.drawEllipse( [ "Wyoming",-10], [150,-10], [ "Wyoming",10], [150,10] );
The DataDrawingCanvas will convert each first value into pixels and combine each pair before it does its drawing.
When to use it
So should you do all your custom chart work this way? Definitely not. While it works pretty well, building your own custom elements from scratch will always be more performant, and there’s still plenty of more complex cases that you’ll need to provide more fine grained logic for. But this is perfect for a widce variety of simple customizations that people ask for every day.
The Payoff
Now, I make a habit of asking for feedback from people when I post examples to this blog, but this time I really mean it. This is prototype code. If it’s something you find useful and would want to see polished up for real production use, let me know.
OK, enough talking. Here’s the demo and source:
DataDrawingCanvas samples
View the source
download the source

1 comments:

Anonymous said...

Hi

This is amazing work. I would love to see production code.

Rich T

Related Flex Samples

Learn Flex: Flex Samples | Flex Video Tutorials Flex Examples