Sunday, June 5, 2011

Labeling the top 5 results on a graph with jqPlot

Hey everyone,


As you can guess by the title, we're back with more on jqPlot! Last time I talked about jqPlot I had wanted to use tick marks in the Highlighter plugin for axes where the data was non-numerical. The whole reason of using the highlighter in the first place was because some of our graphs have large numbers of data points, so we have to hide the axes labels or it looks terrible.

A large data set (129 points). 
So we hide the axes labels and it looks better, but now you can't tell what any of the points are. We added the highlighter plugin so that at least when you hover over a data point, you see its value. That's helpful, but what if you wanted to, for example, print this graph out?

We decided to label the graph for the points with the top X (where X=5 at the moment) y-values on the graph. In the end we get this:

Labeling the top 5 results looks pretty nice. 
Now if you print it out, at least you see the most important values on the graph.

The Plugin

So in order to accomplish this I started from an existing plugin (Highlighter since that's what I'm familiar with). I'm going to explain how I changed the highlighter plugin to accomplish what I needed, but you don't have to start with it at all. There are a couple things we have to change to make it work:

  • Modify the postPlotDraw hook so that it sets up labels for the number of points we want to draw the label for.
  • Make a call to our own function that will determine which points to draw labels
  • Modify the showTooltip function so that it uses more than one label 
None of these are really hard, so let's just start with the first step.

The first thing is to add two new variables to the plugin object itself:

$.jqplot.TopResultDisplay = function(options) {
        this._tooltipElements = []; 
        this.numberOfPoints = 5; 

        /*   the rest of the options .... */ 
};

TopResultDisplay is the name of the plugin. _tooltipElements is empty right now but will contain a number of div elements that represent the labels. numberOfPoints determines how many top points we want to label.

Now we need to modify the postPlotDraw() function so that it creates those divs. It will also make a call to our function that will eventually draw the labels. After modifying it, the postPlotDraw() function looks like this:

// called within context of plot
    // create a canvas which we can draw on.
    // insert it before the eventCanvas, so eventCanvas will still capture events.
    $.jqplot.TopResultDisplay.postPlotDraw = function() {
        this.plugins.TopResultDisplay.highlightCanvas = new $.jqplot.GenericCanvas();
        
        this.eventCanvas._elem.before(this.plugins.TopResultDisplay.highlightCanvas.createElement(this._gridPadding, 'jqplot-highlight-canvas', this._plotDimensions));
        this.plugins.TopResultDisplay.highlightCanvas.setContext();
        
        var p = this.plugins.TopResultDisplay;
        
        for(i =0;i<p.numberOfPoints;i++)
        {
            elem = $('<div class="jqplot-highlighter-tooltip" style="position:absolute;display:none"></div>');
            p._tooltipElements[i] = elem;
            this.eventCanvas._elem.before(elem);
        }        
        
        drawLabelsForPoints(this); 
    }; 

The changes I made start at line 10: we simply create a number of div elements and insert them on the _tooltipElements array.

Then we add a call to drawLabelsForPoints(this) which will be our label drawing function that takes in a plot object.

Now let's write that function. When I was changing the Highlighter plugin this function was originally handleMove, but I ended up changing all of the functionality. The function itself is not that exciting:

function drawLabelsForPoints(plot) 
    {
        var hl = plot.plugins.TopResultDisplay; 
        
        var sortedData = []; 
                
        for(i =0;i<plot.series[0].data.length;i++)
        {            
            var newPoint = new Object(); 
            newPoint.data = plot.series[0].data[i];
            newPoint.seriesIndex = 0; 
            newPoint.gridData = plot.series[0].gridData[i];             
                        
            sortedData.push(newPoint);                                        
        }        
        
        sortedData.sort(function(a, b) { return b.data[1] - a.data[1]; }); 
                
        for(i=0;i<hl.numberOfPoints;i++)
        {            
            showTooltip(plot, plot.series[0], sortedData[i]);
        }    
    }

First we create a new array called sortedData. Then we fill this array with all of our unsorted points, but we use the format that showTooltip of Highlighter expects. The data variable refers to the actual data array of the form [x,y]. seriesIndex is the series that the point is on, and gridData refers to the points x/y pixel coordinates on the canvas. You might notice that I'm only using one series here since all of our graphs have only one series. You could however modify it to support more than one if needed.

After filling the array, we sort in place using a function that sorts the data in descending order based on the y-value. Finally, we call showTooltip on the top results with the number of points that was specified.

The next step is to modify showTooltip to use more than one label element. To do this I changed a small part of the showTooltip function at the top to look like this:

    var hl = plot.plugins.TopResultDisplay;
        
        if(!hl.enabled) return;        
        var elem = hl._tooltipElements.splice(0, 1)[0];         
        if(!elem) return; 
        
        if (hl.useAxesFormatters) {
        /* .. . the rest of the function */

The important line is line 4, where we make the call to _tooltipElements.splice. This removes one element from the array of divs and returns it, which means that as long as it returns non-null we still have labels to use. After that the rest of the code remains the same and references the elem object to draw on.

The very last step is to remove the line

$.jqplot.eventListenerHooks.push(['jqplotMouseMove', handleMove]);
from the top of the code. This line wires up the handleMove method to be called whenever the mouse is moved over the canvas.

After that, we're done with changing the code. All that's left is to enable the plugin. We do that just as any other plugin. Below is the configuration for the plugin as we have it on our graphs page.

TopResultDisplay:
{
 tooltipAxes: 'xy',
 useAxesFormatters:true,
 tooltipLocation: 'n', 
 numberOfPoints: 5
}, 

And the results again for reference:

So that's it! If you have any questions/comments/etc. feel free to leave a comment.

No comments:

Post a Comment