Wednesday, 29 July 2009

Building Interactive "Over-by-Over" Reports

As mentioned in a related post, I've been granted a developer key for the Guardian Open Platform and have used it to put together a website displaying interactive cricket scores and reports. This post details how I've put this together.

Web Service Back End

The application is in two parts. My initial thoughts were to build this purely on the client side using jquery - the API provides JSON as well as XML response formats. But of course this would mean exposing my developer key to anyone who thought to "view source"... and I expected the Guardian wouldn't be too pleased with that. So instead I set up a web service, that will be called by the client side code AJAX style, in order to protect this information.

I also took this opportunity to simplify the output going to the client side code to what I really needed, in order to ease the complexity of the JavaScript manipulations.

The API provides a means of loading the full details of any piece of content via an ID, and a URL of the form:
http://api.guardianapis.com/content/item/[item]?api_key=[key]

So first step was to find the IDs for each of the matches. I found the easiest way to do this was not to actually use the API explorer, but to find the page on the site, view source and search for
"http://www.guardian.co.uk/email/". The number that comes after this URL (used for the "send to friend" feature), is the ID I wanted.

Having stored these in my web.config file, I built a web service that takes the test match number (1-5), accesses each of the 5 reports (once for each day) and loads them into an XMLTextReader:


    1 string apiKey = ConfigurationManager.AppSettings["APIKey"];


    2 int match = 0;


    3 if (int.TryParse(Request.QueryString["match"], out match))


    4 {


    5     string[] contentItems = GetContentItemsForMatch(match);


    6     foreach (string contentItem in contentItems)


    7     {


    8         string url = "http://api.guardianapis.com/content/item/" + contentItem + "?api_key=" + apiKey;


    9         XmlTextReader reader = new XmlTextReader(url);


   10         ...




As it turns out, although the content is well tagged as XML, the body copy all sits within a single node (though paragraphs are marked up). So I extracted the node I needed:

    1 if (reader.NodeType.ToString() == "Element" && reader.Name == "body")


    2     body = reader.ReadElementContentAsString();




And then carried out some string manipulations to tidy up the content and extract it into a JSON collection of the form:
{overs: [
{
day : 1,
innings : 1,
over : 1,
score : "England 2-0 (Strauss 1, Cook 1)",
batsmen: [
{name : "Strauss", score : 1},
{name : "Cook", score : 1}
],
text : "It'll be Mitchell Johnson to bowl The First Ball..."
}
]}
I discovered the API seems to have some form of restriction on frequency of requests, as I would sometimes find my request for day 4 or 5 would fail with a "403 forbidden" message. To get around this I added some error trapping via try/catch/wait/retry, and also some caching to prevent hitting the Guardian services too often.

Finally, having built up the JSON string using a StringBuilder, I output it to the response stream:

    1 Response.ContentType = "text/js";


    2 Response.Write(sb.ToString());





Client Side Code


The front-end of the application consists of a single page, with interaction with the web service achieved using JavaScript, and in particular, jquery.

The core of this is a function wired up to the button click that retrieves the JSON formatted data for the selected match, and parses it to pull out the over objects that correspond to the selected batsman's innings.
function setUpButtonClick() {
$("#show-button").click(function() {
var ddl = $("#match")[0];
var match = ddl.options[ddl.selectedIndex].value; //get selected match
$("#wait-icon").show(); //show ajax "please wait" graphic
$.getJSON("/interactive-obos/services/get-overs/?match=" + match, function(json) {
hideOverDetails(); ///hides any previously selected over information
displayInningsDetails(json.overs); //plots the innings details on the chart
$("#wait-icon").hide();
});
});
}
The function displayInningsDetails() takes the JSON collection over over information and plots it on the chart. This is just a div with a grid background image, and absolutely positioned elements are placed on top of it using jquery append statements.
function displayInningsDetails(overs) {
var ddl;
ddl = $("#batsman")[0];
var batsman = ddl.options[ddl.selectedIndex].value;
ddl = $("#innings")[0];
var innings = parseInt(ddl.options[ddl.selectedIndex].value);
var inningsOver = 0;
$("#display-panel").empty();
for (var i = 0; i < overs.length; i++) {
var j = getBatsmanInOver(overs[i],batsman,innings);
if (j > 0) {
inningsOver++;
$("#display-panel").append("<div class=\"over\" title=\"Runs: " + overs[i].batsmen[j-1].score + "\" style=\"left: " + ((inningsOver - 1) * OVER_WIDTH) + "px; height: " + ((overs[i].batsmen[j-1].score) * RUN_HEIGHT) + "px\"></div><div class=\"text\"><strong>Day " + overs[i].day + "</strong><strong>" + overs[i].score + "</strong>" + overs[i].text + "</div>");
}
}
applyOverHovers();
}

function getBatsmanInOver(over,player,innings) {
if (over.innings == innings && over.batsmen[0].name.toLowerCase() == player.toLowerCase())
return 1;
else
if (over.innings == innings && over.batsmen[1].name.toLowerCase() == player.toLowerCase())
return 2;
else
return 0;
}

And finally, the hover function to display the over information is set up. The text has already been written into a hidden div within the chart, so the steps are to find the one next to the visible bar, extract the HTML and write it to the panel.

function applyOverHovers() {
$("#display-panel div.over").hover(function() {
$("#text-panel div").html($(this).next("div.text").html()).animate({opacity: "show"}, "slow");
}, function() {
$(this).next("div.text").animate({opacity: "hide"}, "fast");
});
}

And that's broadly it! If you'd like to dig-in further, please feel free to download the source code and of course let me know your comments.

2 comments:

  1. Congratulations! A fun project with tight code.

    I just wonder if there is a way to extract the over number from the OBO and put that in the right column above the text.

    Also, perhaps there should be a note if there is no content for a given player in a given innings in a given match. At the moment the loading animation runs and then just stops when it finds nothing to pull.

    Thanks for the cool app!

    Zip

    ReplyDelete
  2. Thanks, and both good points... have added these features if anyone is still re-living the summer success.

    ReplyDelete