Using jQuery to fix CFCHART's tooltip behavior

Categories: HTML/ColdFusion, jQuery

If you've ever tried using the "mouseover" tipStyle in <cfchart /> with an image, you may have noticed there are several issues with the JavaScript that the WebCharts3D engine generates:

  1. The position of the tooltips isn't always accurate--if you're got parent elements that are absolutely position from a relative element, the position is drastically mis-calculated
  2. If your chart happens to be on the right edge of the screen, the tooltips will push off to the right of the screen (both clipping content and creating horizontal scrollbars.)

Here we can see an example of the tooltip running off the screen:

CFCHART Tooltips run off screen

You can see the scrollbar and how the tip goes off the edge of the screen. Obviously, this isn't very usable. To fix the problem, we're going to have to override the native mouse handler functions. Unfortunately, this isn't particularly easy to fix because when the <cfchart /> is executed, it embeds an external call to a JavaScript file which looks something like:

<script language="javascript" src="/CFIDE/GraphData.cfm?graphCache=wc50&graphID=script.js"></script>

This makes overriding the functions a bit tricky. You can override the functions during the window.onload event, but what happens is you're loading your chart via AJAX? Plus, why load JS you have no intention on using?

The solution is to strip out the <script /> call. You can do this by wrapping your <cfchart /> call inside the <cfsavecontent /> tag. This will give you a string containing the HTML you'll need to embed in the page. You can then use a regular expression to strip the <script /> from the output before writing it to the output stream. What you end up with is some source code that looks like:

<cfsavecontent variable="sChartOutput">
     <!---// put your cfchart code here //--->
     <cfchart width="1" height="1" />
</cfsavecontent>
<!---// we need to output the chart, but remove the call for the external JS library //--->
<cfoutput>#reReplace(trim(sChartOutput), "<script[^>]+>
</script>", "", "all")#</cfoutput>

By using a regular expression we're able to completely remove the reference to external script that contains the mouse handlers. Now we've got to replace the functionality with code that actually works.

The WebCharts3D script file contains a number of functions, but there are only two functions that are actually used by the HTML generated: xx_set_visible() and xx_move_tag().

The xx_set_visible() function handles hiding/showing the tooltip and the xx_move_tag() function handles positioning the tooltip on the screen.

Since my site is already using jQuery, I'm going to leverage jQuery to handle the positioning of the tooltip. In the new code I'm going to:

  • Move the tooltip element (a <table /> tag) as a direct descendant to the <body /> tag. I do this to simplify positioning of the element—since we don't have to worry about positioning of any of the parent elements.
  • Set the tooltip element's visibility to "visible"—since it's hidden by default (once again we only need to do this the first time we view a tooltip.)
  • Calculate the edges of the screen (with some padding) to make sure my tooltip always stays in the viewport. We don't want our tooltip to ever be cropped or overlap off the screen. If the tooltip would run off the bottom of the page, we'll put the tooltip at the top of the page. If the tooltip would run off the right edge, we'll make sure it can't move any further than the very right edge of the screen (with some padding.)

So what does our code look like? Here it is:

<!---// we need to replace the WebCharts3D JS scripts with ones that actually work //--->
<script type="text/javascript">
// on first show, we need to move to the body
var __xx_set_visible = {};
function xx_set_visible(imgId, tipId, e, show){
    // get the table we're going to show
    var $tip = $("#" + tipId);
    if( !__xx_set_visible[tipId] ){
        // move to the body and make visible
        $tip.appendTo("body").css("visibility", "visible");
        __xx_set_visible[tipId] = true;
    }
    $tip[show ? "show" : "hide"]();
    // make sure we place the tip in the correct location
    xx_move_tag(imgId, tipId, e);
}
function xx_move_tag(imgId, tipId, e){
    // get the table we're going to show
    var $tip = $("#" + tipId);
    // get the scroll offsets
    var scroll = {top: $(window).scrollTop(), left: $(window).scrollLeft()};
    // if we're IE we need to create the e.pageX/pageY events    
    if( !e.pageY ){
        e.pageY = e.clientY + scroll.top;
        e.pageX = e.clientX + scroll.left;
    }
    var pos = {top: e.pageY + 20, left: e.pageX + 10}; // add padding for cursor
    var tip = {width: $tip.outerWidth() + 10, height: $tip.outerHeight() + 10}; // add padding for edge
    var screen = {right: scroll.left + $("body").width(), bottom: scroll.top + $(window).height()};
    // if we're going to be off the screen, adjust the position
    if( pos.left + tip.width >
screen.right ){
        // don't move past most right of screen
        pos.left = screen.right - tip.width; // pos.left - tip.width || screen.right - tip.width - 10;
    }
    if( pos.top + tip.height > screen.bottom ){
        // don't move past most right of screen
        pos.top = pos.top - tip.height - 15; // since we're moving tip above we need adjust for the original padding we add
    }
    // position the
    $tip.css(pos);
}
</script>

