Using jQuery to fix CFCHART's tooltip behavior
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:
- 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
- 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:
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:
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:
<!---// 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:
<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:

Comments
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.
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
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.
Thanks
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.
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
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
// 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);
}

Thanks.