3 * Pure JavaScript plotting plugin using jQuery
8 * Copyright (c) 2009-2012 Chris Leonello
9 * jqPlot is currently available for use in all personal or commercial projects
10 * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
11 * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
12 * choose the license that best suits your project and use it accordingly.
14 * Although not required, the author would appreciate an email letting him
15 * know of any substantial use of jqPlot. You can reach the author at:
16 * chris at jqplot dot com or see http://www.jqplot.com/info.php .
18 * If you are feeling kind and generous, consider supporting the project by
19 * making a donation at: http://www.jqplot.com/donate.php .
21 * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
25 * http://hexmen.com/blog/2007/03/printf-sprintf/
26 * http://hexmen.com/js/sprintf.js
27 * The author (Ash Searle) has placed this code in the public domain:
28 * "This code is unrestricted: you are free to use it however you like."
33 // class: $.jqplot.CanvasOverlay
34 $.jqplot.CanvasOverlay = function(opts){
35 var options = opts || {};
37 show: $.jqplot.config.enablePlugins,
42 this.objectNames = [];
44 this.markerRenderer = new $.jqplot.MarkerRenderer({style:'line'});
45 this.markerRenderer.init();
46 this.highlightObjectIndex = null;
47 if (options.objects) {
48 var objs = options.objects,
50 for (var i=0; i<objs.length; i++) {
57 case 'horizontalLine':
58 this.addHorizontalLine(obj[n]);
60 case 'dashedHorizontalLine':
61 this.addDashedHorizontalLine(obj[n]);
64 this.addVerticalLine(obj[n]);
66 case 'dashedVerticalLine':
67 this.addDashedVerticalLine(obj[n]);
75 $.extend(true, this.options, options);
78 // called with scope of a plot object
79 $.jqplot.CanvasOverlay.postPlotInit = function (target, data, opts) {
80 var options = opts || {};
81 // add a canvasOverlay attribute to the plot
82 this.plugins.canvasOverlay = new $.jqplot.CanvasOverlay(options.canvasOverlay);
89 this.gridStart = null;
91 this.tooltipWidthFactor = 0;
94 // Optional name for the overlay object.
95 // Can be later used to retrieve the object by name.
98 // true to show (draw), false to not draw.
101 // Width of the line.
104 // Type of ending placed on the line ['round', 'butt', 'square']
110 // wether or not to draw a shadow on the line
113 // Shadow angle in degrees
115 // prop: shadowOffset
116 // Shadow offset from line in pixels
119 // Number of times shadow is stroked, each stroke offset shadowOffset from the last.
122 // Alpha channel transparency of shadow. 0 = transparent.
125 // X axis to use for positioning/scaling the line.
128 // Y axis to use for positioning/scaling the line.
131 // Show a tooltip with data point values.
133 // prop: showTooltipPrecision
134 // Controls how close to line cursor must be to show tooltip.
135 // Higher number = closer to line, lower number = farther from line.
136 // 1.0 = cursor must be over line.
137 showTooltipPrecision: 0.6,
138 // prop: tooltipLocation
139 // Where to position tooltip, 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'
140 tooltipLocation: 'nw',
142 // true = fade in/out tooltip, flase = show/hide tooltip
144 // prop: tooltipFadeSpeed
145 // 'slow', 'def', 'fast', or number of milliseconds.
146 tooltipFadeSpeed: "fast",
147 // prop: tooltipOffset
148 // Pixel offset of tooltip from the highlight.
150 // prop: tooltipFormatString
151 // Format string passed the x and y values of the cursor on the line.
152 // e.g., 'Dogs: %.2f, Cats: %d'.
153 tooltipFormatString: '%d, %d'
161 function Line(options) {
166 // [x, y] coordinates for the start of the line.
169 // [x, y] coordinates for the end of the line.
172 $.extend(true, this.options, opts, options);
174 if (this.options.showTooltipPrecision < 0.01) {
175 this.options.showTooltipPrecision = 0.01;
179 Line.prototype = new LineBase();
180 Line.prototype.constructor = Line;
184 * Class: HorizontalLine
185 * A straight horizontal line.
187 function HorizontalLine(options) {
189 this.type = 'horizontalLine';
192 // y value to position the line
195 // x value for the start of the line, null to scale to axis min.
198 // x value for the end of the line, null to scale to axis max.
201 // offset ends of the line inside the grid. Number
202 xOffset: '6px', // number or string. Number interpreted as units, string as pixels.
206 $.extend(true, this.options, opts, options);
208 if (this.options.showTooltipPrecision < 0.01) {
209 this.options.showTooltipPrecision = 0.01;
213 HorizontalLine.prototype = new LineBase();
214 HorizontalLine.prototype.constructor = HorizontalLine;
218 * Class: DashedHorizontalLine
219 * A straight dashed horizontal line.
221 function DashedHorizontalLine(options) {
223 this.type = 'dashedHorizontalLine';
228 xOffset: '6px', // number or string. Number interpreted as units, string as pixels.
232 // Array of line, space settings in pixels.
233 // Default is 8 pixel of line, 8 pixel of space.
234 // Note, limit to a 2 element array b/c of bug with higher order arrays.
237 $.extend(true, this.options, opts, options);
239 if (this.options.showTooltipPrecision < 0.01) {
240 this.options.showTooltipPrecision = 0.01;
244 DashedHorizontalLine.prototype = new LineBase();
245 DashedHorizontalLine.prototype.constructor = DashedHorizontalLine;
249 * Class: VerticalLine
250 * A straight vertical line.
252 function VerticalLine(options) {
254 this.type = 'verticalLine';
259 yOffset: '6px', // number or string. Number interpreted as units, string as pixels.
263 $.extend(true, this.options, opts, options);
265 if (this.options.showTooltipPrecision < 0.01) {
266 this.options.showTooltipPrecision = 0.01;
270 VerticalLine.prototype = new LineBase();
271 VerticalLine.prototype.constructor = VerticalLine;
275 * Class: DashedVerticalLine
276 * A straight dashed vertical line.
278 function DashedVerticalLine(options) {
280 this.type = 'dashedVerticalLine';
287 yOffset: '6px', // number or string. Number interpreted as units, string as pixels.
291 // Array of line, space settings in pixels.
292 // Default is 8 pixel of line, 8 pixel of space.
293 // Note, limit to a 2 element array b/c of bug with higher order arrays.
296 $.extend(true, this.options, opts, options);
298 if (this.options.showTooltipPrecision < 0.01) {
299 this.options.showTooltipPrecision = 0.01;
303 DashedVerticalLine.prototype = new LineBase();
304 DashedVerticalLine.prototype.constructor = DashedVerticalLine;
306 $.jqplot.CanvasOverlay.prototype.addLine = function(opts) {
307 var line = new Line(opts);
308 line.uid = objCounter++;
309 this.objects.push(line);
310 this.objectNames.push(line.options.name);
313 $.jqplot.CanvasOverlay.prototype.addHorizontalLine = function(opts) {
314 var line = new HorizontalLine(opts);
315 line.uid = objCounter++;
316 this.objects.push(line);
317 this.objectNames.push(line.options.name);
320 $.jqplot.CanvasOverlay.prototype.addDashedHorizontalLine = function(opts) {
321 var line = new DashedHorizontalLine(opts);
322 line.uid = objCounter++;
323 this.objects.push(line);
324 this.objectNames.push(line.options.name);
327 $.jqplot.CanvasOverlay.prototype.addVerticalLine = function(opts) {
328 var line = new VerticalLine(opts);
329 line.uid = objCounter++;
330 this.objects.push(line);
331 this.objectNames.push(line.options.name);
334 $.jqplot.CanvasOverlay.prototype.addDashedVerticalLine = function(opts) {
335 var line = new DashedVerticalLine(opts);
336 line.uid = objCounter++;
337 this.objects.push(line);
338 this.objectNames.push(line.options.name);
341 $.jqplot.CanvasOverlay.prototype.removeObject = function(idx) {
342 // check if integer, remove by index
343 if ($.type(idx) == 'number') {
344 this.objects.splice(idx, 1);
345 this.objectNames.splice(idx, 1);
347 // if string, remove by name
349 var id = $.inArray(idx, this.objectNames);
351 this.objects.splice(id, 1);
352 this.objectNames.splice(id, 1);
357 $.jqplot.CanvasOverlay.prototype.getObject = function(idx) {
358 // check if integer, remove by index
359 if ($.type(idx) == 'number') {
360 return this.objects[idx];
362 // if string, remove by name
364 var id = $.inArray(idx, this.objectNames);
366 return this.objects[id];
371 // Set get as alias for getObject.
372 $.jqplot.CanvasOverlay.prototype.get = $.jqplot.CanvasOverlay.prototype.getObject;
374 $.jqplot.CanvasOverlay.prototype.clear = function(plot) {
375 this.canvas._ctx.clearRect(0,0,this.canvas.getWidth(), this.canvas.getHeight());
378 $.jqplot.CanvasOverlay.prototype.draw = function(plot) {
381 mr = this.markerRenderer,
384 if (this.options.show) {
385 this.canvas._ctx.clearRect(0,0,this.canvas.getWidth(), this.canvas.getHeight());
386 for (var k=0; k<objs.length; k++) {
388 var opts = $.extend(true, {}, obj.options);
389 if (obj.options.show) {
390 // style and shadow properties should be set before
391 // every draw of marker renderer.
392 mr.shadow = obj.options.shadow;
393 obj.tooltipWidthFactor = obj.options.lineWidth / obj.options.showTooltipPrecision;
396 // style and shadow properties should be set before
397 // every draw of marker renderer.
399 opts.closePath = false;
400 start = [plot.axes[obj.options.xaxis].series_u2p(obj.options.start[0]), plot.axes[obj.options.yaxis].series_u2p(obj.options.start[1])];
401 stop = [plot.axes[obj.options.xaxis].series_u2p(obj.options.stop[0]), plot.axes[obj.options.yaxis].series_u2p(obj.options.stop[1])];
402 obj.gridStart = start;
404 mr.draw(start, stop, this.canvas._ctx, opts);
406 case 'horizontalLine':
408 // style and shadow properties should be set before
409 // every draw of marker renderer.
410 if (obj.options.y != null) {
412 opts.closePath = false;
413 var xaxis = plot.axes[obj.options.xaxis],
416 y = plot.axes[obj.options.yaxis].series_u2p(obj.options.y),
417 xminoff = obj.options.xminOffset || obj.options.xOffset,
418 xmaxoff = obj.options.xmaxOffset || obj.options.xOffset;
419 if (obj.options.xmin != null) {
420 xstart = xaxis.series_u2p(obj.options.xmin);
422 else if (xminoff != null) {
423 if ($.type(xminoff) == "number") {
424 xstart = xaxis.series_u2p(xaxis.min + xminoff);
426 else if ($.type(xminoff) == "string") {
427 xstart = xaxis.series_u2p(xaxis.min) + parseFloat(xminoff);
430 if (obj.options.xmax != null) {
431 xstop = xaxis.series_u2p(obj.options.xmax);
433 else if (xmaxoff != null) {
434 if ($.type(xmaxoff) == "number") {
435 xstop = xaxis.series_u2p(xaxis.max - xmaxoff);
437 else if ($.type(xmaxoff) == "string") {
438 xstop = xaxis.series_u2p(xaxis.max) - parseFloat(xmaxoff);
441 if (xstop != null && xstart != null) {
442 obj.gridStart = [xstart, y];
443 obj.gridStop = [xstop, y];
444 mr.draw([xstart, y], [xstop, y], this.canvas._ctx, opts);
449 case 'dashedHorizontalLine':
451 var dashPat = obj.options.dashPattern;
453 for (var i=0; i<dashPat.length; i++) {
454 dashPatLen += dashPat[i];
457 // style and shadow properties should be set before
458 // every draw of marker renderer.
459 if (obj.options.y != null) {
461 opts.closePath = false;
462 var xaxis = plot.axes[obj.options.xaxis],
465 y = plot.axes[obj.options.yaxis].series_u2p(obj.options.y),
466 xminoff = obj.options.xminOffset || obj.options.xOffset,
467 xmaxoff = obj.options.xmaxOffset || obj.options.xOffset;
468 if (obj.options.xmin != null) {
469 xstart = xaxis.series_u2p(obj.options.xmin);
471 else if (xminoff != null) {
472 if ($.type(xminoff) == "number") {
473 xstart = xaxis.series_u2p(xaxis.min + xminoff);
475 else if ($.type(xminoff) == "string") {
476 xstart = xaxis.series_u2p(xaxis.min) + parseFloat(xminoff);
479 if (obj.options.xmax != null) {
480 xstop = xaxis.series_u2p(obj.options.xmax);
482 else if (xmaxoff != null) {
483 if ($.type(xmaxoff) == "number") {
484 xstop = xaxis.series_u2p(xaxis.max - xmaxoff);
486 else if ($.type(xmaxoff) == "string") {
487 xstop = xaxis.series_u2p(xaxis.max) - parseFloat(xmaxoff);
490 if (xstop != null && xstart != null) {
491 obj.gridStart = [xstart, y];
492 obj.gridStop = [xstop, y];
493 var numDash = Math.ceil((xstop - xstart)/dashPatLen);
495 for (var i=0; i<numDash; i++) {
496 for (var j=0; j<dashPat.length; j+=2) {
498 mr.draw([b, y], [e, y], this.canvas._ctx, opts);
500 if (j < dashPat.length-1) {
511 // style and shadow properties should be set before
512 // every draw of marker renderer.
513 if (obj.options.x != null) {
515 opts.closePath = false;
516 var yaxis = plot.axes[obj.options.yaxis],
519 x = plot.axes[obj.options.xaxis].series_u2p(obj.options.x),
520 yminoff = obj.options.yminOffset || obj.options.yOffset,
521 ymaxoff = obj.options.ymaxOffset || obj.options.yOffset;
522 if (obj.options.ymin != null) {
523 ystart = yaxis.series_u2p(obj.options.ymin);
525 else if (yminoff != null) {
526 if ($.type(yminoff) == "number") {
527 ystart = yaxis.series_u2p(yaxis.min - yminoff);
529 else if ($.type(yminoff) == "string") {
530 ystart = yaxis.series_u2p(yaxis.min) - parseFloat(yminoff);
533 if (obj.options.ymax != null) {
534 ystop = yaxis.series_u2p(obj.options.ymax);
536 else if (ymaxoff != null) {
537 if ($.type(ymaxoff) == "number") {
538 ystop = yaxis.series_u2p(yaxis.max + ymaxoff);
540 else if ($.type(ymaxoff) == "string") {
541 ystop = yaxis.series_u2p(yaxis.max) + parseFloat(ymaxoff);
544 if (ystop != null && ystart != null) {
545 obj.gridStart = [x, ystart];
546 obj.gridStop = [x, ystop];
547 mr.draw([x, ystart], [x, ystop], this.canvas._ctx, opts);
552 case 'dashedVerticalLine':
554 var dashPat = obj.options.dashPattern;
556 for (var i=0; i<dashPat.length; i++) {
557 dashPatLen += dashPat[i];
560 // style and shadow properties should be set before
561 // every draw of marker renderer.
562 if (obj.options.x != null) {
564 opts.closePath = false;
565 var yaxis = plot.axes[obj.options.yaxis],
568 x = plot.axes[obj.options.xaxis].series_u2p(obj.options.x),
569 yminoff = obj.options.yminOffset || obj.options.yOffset,
570 ymaxoff = obj.options.ymaxOffset || obj.options.yOffset;
571 if (obj.options.ymin != null) {
572 ystart = yaxis.series_u2p(obj.options.ymin);
574 else if (yminoff != null) {
575 if ($.type(yminoff) == "number") {
576 ystart = yaxis.series_u2p(yaxis.min - yminoff);
578 else if ($.type(yminoff) == "string") {
579 ystart = yaxis.series_u2p(yaxis.min) - parseFloat(yminoff);
582 if (obj.options.ymax != null) {
583 ystop = yaxis.series_u2p(obj.options.ymax);
585 else if (ymaxoff != null) {
586 if ($.type(ymaxoff) == "number") {
587 ystop = yaxis.series_u2p(yaxis.max + ymaxoff);
589 else if ($.type(ymaxoff) == "string") {
590 ystop = yaxis.series_u2p(yaxis.max) + parseFloat(ymaxoff);
595 if (ystop != null && ystart != null) {
596 obj.gridStart = [x, ystart];
597 obj.gridStop = [x, ystop];
598 var numDash = Math.ceil((ystart - ystop)/dashPatLen);
599 var firstDashAdjust = ((numDash * dashPatLen) - (ystart - ystop))/2.0;
600 var b=ystart, e, bs, es;
601 for (var i=0; i<numDash; i++) {
602 for (var j=0; j<dashPat.length; j+=2) {
612 // es += firstDashAdjust;
614 mr.draw([x, b], [x, e], this.canvas._ctx, opts);
616 if (j < dashPat.length-1) {
633 // called within context of plot
634 // create a canvas which we can draw on.
635 // insert it before the eventCanvas, so eventCanvas will still capture events.
636 $.jqplot.CanvasOverlay.postPlotDraw = function() {
637 var co = this.plugins.canvasOverlay;
638 // Memory Leaks patch
639 if (co && co.highlightCanvas) {
640 co.highlightCanvas.resetCanvas();
641 co.highlightCanvas = null;
643 co.canvas = new $.jqplot.GenericCanvas();
645 this.eventCanvas._elem.before(co.canvas.createElement(this._gridPadding, 'jqplot-overlayCanvas-canvas', this._plotDimensions, this));
646 co.canvas.setContext();
651 var elem = document.createElement('div');
652 co._tooltipElem = $(elem);
654 co._tooltipElem.addClass('jqplot-canvasOverlay-tooltip');
655 co._tooltipElem.css({position:'absolute', display:'none'});
657 this.eventCanvas._elem.before(co._tooltipElem);
658 this.eventCanvas._elem.bind('mouseleave', { elem: co._tooltipElem }, function (ev) { ev.data.elem.hide(); });
664 function showTooltip(plot, obj, gridpos, datapos) {
665 var co = plot.plugins.canvasOverlay;
666 var elem = co._tooltipElem;
668 var opts = obj.options, x, y;
670 elem.html($.jqplot.sprintf(opts.tooltipFormatString, datapos[0], datapos[1]));
672 switch (opts.tooltipLocation) {
674 x = gridpos[0] + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset;
675 y = gridpos[1] + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true);
678 x = gridpos[0] + plot._gridPadding.left - elem.outerWidth(true)/2;
679 y = gridpos[1] + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true);
682 x = gridpos[0] + plot._gridPadding.left + opts.tooltipOffset;
683 y = gridpos[1] + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true);
686 x = gridpos[0] + plot._gridPadding.left + opts.tooltipOffset;
687 y = gridpos[1] + plot._gridPadding.top - elem.outerHeight(true)/2;
690 x = gridpos[0] + plot._gridPadding.left + opts.tooltipOffset;
691 y = gridpos[1] + plot._gridPadding.top + opts.tooltipOffset;
694 x = gridpos[0] + plot._gridPadding.left - elem.outerWidth(true)/2;
695 y = gridpos[1] + plot._gridPadding.top + opts.tooltipOffset;
698 x = gridpos[0] + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset;
699 y = gridpos[1] + plot._gridPadding.top + opts.tooltipOffset;
702 x = gridpos[0] + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset;
703 y = gridpos[1] + plot._gridPadding.top - elem.outerHeight(true)/2;
705 default: // same as 'nw'
706 x = gridpos[0] + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset;
707 y = gridpos[1] + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true);
713 if (opts.fadeTooltip) {
714 // Fix for stacked up animations. Thnanks Trevor!
715 elem.stop(true,true).fadeIn(opts.tooltipFadeSpeed);
724 function isNearLine(point, lstart, lstop, width) {
725 // r is point to test, p and q are end points.
728 var px = Math.round(lstop[0]);
729 var py = Math.round(lstop[1]);
730 var qx = Math.round(lstart[0]);
731 var qy = Math.round(lstart[1]);
733 var l = Math.sqrt(Math.pow(px-qx, 2) + Math.pow(py-qy, 2));
735 // scale error term by length of line.
737 var res = Math.abs((qx-px) * (ry-py) - (qy-py) * (rx-px));
738 var ret = (res < eps) ? true : false;
743 function handleMove(ev, gridpos, datapos, neighbor, plot) {
744 var co = plot.plugins.canvasOverlay;
745 var objs = co.objects;
747 var obj, haveHighlight=false;
749 for (var i=0; i<l; i++) {
751 if (obj.options.showTooltip) {
752 var n = isNearLine([gridpos.x, gridpos.y], obj.gridStart, obj.gridStop, obj.tooltipWidthFactor);
753 datapos = [plot.axes[obj.options.xaxis].series_p2u(gridpos.x), plot.axes[obj.options.yaxis].series_p2u(gridpos.y)];
756 // near line, no highlighting
757 // near line, highliting on this line
758 // near line, highlighting another line
759 // not near any line, highlighting
760 // not near any line, no highlighting
762 // near line, not currently highlighting
763 if (n && co.highlightObjectIndex == null) {
766 showTooltip(plot, obj, [gridpos.x, gridpos.y], datapos);
769 case 'horizontalLine':
770 case 'dashedHorizontalLine':
771 showTooltip(plot, obj, [gridpos.x, obj.gridStart[1]], [datapos[0], obj.options.y]);
775 case 'dashedVerticalLine':
776 showTooltip(plot, obj, [obj.gridStart[0], gridpos.y], [obj.options.x, datapos[1]]);
781 co.highlightObjectIndex = i;
782 haveHighlight = true;
786 // near line, highlighting another line.
787 else if (n && co.highlightObjectIndex !== i) {
789 elem = co._tooltipElem;
790 if (obj.fadeTooltip) {
791 elem.fadeOut(obj.tooltipFadeSpeed);
797 // turn on right tooltip.
800 showTooltip(plot, obj, [gridpos.x, gridpos.y], datapos);
803 case 'horizontalLine':
804 case 'dashedHorizontalLine':
805 showTooltip(plot, obj, [gridpos.x, obj.gridStart[1]], [datapos[0], obj.options.y]);
809 case 'dashedVerticalLine':
810 showTooltip(plot, obj, [obj.gridStart[0], gridpos.y], [obj.options.x, datapos[1]]);
816 co.highlightObjectIndex = i;
817 haveHighlight = true;
821 // near line, already highlighting this line, update
825 showTooltip(plot, obj, [gridpos.x, gridpos.y], datapos);
828 case 'horizontalLine':
829 case 'dashedHorizontalLine':
830 showTooltip(plot, obj, [gridpos.x, obj.gridStart[1]], [datapos[0], obj.options.y]);
834 case 'dashedVerticalLine':
835 showTooltip(plot, obj, [obj.gridStart[0], gridpos.y], [obj.options.x, datapos[1]]);
841 haveHighlight = true;
847 // check if we are highlighting and not near a line, turn it off.
848 if (!haveHighlight && co.highlightObjectIndex !== null) {
849 elem = co._tooltipElem;
850 obj = co.getObject(co.highlightObjectIndex);
851 if (obj.fadeTooltip) {
852 elem.fadeOut(obj.tooltipFadeSpeed);
857 co.highlightObjectIndex = null;
861 $.jqplot.postInitHooks.push($.jqplot.CanvasOverlay.postPlotInit);
862 $.jqplot.postDrawHooks.push($.jqplot.CanvasOverlay.postPlotDraw);
863 $.jqplot.eventListenerHooks.push(['jqplotMouseMove', handleMove]);