Visualizing Contacts in a Card Layout
I’ve always been interested in using Flash for visualization of business data. In fact, I evaluated Flash 2 as an alternative technology for the Java applets I had been employing to that end. The other day, an opportunity presented itself to play around with contact data. My first thought was that of mirroring the physical world of business cards, and the following layout management example was born.
Note: This example was built using Flex Builder, but is an ActionScript project, which does not use any of the Flex framework itself. I did actually try this using Flex, but encountered problems with swapping the depths of display objects. Flex has a tendency to inject display objects that aren’t part of your code to manage the viewport. These rogue objects interfere with some of the mechanics of the layout. If you decide to try it in Flex, and get it working, please let me know.
While this is a pure ActionScript project, I’m not particularly fond of those applications which don’t take screen real estate into consideration. My first task then was to find a pattern that worked best for manually managing the resize of the stage. I fumbled around here a bit and came up with a general pattern that seemed to work, which was later improved upon by an exchange with Lee Brimelow.
package { import comp.CardLayout; import comp.Toolbar; import flash.display.Sprite; import flash.display.StageAlign; import flash.display.StageScaleMode; import flash.events.Event; public class ContactManager extends Sprite { public var cards:CardLayout = null; public var toolbar:Toolbar = null; public var endpoint:String = null; public function ContactManager() { super(); endpoint = loaderInfo.parameters.endpoint; stage.scaleMode = StageScaleMode.NO_SCALE; stage.align = StageAlign.TOP_LEFT; addEventListener( Event.ADDED_TO_STAGE, doAdded ); } public function init():void { cards = new CardLayout( 15, Toolbar.HEIGHT + 20 ); addChild( cards ); toolbar = new Toolbar(); toolbar.addEventListener( ToolbarEvent.FILTER, doFilter ); toolbar.addEventListener( ToolbarEvent.FILTER_COMPLETE, doFilterComplete ); addChild( toolbar ); layout(); stage.addEventListener( Event.RESIZE, doResize ); } public function layout():void { var colors:Array = [0x34383D, 0x2E3136]; var alphas:Array = [1, 1]; var ratios:Array = [0x00, 0xFF]; var matrix:Matrix = new Matrix(); matrix.createGradientBox( stage.stageWidth, stage.stageHeight, 90 * ( Math.PI / 180 ) ); graphics.clear(); graphics.moveTo( 0, 0 ); graphics.lineStyle( 1, 0xFF0000, 0 ); graphics.beginFill( 0xFF0000 ); graphics.beginGradientFill( GradientType.LINEAR, colors, alphas, ratios, matrix, SpreadMethod.PAD ); graphics.drawRect( 0, 0, stage.stageWidth, stage.stageHeight ); graphics.endFill(); toolbar.y = stage.stageHeight - Toolbar.HEIGHT; } public function doAdded( event:Event ):void { init(); } public function doResize( event:Event ):void { layout(); } } }
In most cases, you’ll want your components to manage their own awareness of the stage size. This means that they’ll need to have been added to the stage before they can access the appropriate properties (e.g. Stage.stageWidth). Simply calling addChild() alone doesn’t mean that the display object is suddenly on the stage. The best way to confirm this then is to listen for the Event.ADDED_TO_STAGE event. Then you know you’ve been added to the stage and can measure your component against those values.
Once you know you’ve been added to the stage, you can begin the baseline initialization of the additional display objects that you’ll be leveraging. Generally speaking you won’t have to worry about positioning here, just instantiation and setting non-layout properties. Add the end of the initialization routing, you’ll want to call a method to perform layout, and then listen to the Event.RESIZE event on the stage. This is another reason you need to listen to being added to the stage before you go initializing and laying out objects.
The layout method performs basic layout based on the size of the stage. When the resize event is fired, it too calls the layout method to update the position of the objects. I tend to do a lot of drawing in this method as well (at least in this example). Also, don’t forget to set the Stage.scaleMode and Stage.align properties to avoid distortion of your content. For those of you interested, at a very high level, this is basically what Flex is doing to manage layout.
Basic layout management in place, I wanted to present the contact data as business cards. I studied a number of business cards that I collected over the years, and came up with a set of common fields that I felt adequately encompassed a large percentage of use-cases.

I have an slightly different version of this application that will be used behind the firewall here at Adobe, and I’ve modeled very closely my digital business card representation against a physical business card issued by Adobe. For the purposes of this example, I’ve largely just put the data on the cards. I’ll leave the decision about which fields appear, and where to you.

