By John Fronckowiak
November 7, 2007
If you want an audience...start a fight! – Irish Proverb
The AIM Fight! service is best described by its developers at http://www.aimfight.com/whatisaimfight.php:
Using a complicated algorithm, AIM Fight crawls through the depths of the Internet to answer the all-important question that plagues us all: How popular am I, right this second? As shown in Figure 1, your score is the sum of the current number of people online who have you listed as a buddy, out to three degrees. This means the score is constantly changing, and the winner of the battle will constantly change with it.
Figure 1. How AIM Fight! measures your popularity
AIM Fight! is available through the web site http://www.aimfight.com, shown in Figure 2. Your AOL screen name and your challenger's screen name are entered on the page. Clicking the Fight button displays the results, shown in Figure 3.
Figure 2. The AIM Fight! web site
Figure 3. The Fight results
The displayed score shows how many AOL Instant Messenger (AIM) connections you have relative to other users.
AIM Fight! and Mac OS X Dashboard Widgets
Apple introduced Dashboard and widgets to the Mac world with the release of Mac OS X 10.4 Tiger. Dashboard widgets are actually mini Web 2.0 applications. Essentially, Dashboard widgets are comprised of an HTML page that describes the layout of the front and back of the widget, and a JavaScript file that contains the code. It's important to realize that widgets aren't full-blown applications; rather, they are small and focused in purpose. The AIM Fight! widget you will build in this article is a Mac OS X Dashboard widget; however, the development techniques I review here can be applied to any Web 2.0 application development that uses Asynchronous JavaScript and XML (Ajax).
Using the AIM Fight! Widget
Dashboard widgets have a front, which is the primary user interface, and a back, which is typically used for configuration. The front of the AIM Fight! widget is shown in Figure 4.
Figure 4. The AIM Fight! widget front
Clicking the AIM Fight button performs a fight against the specified challenger. Clicking the My Rank button captures the score of the primary fighter, which can be graphed.
The back of the widget is shown in Figure 5. The primary fighter buddy name is the AIM ID of the primary fighter. The primary fighter rank and fight results (wins, losses, and ties) are saved. The widget can automatically check your ranking on a daily, weekly, or monthly basis. Clicking the Rank History and Fight History buttons display charts, shown in Figures 6 and 7, respectively. The Reset History button will clear the saved history graph data. And finally, clicking the Done button returns the user to the front of the widget.
Figure 5. The AIM Fight! widget back
Figure 6. The Rank History graph
Figure 7. The Fight History graph
Video 1 demonstrates the AIM Fight! widget in action.
Video 1. The AIM Fight! widget in action
Dissecting the AIM Fight! Widget Interface
Figure 8 illustrates the primary elements of the AIM Fight! user interface. The result1, result2, and tie div elements are invisible. They are used to show the results of a fight or a ranking. The fighter images are named img1 and img2. The user2 search box automatically saves a history of challengers.
Figure 8. Important AIM Fight! interface elements
The AIM Fight! Web Service
In addition to the web site interface to AIM Fight!, a web service interface is also provided. The web interface is called using the following URL:
http://www.aimfight.com/getFight.php?name1=
{fighter 1 user name}&name2={fighter 2 user name}
The web interface takes two parameters: {fighter 1 user name} is the name of the primary fighter, and {fighter 2 user name} is the name of the challenger. Upon return, the web service returns an &-separated list of values. For example:
&success=1&score1=195&score2=11&oscore1=0&oscore2=0&height1=99&height2=5
Three of the return values are important: success, score1, and score2. Upon a successful call, success=1 is returned. The values score1 and score2 are the current ranking of {fighter 1 user name} and {fighter 2 user name}, respectively. The fighter with the highest ranking is considered the winner of the fight.
The Prototype JavaScript Library
The Prototype Javascript Framework is a JavaScript framework created by Sam Stephenson that provides an Ajax framework and other utilities. The Plotr, Prototip, and Script.aculo.us libraries, also used in AIM Fight!, use the Prototype framework. Shown in the upcoming code samples, a common Prototype function is the $() function. The typical method for identifying an element is document.getElementById("id_of_element"); the Prototype Framework $() function reduces this to $("id_of_element"). The Prototype framework provides a number of extensions to the JavaScript DOM (Document Object Model) that are documented at http://prototypejs.org/learn/extensions.
The frameworks are included in the HTML page of the widget using the <script> tags shown in Listing 1.
Listing 1. The <script> tags required to include the necessary frameworks
<script type="text/javascript" src="lib/prototype/prototype.js"
charset="utf-8"></script> <script type="text/javascript" src="lib/scriptaculous/scriptaculous.js"
charset="utf-8"></script> <script type="text/javascript" src="lib/prototip/prototip.js"
charset="utf-8"></script> <script type="text/javascript" src="lib/plotr.js"
charset="utf-8"></script>
Fighting and Ranking
The AIM Fight! widget uses the AIM Fight! web service for two functions: fighting and ranking. In fight mode, the AIM buddy name of the primary fighter is passed as the {fighter 1 user name} parameter and the challenger's AIM buddy name is passed as the {fighter 2 user name} parameter.
The call to the web service is made through the JavaScript XMLHttpRequest API. XMLHttpRequest is used to establish an asynchronous communication channel between the widget and the web service, and is an essential technique for Ajax-based web development.
Clicking the My Ranking or AIM Fight buttons results in the getRank or getFight, respectively, JavaScript methods being called. Shown in Listing 2, getRank and getFight call the doRequest method. The fightURL is retrieved from the widget preferences, and the URL necessary to call the AIM Fight! web service is constructed. When a ranking is being performed, only the primary fighter's buddy name is passed, because we are only interested in the ranking value that is returned, and not in the result of a fight. The global variable fightReq is set to the XMLHttpRequest object that is created for the call. If a call is already in progress it is aborted before another call is made.
Listing 2. The getRank and getFight functions // ***** // Function: getRank // Purpose: Configure the AIM Fight URL to get the ranking of
// the user identified by // screename // // Author: Fronckowiak // ***** function getRank(screenName) { // if there's a pending request - abort it! if (fightReq) { fightReq.abort(); } // call doRequest with the proper AIM Fight! URL - this request is // processed asynchronosuly // upon successful completion - call the setRank function to process the results fightReq = doRequest( loadPref("fightURL") + "name1=" + screenName + "&name2=" +
screenName, setRank ); } // ***** // Function: getFight // Purpose: Configure the AIM Fight URL for a battle between screenName1
// and screenName2 // // Author: Fronckowiak // ***** function getFight(screenName1, screenName2) { // if there's a pending request - abort it! if (fightReq) { fightReq.abort(); } // call doRequest with the proper AIM Fight! URL - this request is processed // asynchronously upon successful completion - call the setFightResult // function to process the results fightReq = doRequest( loadPref("fightURL") + "name1=" + screenName1 +
"&name2=" + screenName2, setFightResult ); }
The doRequest method is shown in Listing 3. doRequest takes two parameters: the URL to call, and a function to call when the asynchronous XMLHttpRequest returns. For the getRank call to doRequest, the setRank method is called upon return. For the getFight call to doRequest, the setFightResult method is called upon return. The setRank and setFightResult methods store the data necessary for graphing the results. The tie div is updated to show the status information of the call.
Listing 3. The doRequest function // ***** // Function: doRequest // Purpose: Asynchronously call the specified url, GET the results,
// upon result return // call the specified callback function // // Author: Fronckowiak // ***** function doRequest( url, callback ) { // in the area we display tie results... // display a message indicating we are loading the AIM Fight! results if(inAutoCheck) { $('tie').innerHTML = "Loading..."; } else { $('tie').innerHTML = "Auto Rank Check..."; } // make the Loading message appear with a script.aculo.us animation Effect.Appear('tie'); // create new XMLHttpRequest var request = new XMLHttpRequest(); // ensure we don't get cached results... request.setRequestHeader( "Cache-Control", "no-cache" ); // Call Aim Fight! service request.open( "GET", url ); // On return - call callback function request.onload = function( e ) { callback( e, request ) } // send request request.send( null ); // return request object return request; }
The Plotr JavaScript Library
The Plotr JavaScript library was built using the Prototype JavaScript Framework. I reviewed a number of the JavaScript libraries before I chose Plotr. Most of the other libraries used SVG (Scalable Vector Graphics) to display the charts. This was not suitable for a Dashboard widget, because only the <canvas> tag is supported. This considerably narrowed my choices. In the end my choice to use Plotr was based on native <canvas> tag support, and JavaScript Object Notation (JSON) support for chart and configuration data.
Storing Graph Data Using JSON
When the web service request returns, processing the returned data is done by the setRank or setFightResult method as specified in the call to doRequest. As shown in Listing 4, the primary purpose of these methods is to store the results for later graphing. After a successful web method call, the setRank and setFightResults method parse the response text to retrieve the scores. When the data has successfully been parsed, the saveRank and saveFightResults methods are called to store the data for later graphing. The saveRank method saves the ranking score of the primary fighter. The saveFightResults method compares the rankings of the primary fighter and the challenger. If the primary fighter's score is greater than the challenger's, it is registered as a win. If the primary fighter's score is less than the challenger's, it is registered as a loss. If the scores are equal, then it's a tie.
The graphing data is stored in a JSON data structure that is stored in the widget's preferences. The Prototype library methods toJSON and evalJSON are used for conversion between strings and JavaScript objects. In addition to saving the rank score, the saveRanking method also saves the date of the ranking for display on the x-axis of the graph. Only MAX_RESULTS (which is set to 10) rankings are saved for ranking. This requires additional array processing to push the oldest data out of the x-axis and graph data arrays.
Listing 4. The setRank, saveRank, setFightResults, and saveFightResults methods
// *****
// Function: setRank
// Purpose: Return point from asynchronous call the to the AIM Fight! service.
// Upon success status is 200, parse the service request for the ranking, then
/ save it in the rankings graph JSON data structure. If an error occurred –
/ display it!
//
// Author: Fronckowiak
// *****
function setRank( e, request ) {
// upon successful return status is 200
if(fightReq.status == 200) {
// clear the status message
$('tie').innerHTML = "";
Element.hide('tie');
// retrieve the response
var response = request.responseText;
var match;
// on success - success=1 is returned
if ( ( match = response.match( /success=1/ ) ) != null ) {
// retrieve first score (they will both be the same for rank)
match = response.match( /score1=(.*?)&/ );
// save the ranking for later graphing
saveRanking(parseInt(match[1]));
}
} else {
// AIM Fight called failed - display an error message
PlaySound("alarm");
// display error message
$('tie').innerHTML = "Error: " +fightReq.status.toString()
+ " Accessing AIM Fight Service!";
// enable buttons so the user can try again...
$('aimFight').object.setEnabled(true);
$('myRank').object.setEnabled(true);
$('user2').enabled = true;
}
// release XMLHttpRequest
fightReq = null;
}
// *****
// Function: saveRanking
// Purpose: Retrieve the ranking graph JSON data from preferences,
// and add the current
// ranking to the data set and axis.
// Make sure the data set does not grow beyond MAX_RESULTS items
// - otherwise the
// display will be squished. When complete save the JSON data
// in preferences
//
// Author: Fronckowiak
// *****
function saveRanking(ranking) {
// load ranking graph JSON data from preferences
var options = loadPref("rankGraphOptions").evalJSON();
var rankData = loadPref("rankData").evalJSON();
// when did we do the last ranking?
var lastRanking = parseInt(loadPref("lastRanking"));
// get today's date..
var today = new Date();
// have we saved rankings before?
if(lastRanking > 0) {
// save the latest ranking
if(rankData.rankings.length >= MAX_RESULTS) {
// more than maxResults? remove first entry so we can add
// at then end...
rankData.rankings.shift();
options.axis.x.ticks.shift();
// adjust tick and rankings data item indices...
// the v item indicates the ticks position in the data set
// we must move all ticks down one notch as we popped the
// first one
for(var i = 0; i < (MAX_RESULTS - 1); i++) {
// move the tick and ranking indicies down
options.axis.x.ticks[i].v = i;
rankData.rankings[i][0] = i;
}
}
// add rank data - first clone an existing object
var newRank = rankData.rankings[0].clone();
// set the parameters
newRank[0] = rankData.rankings.length;
newRank[1] = ranking;
// push it on to the end of the array
rankData.rankings.push(newRank);
// add axis data
var newTick = Object.clone(options.axis.x.ticks[0]);
newTick.v = options.axis.x.ticks.length;
// the tick label is the date in the form mm/dd
newTick.label = (today.getMonth() + 1).toString() + "/"
+ today.getDate().toString();
// push the new tick on to the ed
options.axis.x.ticks.push(newTick);
} else {
// save the first ranking!
rankData.rankings[0][0] = 0;
rankData.rankings[0][1] = ranking;
options.axis.x.ticks[0].v = 0;
// the tick label is the date in the form mm/dd
options.axis.x.ticks[0].label = (today.getMonth() + 1)
.toString() + "/" + today.getDate().toString();
}
// save preferences...
savePref("rankGraphOptions",Object.toJSON(options));
savePref("rankData",Object.toJSON(rankData));
savePref("lastRanking",today.getTime().toString());
// don't display results in auto check...
if(!inAutoCheck) {
// display ranking data... in the same area a win is displayed
fightResults = loadPref("primaryFighter") + "'s Current Ranking is " +
ranking.toString();
// one the display effect is finished - call the displayWin function
// to display the reset button
Effect.SwitchOff('img1', {duration: 2.0, afterFinish:displayWin});
} else {
// perform a manual reset...
resetClick(null);
inAutoCheck = false;
}
}
// *****
// Function: setFightResults
// Purpose: Return point from asynchronous call the to the AIM
// Fight! service.
// Upon success status is 200, parse the service request
// for the ranking,
// then save it in the fight results graph JSON data
// structure. If an
// error occurred - display it!
//
// Author: Fronckowiak
// *****
function setFightResult( e, request ) {
// upon successful return status is 200
if(fightReq.status == 200) {
// clear the status message
$('tie').innerHTML = "";
Element.hide('tie');
// retrieve the response
var response = request.responseText;
var match;
// on success - success=1 is returned
if ( ( match = response.match( /success=1/ ) ) != null ) {
// retrieve first score
match = response.match( /score1=(.*?)&/ );
var score1 = parseInt(match[1]);
match = response.match( /score2=(.*?)&/ );
// retrieve the second score
var score2 = parseInt(match[1]);
// create the result message and save it in fightResults
if(score1 > score2) {
fightResults = loadPref('primaryFighter') + " Wins! " +
score1.toString() + " To " + score2.toString();
} else if (score2 > score1) {
fightResults = $('user2').value + " Wins! " +
score1.toString() + " To " + score2.toString();
} else {
fightResults = "Tie! Both Fighters Score " + score1.toString();
}
// save fight results - only one parameter will be true -
// won, loss, or tie
saveFightResults((score1 > score2),(score1 < score2),(score1 == score2));
} else {
// AIM Fight called failed - display an error message
PlaySound("alarm");
// display error message
$('tie').innerHTML = "Error: " +fightReq.status.toString() +
" Accessing AIM Fight Service!";
// enable buttons so the user can try again...
$('user2').enabled = true;
$('aimFight').object.setEnabled(true);
$('myRank').object.setEnabled(true);
}
}
// release XMLHttpRequest
fightReq = null;
}
// *****
// Function: saveRanking
// Purpose: Retrieve the ranking graph JSON data from preferences,
// based on won, loss, or
// tied, update the results graph data, and turn off the losing
// fighter to
// display the results
//
// Author: Fronckowiak
// *****
function saveFightResults(won, lost, tied) {
// load fight results JSON data from preferences
var resultsData = loadPref("resultsData").evalJSON();
var options = loadPref("resultsGraphOptions").evalJSON();
// based on won, loss, or tie, update results data and display message
// in appropriate area...
// use script.aculo.us switch off animation to turn off image of
// losing fighter
if (won) {
// after fighter is turned off - call displayWin to display
// result message
Effect.SwitchOff('img1', {duration: 2.0, afterFinish:displayWin});
resultsData.results[0][1]++;
} else if (lost) {
// after fighter is turned off - call displayLoss to display
// result message
Effect.SwitchOff('img', {duration: 2.0, afterFinish:displayLoss});
resultsData.results[1][1]++;
} else {
// tie message are displayed in the center
$('tie').innerHTML = fightResults;
Effect.Appear('tie');
Element.show('reset');
resultsData.results[2][1]++;
}
// save results JSON data to preferences
savePref("resultsData",Object.toJSON(resultsData));
savePref("resultsGraphOptions",Object.toJSON(options));
}
Creating Ranking and Fight Statistics Graphs
The process of saving the graph data is much more complex than actually displaying the graphs. Displaying the graphs is quite straightforward--retrieve the graph configuration and data point JSON data from the widget's preferences, a new Plotr chart is created, the data set is added, and the chart is rendered. The rankHistoryChart and fightHistoryChart functions are shown in Listing 5; they are called when you click the Rank History and Fight History buttons. Sample graphs are shown in Figures 5 and 6.
Listing 5. The rankHistoryChart and fightHistoryChart methods // ***** // Function: rankHistoryClick // Purpose: When the back Rank History button is clicked -
// display the ranking graph. // // Author: Fronckowiak // ***** function rankHistoryClick(event) { // display the graph area Element.show('graphBackGround'); // clear the canvas clearCanvas("graphDiv"); // retrieve the JSON ranking graph options and data
// from preferences var options = loadPref("rankGraphOptions").evalJSON(); var dataset = loadPref("rankData").evalJSON(); // create a new line chart var graph = new Plotr.LineChart('graph',options); // add the data set graph.addDataset(dataset); // render the graph graph.render(); // display the graph title $('graphTitle').innerHTML = "Ranking History"; } // ***** // Function: fightHistoryClick // Purpose: When the back Fight History button is clicked -
// display the fight history // graph. // // Author: Fronckowiak // ***** function fightHistoryClick(event) { // display the graph area Element.show('graphBackGround'); // clear the canvas clearCanvas("graphDiv"); // retrieve the JSON fight history graph options and
// data from preferences var options = loadPref("resultsGraphOptions").evalJSON(); var dataset = loadPref("resultsData").evalJSON(); // create a new bar chart var graph = new Plotr.BarChart('graph',options); // add the data set graph.addDataset(dataset); // render the graph graph.render(); // display the graph title $('graphTitle').innerHTML = "Fight History"; }
Animation with the Script.aculo.us JavaScript Library
When the results of a ranking or fight are returned, the fighter images are animated as part of displaying the results. You will use the Script.aculo.us JavaScript library for animation. As shown in the saveRanking and saveFightResults functions, the Effect.SwitchOff method is called to dramatically remove the image of the losing fighter or display the ranking data. The SwitchOff method takes two parameters: the name of the image to "turn off" and the configuration data. The configuration data used for the SwitchOff method in the AIM Fight! widget consists of two items, the duration (in seconds) of the effect, and a method to call after the effect has finished. Listing 6 shows a typical call to the Effect.SwitchOff method.
Listing 6. An Effect.SwitchOff method call
Effect.SwitchOff('img1', {duration: 2.0, afterFinish:displayWin});
The Prototip JavaScript Library
You will use the Prototip JavaScript library to display tool tip descriptions when the user moves the mouse over a user interface element, shown in Figure 9. You add the tool tips using the addToolTips function, shown in Listing 7. The addToolTips function is called when the widget is loaded.
Figure 9. A tool tip example
Listing 7. The addToolTips method
// *****
// Function: addToolTips
// Purpose: Add a tool tip for the challenger search box
//
// Author: Fronckowiak
// *****
function addToolTips() {
new Tip(user2, "Enter Challenger AIM Buddy Name");
new Tip(fighterName, "Enter the Primary Fighter AIM Buddy Name");
new Tip(autoCheckList,"Select the Automatic Ranking Check Interval");
new Tip(soundCheck, "Turn Sound On or Off");
}
Automatic Ranking
The user can specify an interval to perform automatic rankings. Each time a ranking is performed, manually or automatically, the time is stored in the widget's preferences. When the widget is loaded or shown again on the Dashboard, the current time is compared to the time of the last ranking. If the interval is greater than that specified to perform an automatic ranking check, then a ranking is performed. Automatic rankings do not display the typical animation and results of a manual ranking. The rankingAutoCheck function, shown in Listing 8, performs the automatic ranking check. The daysBetween function, also shown in Listing 8, returns the number of days between the last ranking and the current time.
Listing 8. The rankingAutoCheck and daysBetween functions // ***** // Function: daysBetween // Purpose: Calculate the number of days between today and
// specified date/time passed in // seconds // // Author: Fronckowiak // ***** function daysBetween(fromTime) { // Current Date var nDate = new Date(); // Current Time in UTC var nTime = nDate.getTime(); // Calculate Difference var bTime = Math.abs(nTime - fromTime) // Round to Days return Math.round(bTime / DAY); } // ***** // Function: rankingAutoCheck // Purpose: Check to see if the time since the last ranking
// is more than the auto check // interval...if so perform a silent ranking check to
// store the data point... // // Author: Fronckowiak // ***** function rankingAutoCheck() { // when did we do the last ranking? var lastRanking = parseInt(loadPref("lastRanking")); // what is the required interval? var interval = loadPref("autoCheck"); // perform n auto check flag var doCheck = false; switch(interval) { case "Daily": // 1 or more days since last check? doCheck = daysBetween(lastRanking) >= 1; break; case "Weekly": // 7 or more days since last check? doCheck = daysBetween(lastRanking) >= 7; break; case "Monthly": // 30 or more days since last check? doCheck = daysBetween(lastRanking) >= 30; break; default: doCheck = false; break; } // should we do an auto check? if(doCheck) { // set auto check flag! inAutoCheck = true; // simulate a rank button click event... myRankClick(null); } }
More
Please also visit my blog on the AOL Developer Network. I'll be discussing more AOL technologies, applications of those technologies to the Mac, and much more!
Resources
The source code for this article is available to download in the file .
- AIM Fight!
- The Original AIM Fight! Widget
- The Prototype JavaScript Framework
- The Script.aculo.us JavaScript Framework
- The Plotr JavaScript Library
- The Prototip JavaScript Library
- The Complete JavaScript Reference
Special thanks to AOL's Kevin Lawver for the idea to re-imagine his original AIM Fight! widget, and to Becky Straka of Straka-Digital for essential help with the boxer design and graphics.

nice app : )
Bst Rgds,
Michael B.