Using jQuery to fix CFCHART's tooltip behavior

Posted by Dan on Mar 6, 2009 @ 4:12 PM

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!

Categories: HTML/ColdFusion, jQuery

22 Comments

  • 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.
  • @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.
  • Many thanks Dan, this was very useful!
  • 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
  • @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.
  • 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
  • @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.
  • 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
  • @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.
  • 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);
    }
  • Hi,

    Thanks for a great fix for this, I had problem with this for a long time now.. I just wonder how would you solve this if you have more then one graph on the same page?

    //Martin
  • @Martin:

    The functions should work as-is. The functions you're replacing are used by multiple charts. The functions are generic and when they get called they reference the chart that was generated. So it should just work.
  • Hi,

    Thanks for the quick reply, I tried with two graphs and it did not work so I removed one of the graphs and it started to work again. But I will give it a second go today, I let you know how it worked out..

    Thanks
    Martin
  • Hi Dan,

    It worked just fine, the thing was that I had six graphs but to test I just choose two, but all of them need to have the setting on the same page so it worked by adding the code to all graphs.

    Thanks for a great solution

    have a nice day
    Martin
  • @Martin:

    Actually, what I'd probably do for the additional charts is just remove the script tag altogether. You only need the functions in there once.

    If you wrap this into some kind of wrapper functionality, you can use the Request scope to track if you've already inserted the JS into the page output, so the functions only get in the page once.
  • Tip my hat to you sir. Perfect fix for the IE8 misplaced tool tip..
  • Dan, Thanks alot for this blog. it just worked like charm.

    I had issues in IE11 after taking out code out of quirks mode.

    Without fix it just worked in firefox. fyi
  • I tried adding this but for some reason, I get this error on mouseover, on the line that says: var $tip = $("#" + tipId);

    "The value of the property '$' is null or undefined, not a Function object"

    Unfortunately I don't get jQuery. If this was simple Javascript, I'd have a chance to fix it, but here I need help.

    I figured I need to include jQuery so I added this line before the whole JS block:
        <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>

    What could be wrong?

    -Michael
  • @Michael:

    That error indicates that you jQuery is not defined on your page.
  • Thank you so much!!
    This was amazingly helpful!!
    Cheers from Mexico!
  • Thank you!! Still relevant!! Works perfectly. The stock code didn't work in Chrome at all but this works great now.
  • This just saved me for a client on CF9. For whatever reason when we switched the charts from flash to PNG or JPG the tooltip would flicker like mad in Chrome. This fixed it! Anyone know if this script causes any issues in CF 2016?

Comments for this entry have been disabled.