I’m using ColdFusion in this application to query the database and order by last name. The results are fairly limited in size, so I’ve chosen to use XML to describe my data. If you’re using a larger data set, then you may be inclined to use remoting and AMF. I should note however that I’ve not significantly tested the scalability of this user interface, so your mileage may vary.
<cfquery datasource="kevin" name="contacts">
SELECT *
FROM contact
ORDER BY last
</cfquery>
<rolodex count="<cfoutput>#contacts.RecordCount#</cfoutput>">
<cfloop query="contacts">
<contact id="<cfoutput>#id#</cfoutput>" email="<cfoutput>#email#</cfoutput>">
<name first="<cfoutput>#first#</cfoutput>" last="<cfoutput>#last#</cfoutput>" />
<phone office="<cfoutput>#office#</cfoutput>" mobile="<cfoutput>#mobile#</cfoutput>" fax="<cfoutput>#fax#</cfoutput>" />
<role title="<cfoutput>#title#</cfoutput>" group="<cfoutput>#group#</cfoutput>" department="<cfoutput>#department#</cfoutput>" />
<company name="<cfoutput>#company#</cfoutput>">
<address><cfoutput>#address#</cfoutput></address>
<box><cfoutput>#box#</cfoutput></box>
<city><cfoutput>#city#</cfoutput></city>
<state><cfoutput>#state#</cfoutput></state>
<zip><cfoutput>#zip#</cfoutput></zip>
</company>
<comment><cfoutput>#comment#</cfoutput></comment>
</contact>
</cfloop>
</rolodex>With data acquired, and [digital] business cards created, the next step is to manage their layout and how they fit on the screen. I talked earlier about the pattern I use for this, but it took me some time to really fine tune the display. I never knew how many business cards would be displayed, or the width or height of the display, simply the size of my digital business card.
The math I came up with looks to position as many cards horizontally as possible with a slight gap. The layout also looks to fit the cards vertically with as much gap as possible. When there’s no room left for a gap, then the cards overlap equally. Finally, to make accessing individual cards easier, the even rows of cards are inset a degree of spacing. Note also that while the first and last columns push as far to the edges as possible, while accounting for margin spacing, the columns between the first and the last, center their business cards.
public function layout( isFilter:Boolean = false ):void { var card:BusinessCard = null; var cardHeight:Number = 0; var cardWidth:Number = 0; var ccol:Number = 0; var children:Number = 0; var coff:Number = 0; var cols:Number = 0; var crow:Number = 0; var hbox:Number = 0; var hspace:Number = 0; var newx:Number = 0; var newy:Number = 0; var rows:Number = 0; var vbox:Number = 0; var vspace:Number = 0; // Initial sizing and baseline space measurements cardHeight = BusinessCard.HEIGHT + ( spacing * 2 ); cardWidth = BusinessCard.WIDTH + ( spacing * 2 ); hspace = stage.stageWidth - ( 2 * spacing ); vspace = stage.stageHeight - top - bottom; cols = Math.floor( hspace / cardWidth ); // Number of children to display for( var c:Number = 0; c < numChildren; c++ ) { card = getChildAt( c ) as BusinessCard; if( !card.filtered ) { children = children + 1; } } // How many rows and how wide is a column rows = Math.ceil( children / cols ); hbox = hspace / cols; // Do not worry about vertical alignment if only one row if( rows == 1 ) { vbox = 0; } else { // How high is a vertical row // Accounts for overlap if insufficient space vbox = Math.floor( ( vspace - 191 ) / ( rows - 1 ) ); } // Horizontal column offset and centered in column coff = ( hbox - cardWidth ) / 2; // Iterate over all children for( c = 0; c < numChildren; c++ ) { card = getChildAt( c ) as BusinessCard; // Do not layout children that are filtered out if( card.filtered ) { continue; } // New Y position for child newy = top + ( crow * vbox ); // Offset odd rows if( crow % 2 == 1 ) { newx = spacing; } else { newx = 0; } // Un-indent columns if more than one row // Don't un-indent first column (0) if( rows > 1 && ccol > 0 ) { newx = newx - spacing; } // Increment column counter ccol = ccol + 1; // Determine x-position based on column index // If first column if( ccol == 1 ) { newx = newx + spacing + coff; // Move to next row if there is only space for one column if( cols == 1 ) { ccol = 0; crow = crow + 1; } // If last column } else if( ccol == cols ) { newx = newx + stage.stageWidth - 335 - spacing - coff; ccol = 0; crow = crow + 1; // All other columns } else { newx = newx + ( hbox * ( ccol - 1 ) ) + coff + ( spacing * 2 ); } // Animate to new position in case of filtering if( isFilter ) { // TweenMax.to( card, 0.5, {x: newx, y: newy} ); card.x = newx; card.y = newy; } else { card.x = newx; card.y = newy; } } }
The last major step was in allowing the user to filter the data, and narrow the number of cards that are visible in the display. In the version of this application deployed here at Adobe, I filter based on a selected set of products. For this application, I’ve chosen simply to go with a filter by last name of the contact. I wanted to put a combo box in that would allow you to chose your own field, but ran out of time. Filtering in general is pretty easy - if a card matches, then it is marked and hidden.
public function filter( value:String ):void { var card:BusinessCard = null; var last:String = null; for( var c:Number = 0; c < numChildren; c++ ) { card = getChildAt( c ) as BusinessCard; last = card.contact.last.toLowerCase(); if( last.indexOf( value.toLowerCase() ) == 0 ) { // TweenMax.to( card, 0.5, {alpha: 1} ); card.visible = true; card.filtered = false; } else { // TweenMax.to( card, 0.5, {alpha: 0} ); card.visible = false; card.filtered = true; } } }
Marking the object is actually pretty important to the layout. I didn’t just want to leave the cards where they were, but rearrange and update the layout based on how many cards matched the given criteria. For the internal version, I actually animate the removal/addition of cards, and their location in the updated layout. In this example, the cards just appear/disappear and relocate instantly. The hooks are there though, and the comments to roll in animation should you see fit.
The end result is a pretty fun representation of displaying contact data as business cards. There’s filtering and layout management, which also works when you resize the browser (as should be expected, in my opinion). What’s not addressed, and what’s not addressed in most data visualizations that operate on business data, is how to create, update and delete contacts and their respective business card representations. This in and of itself would be fun to explore, but is beyond the scope of what I had time for this round.
Circling back on the depth management statement from earlier, when the cards overlap significantly, the labels become obscured. In order to address this, I allow the cards to surface themselves when the mouse rolls over them. They are in turn put back in the stack when the mouse leaves the surface of the card. This is accomplished through swapping depths in the display list, and is where Flex caused me problems. I don’t doubt that can be remedied however, but cut and paste didn’t work, so I just stuck with what worked.
If you want to play around with the code yourself, to see if you can get it to work in Flex, the source code (both ActionScript and ColdFusion) for this project are available for download. A sample application is also available to play around with for your viewing pleasure.
