2

I am at an embarrassing standstill in developing a simple educational app in Flash Pro (AS3).

I'm trying to make a simple graph showing vertical peaks when something happens (and a flat line when idle). The graph is realtime since it starts with the program and is updated every other frame. The line starts at one end of the "Graph" object and continues until it reaches the other side (say, over 60s). After it reaches the end, the entire graph starts to scroll to the left to make room for more data, which is added at the end. It is designed to keep on scrolling and recording data forever (until user stops/pauses it).

Here's an example: http://img442.imageshack.us/img442/4784/runninggraph.png

What's the best way to implement such a graph? A simple scrolling graph made up of a baseline and ticks.

Shouldn't be so hard... or so I thought. I tried 4 different ways (two of which proved 100% working but too slow). There must be something better out there! Maybe matrix transformations + bitmapFill?


END OF QUESTION; simplified code follows.


public class Graph extends Sprite
{
    include "constants.as";
    const pixelFactor:Number = (1 / GRAPH_TIMESCALE) * 1050; //60,000ms
    const amountToMove:Number = POLLING_RATE * pixelFactor, inverseTS:Number = 1.0/GRAPH_TIMESCALE;
    var graphHolder:GraphHolder; // A simple (large) sprite with many Graph instances
    var baseLine:Shape = new Shape(), stretched:Boolean = false;
    var apArray:Array = [], apRecycle:Vector.<Shape> = new <Shape>[];
    var apLength:int = 0, firstPeak:Shape;
    //var numberIndicator:TextField = new TextField();

    public function Graph(graphHolder:GraphHolder) 
    {
        //Set variables
        this.graphHolder = graphHolder; 
        mouseEnabled = false; mouseChildren = false;

        addChild(baseLine);
        with (baseLine)
        {
            x = 55; y = 10; // the base-line. Starts 55 pixels out so labels can be added
            graphics.lineStyle(2,0x000000); // Thickness 2, color black
            graphics.lineTo(1050,0);
            scaleX = 0.0005; // base-line starts as a little dot, expands with time
        }
    }

    // When not scrolling, draws idle baseline. When scrolling, drags peaks backwards.
    function drawIdle(timeElapsed:int):void
    {
        if (timeElapsed <= GRAPH_TIMESCALE) baseLine.scaleX = inverseTS * timeElapsed;
        else 
        {
            //if (graphNo < 1) graphHolder.scrollAxis(pixelFactor);
            if (!stretched) { baseLine.scaleX = 1.001; stretched = true; }

            // scroll all peaks
            if (apLength)
            {
                for (var i:int = 0; i < apLength; i++)
                {
                    apArray[i].x -= amountToMove;
                }
                firstPeak = apArray[0];
                if (firstPeak.x < 55) 
                { 
                    firstPeak.visible = false;
                    apRecycle[apRecycle.length] = firstPeak; // peaks are recycled instead of removed
                    apArray.shift(); // apArray is a linked-list Array; less shift cost

                    --apLength;
                }
            }
        }
        //lbd.copyPixels(graphHolder.baseCache,lbr,new Point(timeElapsed * pixelFactor,0), null, null, true);
        //line.graphics.lineTo(timeElapsed * pixelFactor,10);
    }
    function drawAP(timeElapsed:int):void
    {
        // Check for a peak in the recycle array
        if (apRecycle.length)
        {
            firstPeak = apRecycle.pop();
            firstPeak.x = ( (timeElapsed > GRAPH_TIMESCALE) ? 1050 : (timeElapsed * pixelFactor) ) + 54; //55-1
            firstPeak.visible = true;
            apArray[apArray.length] = firstPeak;
        }
        else
        {
            // Line drawn from baseline up 7 pixels.
            var peakShape:Shape = new Shape(); 
            //peakShape.cacheAsBitmap = true;
            addChild(peakShape);
            with (peakShape)
            {
                //cacheAsBitmap = true;
                graphics.lineStyle(2, 0x000000);
                graphics.moveTo(1, 10);
                graphics.lineTo(1, 3);
                x = ( (timeElapsed > GRAPH_TIMESCALE) ? 1050 : (timeElapsed * pixelFactor) ) + 54; //55-1
                apArray[apArray.length] = peakShape;
            }
        }
        ++apLength;
    }

    /* Reset baseline as tiny dot, remove peaks, reset peak array. 
       All peaks in recycle remain to draw from during next cycle. */
    function resetGraph():void
    {
        baseLine.scaleX = 0.0005; stretched = false;
        for each (var peak:Shape in apArray) removeChild(peak);
        apArray.length = 0; apLength = 0;
    }
}