That's all there is to it! Now our tooltips will never run off the edge of the page! Here's the result of our new custom mouse handlers:

CHCART with fixed tooltip!

Comments

Joshua Scott's Gravatar This is a great script. Thanks. What styling do you have for your tool tip? I like the gray text and the transparent bg. If you can point me in the right direction that would be awesome.

Thanks.
Dan G. Switzer, II's Gravatar @Joshua:

I can't recall what is being used, but there are lots of options available via the Webcharts 3D designer (i.e. via the Webcharts XML) that aren't always available via the CF tags.
Steve's Gravatar Many thanks Dan, this was very useful!
maramandan's Gravatar Hi Dan

I have a normal chart ( type="horizontalbar") Is there any way by which I could put the X axis on the top ? Sort of like primaryXAxisplacement = Top|Bottom. I didn't find this setting in CF 8 / WebCharts3D 5.1. Any workaround/Idea how this could be done? Please help me

Thanks in Advance
Dan G. Switzer, II's Gravatar @maramandan:

There probably is, but I can't tell you offhand. My guess is the option is in there somewhere within the nested options. You may have already done this, but a good way to find options is to browse through all the existing WebCharts templates and see if you can find on that closely matches what you want.
Tanya Crawford's Gravatar I'm creating a graph of a users test scores over time (the data comes from a query) Its just a line graph with the date on the x-axis and their score on the y-axis. The query also contains a comment that we want to appear as the tooltip. Is there a way to do this?
Thanks
Tanya
Dan G. Switzer, II's Gravatar @Tanya:

I'm not sure if you can do that with a line graph. You might be able to do it with a horizontal chart if you made each data point a chart series. However, you might be better off just using another charting services.

Adobe really needs to improve the charting functionality in ColdFusion. Every time I've tried to do anything other than the most very basic of charts, I've run into limitations I have to work around--and it's not like I'm even trying to do very difficult charts either.
Mark Llewellyn's Gravatar Hi Dan,

This solution does a great job of keeping the misplaced tips in line. Have you noticed (in IE7 or 8) that when mousing off of a chart point that the entire chart flashes invisible and back again? I made a change in your script that handles this well, but now I can't use the mouse wheel to scroll down a long page unless the pointer is over one of the charts!

If you want to see the changes I made, let me know what email to send to...

Mark
Dan G. Switzer, II's Gravatar @Mark:

Not sure what your "fix" is, so it's hard to say why your mousewheel behavior has changed. Feel free to paste your version in the comments and I'll look at it.

The only thing that I could see potentially preventing your mouse events from scrolling the page is if you have some invisible layer over top of your content that's essentially blocking the underlying body element. This script changes doesn't do anything like that.
Mark Llewellyn's Gravatar Dan, here is the code (didn't think your system would keep it as-is!)
~Mark

// on first show, we need to move to the body
function xx_set_visible(imgId, tipId, e, show){
    // get the table we're going to show
    var $tip = $("#" + tipId);
    if( !__xx_set_visible[tipId] ){
        // move to the body and make visible
        $tip.appendTo("body").css("visibility", "visible");
        __xx_set_visible[tipId] = true;
    }
    $tip[show ? "show" : "hide"]();
    // make sure we place the tip in the correct location
    xx_move_tag(imgId, tipId, e, show);
  
}

function xx_move_tag(imgId, tipId, e, show){
    // fix for IE jumping scroll-up issue (and tip lingering on click to drilldown URL)
    // note that tip will no longer smoothly move with mouse pointer within same chart element
    if( !show ){
        var $tip = $("#" + tipId);
        $tip.zIndex = -1;
        return false;
    }
    // get the table we're going to show
    var $tip = $("#" + tipId);
    // get the scroll offsets
    var scroll = {top: $(window).scrollTop(), left: $(window).scrollLeft()};
    // if using IE we need to create the e.pageX/pageY events  
    if( !e.pageY ){
      e.pageY = e.clientY + scroll.top;
      e.pageX = e.clientX + scroll.left;
    }
    var pos = {top: e.pageY + 20, left: e.pageX + 10}; // add padding for cursor
    var tip = {width: $tip.outerWidth() + 10, height: $tip.outerHeight() + 10}; // add padding for edge
    var screen = {right: scroll.left + $("body").width(), bottom: scroll.top + $(window).height()};
    // if we're going to be off the screen, adjust the position
    if( pos.left + tip.width > screen.right ){
      // don't move past most right of screen
      pos.left = screen.right - tip.width; // pos.left - tip.width || screen.right - tip.width - 10;
    }
    if( pos.top + tip.height > screen.bottom ){
      // don't move past most right of screen
      pos.top = pos.top - tip.height - 15; // since we're moving tip above we need adjust for the original padding we add
    }
    // make sure tip z-index is on top of all else (especially when using jqm popup displays)
    pos.zIndex = 10000;
    
    // position the tip
    $tip.css(pos);
}

Add Comment

Leave this field empty


If you subscribe, any new posts to this thread will be sent to your email address.