Thursday, April 14, 2011

Enabling the jqPlot highlighter to use tick labels instead of tick indices.

Background

So I'm a big fan of jqPlot, which we've been using in our project for our graphing page. Right now it's coming along pretty, well, we even have Real Data now.

Real Data.
Now, one "gotcha" you have to watch out for in jqPlot is how non-numeric data points are rendered. For example, let's say we have a plot of cities versus year. Our x-axis is simple, it's just numeric year values. On the

y-axis however, we have city names. Now I'd expect a decent graphing library to be handle non-numeric axes just as well as numerical ones. It would be "good enough" to just display the data points in no particular order, but even better if you could specify a custom comparator. Right now we handle that server-side and just send a pre-sorted list to the client for jqPlot to render.

The Problem

The way jqPlot handles these text data points is a little awkward (unless I'm missing something). Instead of specifying a point such as (1986, "Boston"), you specify (1986, 0) and then have a list of tick labels, where ticks[0] = "Boston."

So a little weird, but not deal-breaking. However, if you try and use one of jqPlot's plugins called the Highlighter, things don't work so well. Highlighter enables displaying of the x/y values when you hover the mouse over a data point on the graph. Unfortunately, if your data point is (1986, 0), it will display "1986, 0" instead of "1986, Boston." Not good.

As far as I can tell, it's not possible to use highlighter to display string values instead of the tick index. So, being bored and not wanting to study for a chemistry class, I dived into the jqPlot highlighter source to change that.

The Source Code

After spending some time in the source code and experimenting through Firebug, the relevant function responsible for displaying the data point is called showTooltip. This function first sets up a list of y-values (since a series can have more than one y-value), and then adds the x-values depending on the options specified.

For example, the relevant line of code to display the y values of the data point is:
ystrs.push(yf(yfstr, neighbor.data[i]));
 Where neighbor.data[i] is the actual value of the data point, yf is a function that formats the string to be displayed, and yfstr is the "format string" which follows the conventions similar to sprintf.

How do we change this so that it will display the tick mark value instead of just the y tick index? Well, it turns out that showTooltip is passed a series object, which contains the ticks array already. The exact name of the array is series._yaxis._ticks. There is also one for the x axis.

How do we know the tick index? Our y data point will indicate it, so that the line above can become:
ystrs.push(yf(yfstr, series._yaxis._ticks[neighbor.data[1]].label));
 Where neighbor.data[1] is the first y-value. Since we don't want this to happen all the time (just for non-numeric data points), we can add a few parameters to the highlighter plugin to specify whether to enable displaying of tick labels or not. In our situation, we know when to enable it because numeric data points have no associated ticks array that's sent by the server. Some similar modifications for the x-axis give us the results we want:

jqPlot modified Highlighter plugin in action
Success!

Now you ask, why go through all this hassle? Well, our particular reason was that we can have too many data labels (such as 50 cities) so the client will cut off displaying them. This way, the user can still hover over interesting data points and see what the values are.

For anyone interested, the modified highlighter plugin is hosted on my website. The relevant Highlighter parameters to set are useXTickMarks and useYTickMarks.

10 comments:

  1. Hey Christ, sounds very interesting and is exactly what I need right now, but I can seem to get it running, so I guess I'm missing out on some crucial point.
    Which plugins are to be activated. Only highlighter or tooltips also? Could you post the complete configuration for an example?

    Thanks a lot,
    Phil

    ReplyDelete
  2. Hey Philipp,

    Below us the necessary code in the call to $.jqplot to get the highlighter working (using the jqplot.highlighter.mod.js file from my website):

    highlighter:
    {
    tooltipAxes: 'xy',
    useXTickMarks: true,
    useYTickMarks: true
    },

    axes:
    {
    xaxis:
    {
    ticks:xticks
    },
    yaxis:
    {
    ticks:yticks
    }
    }

    Note we also have to enable plugins before the call using this line:
    $.jqplot.config.enablePlugins = true;

    I just uploaded a small update so that you can always leave useXTickMarks and useYTickMarks true, where as before you had to explicit set them to false for the axis with numeric data or sometimes the highlighter would not work (maybe that helps you?).

    For reference, here's an example of what our x-axis data and tick array look like, sent from the server in JSON format:

    "data":[[0.0,1],[1.0,1],[2.0,1]]
    "xticks":[[0,"Aarhus"],[1,"Liverpool"],[2,"Long Beach"]]


    -- Chris

    ReplyDelete
  3. That was fast, thanks a lot. I managed to get it running now, though I had to make some adjustments to the source code myself. Nonetheless, your clue that the whole data is actually passed to the plugin script made my day. Thanks a lot, again.

    ReplyDelete
  4. Hi Chris, I found your post and this answers my need now, but it seems that I didn't set it up properly thus I could not see the right label. Below is what I did, can you help where my mistakes or do I miss anything.

    $.jqplot.config.enablePlugins = true;
    $(document).ready(function(){

    s1 = [<%=values%>];
    ticks = [<%=labels%>];

    plot1 = $.jqplot('chart1', [s1], {

    seriesDefaults:{
    renderer:$.jqplot.BarRenderer,
    rendererOptions:{barWidth:35, highlightMouseDown: true },
    pointLabels: { show: false }
    },
    axes: {
    xaxis: {
    tickRenderer: $.jqplot.CanvasAxisTickRenderer ,
    tickOptions: {
    angle: -90,
    fontSize: '12pt'
    },
    tickInterval : '10',
    renderer: $.jqplot.CategoryAxisRenderer,
    ticks: ticks,
    label: '<%= chartBy.replace("_", " ").toUpperCase()%>',
    labelOptions:{
    enableFontSupport:true,
    fontFamily:'Verdana',
    fontSize: '12pt'
    }
    },
    yaxis:{
    min:0,
    pad:1.2,
    tickOptions:{formatString:'%d'},
    label: 'Lots On Hold',
    autoscale:true,
    labelRenderer: $.jqplot.CanvasAxisLabelRenderer,
    labelOptions:{
    enableFontSupport:true,
    fontFamily:'Verdana',
    fontSize: '12pt'
    }
    }

    },

    highlighter:
    {
    tooltipAxes: 'xy',
    useXTickMarks: true,
    useYTickMarks: true
    }
    });
    });

    ReplyDelete
  5. Hello,

    I did some searching -- and as far as I can tell the highlighter didn't support bar charts to begin with, so that may explain the problem as the changes I made were minor.

    Someone else had the same problem and created their own tooltip over the chart, see
    this post on stackoverflow. Hope that helps you out.


    -- Chris

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. Hi !

    Here is my source code, I used your modified pluggin but Firebug give me some errors like "af is undefined".

    I need your help Chris :)

    Thx


    var graphe = $.jqplot('graphe', [tabAVGs], {
    axesDefaults: { pad : 0 , show: false , showMark: false, showTicks : false },
    axes: { xaxis: { min : 1 , max : 13 , numberTicks: 13 , ticks: [tabAxeX] }, yaxis: { min : minTab , max : maxTab, numberTicks: Math.ceil(maxTab-minTab)*5 , ticks: [tabAVGs] } },
    seriesDefaults: { color: '', shadow: true , shadowAngle: 0 , shadowOffset: 0 , shadowDepth: 2 , shadowAlpha: 0.1 , showMarker: true , lineWidth: 4, markerOptions : { size : 3 , shadow:false} },
    highlighter: {show: true , sizeAdjust: 5 , fadeTooltip : false, tooltipAxes: 'xy' , useXTickMarks: true, useYTickMarks: false },
    cursor: { style: 'crosshair', show: false, showTooltip: true , followMouse: true},
    grid: { drawGridLines: true , gridLineColor: '#E8E8E8', background: '#FAFAFA', borderColor: '#E8E8E8', borderWidth: 1 , shadow: false }
    });

    ReplyDelete
  8. Updated highlighter to use jqplot 1.0.1.r1096

    https://gist.github.com/2422033

    ReplyDelete
  9. Scott, I'm using jqPlot version 1.0.7 and your link doesn´t work for me so to fixed it I had to change

    xstr = xf(xfstr, series._xaxis._ticks[(neighbor.data[0])-1].label);

    by

    xstr = xf(xfstr, series._xaxis._ticks[(neighbor.data[0])*2-1].label);

    Thanks!

    ReplyDelete