So again, essentially, there should be tickmarks on the graph indicating "actions" that occur in realtime. The graph starts scrolling to the left after it reaches the end of the screen (at N seconds). In other words, it keeps scrolling indefinitely after it hits the edge of its designated space. Here are the 3 ways I've attempted to implement this -- none of them are fast, and some of them have other problems.

  1. Create and draw a new line segment at the corresponding timepoint on the graph -- the baseline segment (tiny horizontal line) and the tickmark (vertical). The graph only goes up to N seconds, but all tickmarks at times N+x are still drawn as if the graph went on forever to the right. I then use a mask for the rectangular area of the graph and literally move the graph to the left over time once it reaches N seconds.

    • This was slow to begin with, but led to incredible slowdown after a few minutes because Flash is apparently still processing the whole graph to the left, off-screen.
    • Drawing line segments directly to the graph object is bad because each segment is apparently an object, and each of these myriad objects has no explicit (or implicit) reference after it is drawn. ("Out of sight, out of mind" == slow and wrong.)
  2. Create a bitmapdata object containing the shape of a tickmark (small verticle line) and the baseline (tiny horizontal line), and every frame copypixel the next bitmap (line segment) into place on the graph object (now also a bitmap). ScrollRect to scroll to the left. Simple, very much like #1 except using bitmapData and copyPixel instead of display objects.

    • This is bad news because after 2048 pixels, no more data can be drawn to the bitmap.
    • Scaling the app caused all the bitmaps to appear rough and jagged, and for gaps to be introduced (despite flagging each bitmap "smoothing on"), likely because of the sheer number of line segments comprising a graph that are all disparate and drawn near other pixel boundaries.
    • Scrollrect cannot be accelerated by the GPU (and the app needs to work on mobile devices)
    • Maintaining concurrent bitmaps, copying and pasting one onto the other to refresh the graph ("blitting"?) is tedious, but it is also very inaccurate at the subpixel level, unlike the vector techniques. Also, the calculations required to either recalculate all peaks from scratch each frame or create, destroy, and merge multiple width-2048 bitmaps in the background may prove more costly in the long run.
  3. Have one Shape, a horizontal line (baseline), "scale up" to fill the graph, then stop (N seconds). Hence, it is the "baseline" since it appears to stretch across the graph with time and then do nothing after N seconds. Each tick atop the baseline, then, is a Shape object and is drawn on top of the baseline. A foreach loop then changes the x coordinates of all ticks until they reach the left of the graph, at which point they are made invisible and added to a recycle vector which will be used, if it contains anything, for any subsequent peaks coming in from the right (drawn at N).

    • Still slow, but much faster than #1 and maintains constant speed when the graph starts scrolling.
    • I can't figure out how to speed up the process. I've tried caching the background and the graph and even the line on top as bitmaps (even tried with cacheasbitmapmatrix), but it seems the plain vector rendering is fastest on the debug player.
  4. Tween. Actually, haven't yet implemented the graph itself with tween, but so far, replacing my enterFrame movement handlers (which change coordinates of objects manually each frame) with Tweens has made all the animations much, much slower. Go figure. So I don't think it will help on the graph... I also can't use TweenLite (licensing).


With #3, I think I'm on the right track, but I fear I just don't know enough about the details of AVM rendering. Should I use bitmaps instead of Shapes for the peaks? Should I use the same technique as #1 with #3's objects (i.e. adding the peaks to a big moving "tickSprite" but still "recycling" the peaks when they go offscreen)? In this case, the containing Sprite, despite absence of ticks offscreen, will continue to be pushed to the left offscreen. Will it still take up resources linearly if it is completely empty to the left of the screen?

Suspected speed culprits in #3: - large number of moving Shapes and the for loops required to move them. - Features of the vectors, bitmaps, and backgrounds. For instance, the Graph object itself is just a simple container for a Background shape (cached as bitmap) which is an alpha 0.75 gray rectangle behind the graph, a bunch of labels and axes (also cached as bitmaps), and the graph shapes (one scaling "line" Shape for the baseline and numerous "tick" Shapes stored in a vector)... - Users can drag normal vector objects behind the Graph, which might kill any performance benefit in caching the graph's children as bitmaps since they have to be rendered in motion over everything behind the alpha 0.75 background shape.

user1649948
  • 651
  • 4
  • 12
  • 2
    have to be honest, your question was too long for me to read all of it... Are you using GPU acceleration? – BadFeelingAboutThis Sep 05 '12 at 21:54
  • Post your source code or fla for the option you currently have implemented. I"m having a hard time visualizing what you're trying to accomplish – BadFeelingAboutThis Sep 05 '12 at 21:57
  • surely this question can be simplified – Ronnie Sep 05 '12 at 22:21
  • 1
    Didn't read the whole thing, but have you tried BitmapData.scroll? – Cay Sep 05 '12 at 22:38
  • Also, an image of the graph might help us understand the question better... – Cay Sep 05 '12 at 22:39
  • Here's a conceptual depiction of the graph in action. The top line is after the program has run for 35 seconds, and the bottom line is after it has run for 90. The graph scrolls to the left after it reaches 60 in order to make room for subsequent data. http://img442.imageshack.us/img442/4784/runninggraph.png As for my question being too long, well -- the question itself is actually just the line in bold (9 words). The rest of my post was explaining what I tried to get it working. – user1649948 Sep 05 '12 at 23:22
  • I've tried both GPU and Direct rendering modes. I've never tried BitmapData.scroll -- it seems like scrollRect where I'm limited to the width of a single bitmapData since it doesn't discard the old data after the graph scrolls left. The thing is, I need the graph to **keep scrolling forever** as time goes on. Chances are it's going to exceed the max width of a single bitmap object if Flash had to render the whole thing (offscreen or otherwise) after a few minutes. – user1649948 Sep 05 '12 at 23:33
  • Instead of using scrollRect, just copy the pixels that are there over a bit. Check out http://flexdiary.blogspot.com/2012/06/endless-scrolling-bathroom.html – Amy Blankenship Sep 06 '12 at 00:31
  • That's actually a wonderful idea, Amy -- that was foremost on my list of priorities when I tried method #2, though, where I found that the bounding rectangle on copyPixel is only accurate in fullpel (a shift this small in magnitude requires at least qpel resolution). Two potential workarounds I haven't tried are: 1) move the peaks according to an absolute function instead of a delta, and 2) use the scrollRect method until the contents are on a fullpel boundary and then perform the shift-copy-paste. I'm worried that 1) will be slow due to calculation overhead and 2) will do double the work... – user1649948 Sep 06 '12 at 00:47

1 Answers1

0

Option 1 is the only viable way.

What you need to do is save your data somewhere (an array/vector) and then dispose your lines/ticks once they are off screen. If the user wants to scroll left, then using your saved data re-create the ticks (same for things spilling out the right when scrolling).

Eventually though you will run out of memory in theory, so you may need to implement local storage techniques like a local shared object with some sort of seed (date-time?) to store your data so it's not always in memory.

EDIT:

Since you don't need to scroll left, it's pretty easy then. Just make each of your 'ticks' it's own display object. (be it Shape or Sprite), then as your scroll check to see if the individual tick is offscreen, if so - dispose of it.

Sample graph class:

public class Graph extends Sprite

    private var container:Sprite;

    public function Graph():void {
        //create your baseline

        //create the container
        container = new Sprite();
        addChild(container);

        this.addEventListener(Event.ENTER_FRAME, drawIdle);
    }

    private function drawIdle(e:Event){
        //move your baseline?

        //scroll container
        container.x += scrollAmount;


        //iterate through all the ticks
        var i:int = container.numChildren;
        var tick:Shape;

        //iterate through the container children backwards (has to be backwards since your potentially removing items)
        while(i--){
            tick = container.getChildAt(i);
            //check if the tick is off screen to the left
            if(container.x + tick.x < 0){
                //if so, remove it so it can be garbage collected
                container.removeChild(tick);
            }
    }

    public function drawAp(timeElapsed):void {
        //create your tick
        var tick:Shape = new Shape(); 

        //draw your tick
        tick.graphics.beginFill(0);
        tick.graphics.drawRect(0,0,tickWidth,tickHeight);
        tick.graphics.endFill();

        //position your tick
        tick.x = whatever;
        tick.y = whatever;

        //add it to your tick container
        container.addChild(tick);
    }

}
BadFeelingAboutThis
  • 14,445
  • 2
  • 33
  • 40
  • Very interesting. In my case, there's absolutely no need (or implemented way) for the user to interact with the graph other than to hide it. That is, only the current window contents are needed, and the data to the left can be safely discarded. – user1649948 Sep 05 '12 at 23:59
  • Oh great, thanks! Would it be fair to condense your suggestion to "add all the peaks to a container Sprite, and recycle/discard each peak once the container moves it offscreen"? Wouldn't the ever-expanding container cause eventual slowdown? I do like the entirety of your suggestion -- it opens up a possibility of graph interactivity I hadn't even considered. – user1649948 Sep 06 '12 at 00:08
  • with this method the container bounds, which is what matters, will always be more or less the bounds of the stage (or less). At some point you could reset the container.x to 0 (and adjust the children accordingly), if there is some hard limit I don't know about in the flash player where a display object's x value can only go so far. – BadFeelingAboutThis Sep 06 '12 at 00:15
  • Concise, elegant, and certainly does the job. Actually, that's quite similar to what I do with #3 (code posted in original post), in which I placed some additional attempts at optimization (baseline stretching, shape recycling). The basic idea is that the interframe handler calls drawIdle every other frame, but drawAP is only called when an event occurs. In each case, the time is passed in to ensure the peak is placed in the correct location. – user1649948 Sep 06 '12 at 00:28
  • I updated my code to show a basic example of your graph class. – BadFeelingAboutThis Sep 06 '12 at 00:46
  • I can't upvote yet (I need a 15 reputation), so I accepted the answer. It's actually a less optimized way of doing what I already did in method #3 -- I was hoping to improve speed further. But you took the time and effort to respond, so there you go! I'm going to look into matrix transformations for a fresh take on the subpixel scrolling issues I'm running into using blitting. Cheers! – user1649948 Sep 06 '12 at 01:06