Home | History | Annotate | Download | only in flot
      1 /* Flot plugin for adding the ability to pan and zoom the plot.
      2 
      3 Copyright (c) 2007-2014 IOLA and Ole Laursen.
      4 Licensed under the MIT license.
      5 
      6 The default behaviour is double click and scrollwheel up/down to zoom in, drag
      7 to pan. The plugin defines plot.zoom({ center }), plot.zoomOut() and
      8 plot.pan( offset ) so you easily can add custom controls. It also fires
      9 "plotpan" and "plotzoom" events, useful for synchronizing plots.
     10 
     11 The plugin supports these options:
     12 
     13 	zoom: {
     14 		interactive: false
     15 		trigger: "dblclick" // or "click" for single click
     16 		amount: 1.5         // 2 = 200% (zoom in), 0.5 = 50% (zoom out)
     17 	}
     18 
     19 	pan: {
     20 		interactive: false
     21 		cursor: "move"      // CSS mouse cursor value used when dragging, e.g. "pointer"
     22 		frameRate: 20
     23 	}
     24 
     25 	xaxis, yaxis, x2axis, y2axis: {
     26 		zoomRange: null  // or [ number, number ] (min range, max range) or false
     27 		panRange: null   // or [ number, number ] (min, max) or false
     28 	}
     29 
     30 "interactive" enables the built-in drag/click behaviour. If you enable
     31 interactive for pan, then you'll have a basic plot that supports moving
     32 around; the same for zoom.
     33 
     34 "amount" specifies the default amount to zoom in (so 1.5 = 150%) relative to
     35 the current viewport.
     36 
     37 "cursor" is a standard CSS mouse cursor string used for visual feedback to the
     38 user when dragging.
     39 
     40 "frameRate" specifies the maximum number of times per second the plot will
     41 update itself while the user is panning around on it (set to null to disable
     42 intermediate pans, the plot will then not update until the mouse button is
     43 released).
     44 
     45 "zoomRange" is the interval in which zooming can happen, e.g. with zoomRange:
     46 [1, 100] the zoom will never scale the axis so that the difference between min
     47 and max is smaller than 1 or larger than 100. You can set either end to null
     48 to ignore, e.g. [1, null]. If you set zoomRange to false, zooming on that axis
     49 will be disabled.
     50 
     51 "panRange" confines the panning to stay within a range, e.g. with panRange:
     52 [-10, 20] panning stops at -10 in one end and at 20 in the other. Either can
     53 be null, e.g. [-10, null]. If you set panRange to false, panning on that axis
     54 will be disabled.
     55 
     56 Example API usage:
     57 
     58 	plot = $.plot(...);
     59 
     60 	// zoom default amount in on the pixel ( 10, 20 )
     61 	plot.zoom({ center: { left: 10, top: 20 } });
     62 
     63 	// zoom out again
     64 	plot.zoomOut({ center: { left: 10, top: 20 } });
     65 
     66 	// zoom 200% in on the pixel (10, 20)
     67 	plot.zoom({ amount: 2, center: { left: 10, top: 20 } });
     68 
     69 	// pan 100 pixels to the left and 20 down
     70 	plot.pan({ left: -100, top: 20 })
     71 
     72 Here, "center" specifies where the center of the zooming should happen. Note
     73 that this is defined in pixel space, not the space of the data points (you can
     74 use the p2c helpers on the axes in Flot to help you convert between these).
     75 
     76 "amount" is the amount to zoom the viewport relative to the current range, so
     77 1 is 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out). You
     78 can set the default in the options.
     79 
     80 */
     81 
     82 // First two dependencies, jquery.event.drag.js and
     83 // jquery.mousewheel.js, we put them inline here to save people the
     84 // effort of downloading them.
     85 
     86 /*
     87 jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com)
     88 Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt
     89 */
     90 (function(a){function e(h){var k,j=this,l=h.data||{};if(l.elem)j=h.dragTarget=l.elem,h.dragProxy=d.proxy||j,h.cursorOffsetX=l.pageX-l.left,h.cursorOffsetY=l.pageY-l.top,h.offsetX=h.pageX-h.cursorOffsetX,h.offsetY=h.pageY-h.cursorOffsetY;else if(d.dragging||l.which>0&&h.which!=l.which||a(h.target).is(l.not))return;switch(h.type){case"mousedown":return a.extend(l,a(j).offset(),{elem:j,target:h.target,pageX:h.pageX,pageY:h.pageY}),b.add(document,"mousemove mouseup",e,l),i(j,!1),d.dragging=null,!1;case!d.dragging&&"mousemove":if(g(h.pageX-l.pageX)+g(h.pageY-l.pageY)<l.distance)break;h.target=l.target,k=f(h,"dragstart",j),k!==!1&&(d.dragging=j,d.proxy=h.dragProxy=a(k||j)[0]);case"mousemove":if(d.dragging){if(k=f(h,"drag",j),c.drop&&(c.drop.allowed=k!==!1,c.drop.handler(h)),k!==!1)break;h.type="mouseup"}case"mouseup":b.remove(document,"mousemove mouseup",e),d.dragging&&(c.drop&&c.drop.handler(h),f(h,"dragend",j)),i(j,!0),d.dragging=d.proxy=l.elem=!1}return!0}function f(b,c,d){b.type=c;var e=a.event.dispatch.call(d,b);return e===!1?!1:e||b.result}function g(a){return Math.pow(a,2)}function h(){return d.dragging===!1}function i(a,b){a&&(a.unselectable=b?"off":"on",a.onselectstart=function(){return b},a.style&&(a.style.MozUserSelect=b?"":"none"))}a.fn.drag=function(a,b,c){return b&&this.bind("dragstart",a),c&&this.bind("dragend",c),a?this.bind("drag",b?b:a):this.trigger("drag")};var b=a.event,c=b.special,d=c.drag={not:":input",distance:0,which:1,dragging:!1,setup:function(c){c=a.extend({distance:d.distance,which:d.which,not:d.not},c||{}),c.distance=g(c.distance),b.add(this,"mousedown",e,c),this.attachEvent&&this.attachEvent("ondragstart",h)},teardown:function(){b.remove(this,"mousedown",e),this===d.dragging&&(d.dragging=d.proxy=!1),i(this,!0),this.detachEvent&&this.detachEvent("ondragstart",h)}};c.dragstart=c.dragend={setup:function(){},teardown:function(){}}})(jQuery);
     91 
     92 /* jquery.mousewheel.min.js
     93  * Copyright (c) 2011 Brandon Aaron (http://brandonaaron.net)
     94  * Licensed under the MIT License (LICENSE.txt).
     95  * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers.
     96  * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix.
     97  * Thanks to: Seamus Leahy for adding deltaX and deltaY
     98  *
     99  * Version: 3.0.6
    100  *
    101  * Requires: 1.2.2+
    102  */
    103 (function(d){function e(a){var b=a||window.event,c=[].slice.call(arguments,1),f=0,e=0,g=0,a=d.event.fix(b);a.type="mousewheel";b.wheelDelta&&(f=b.wheelDelta/120);b.detail&&(f=-b.detail/3);g=f;void 0!==b.axis&&b.axis===b.HORIZONTAL_AXIS&&(g=0,e=-1*f);void 0!==b.wheelDeltaY&&(g=b.wheelDeltaY/120);void 0!==b.wheelDeltaX&&(e=-1*b.wheelDeltaX/120);c.unshift(a,f,e,g);return(d.event.dispatch||d.event.handle).apply(this,c)}var c=["DOMMouseScroll","mousewheel"];if(d.event.fixHooks)for(var h=c.length;h;)d.event.fixHooks[c[--h]]=d.event.mouseHooks;d.event.special.mousewheel={setup:function(){if(this.addEventListener)for(var a=c.length;a;)this.addEventListener(c[--a],e,!1);else this.onmousewheel=e},teardown:function(){if(this.removeEventListener)for(var a=c.length;a;)this.removeEventListener(c[--a],e,!1);else this.onmousewheel=null}};d.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})})(jQuery);
    104 
    105 
    106 
    107 
    108 (function ($) {
    109     var options = {
    110         xaxis: {
    111             zoomRange: null, // or [number, number] (min range, max range)
    112             panRange: null // or [number, number] (min, max)
    113         },
    114         zoom: {
    115             interactive: false,
    116             trigger: "dblclick", // or "click" for single click
    117             amount: 1.5 // how much to zoom relative to current position, 2 = 200% (zoom in), 0.5 = 50% (zoom out)
    118         },
    119         pan: {
    120             interactive: false,
    121             cursor: "move",
    122             frameRate: 20
    123         }
    124     };
    125 
    126     function init(plot) {
    127         function onZoomClick(e, zoomOut) {
    128             var c = plot.offset();
    129             c.left = e.pageX - c.left;
    130             c.top = e.pageY - c.top;
    131             if (zoomOut)
    132                 plot.zoomOut({ center: c });
    133             else
    134                 plot.zoom({ center: c });
    135         }
    136 
    137         function onMouseWheel(e, delta) {
    138             e.preventDefault();
    139             onZoomClick(e, delta < 0);
    140             return false;
    141         }
    142 
    143         var prevCursor = 'default', prevPageX = 0, prevPageY = 0,
    144             panTimeout = null;
    145 
    146         function onDragStart(e) {
    147             if (e.which != 1)  // only accept left-click
    148                 return false;
    149             var c = plot.getPlaceholder().css('cursor');
    150             if (c)
    151                 prevCursor = c;
    152             plot.getPlaceholder().css('cursor', plot.getOptions().pan.cursor);
    153             prevPageX = e.pageX;
    154             prevPageY = e.pageY;
    155         }
    156 
    157         function onDrag(e) {
    158             var frameRate = plot.getOptions().pan.frameRate;
    159             if (panTimeout || !frameRate)
    160                 return;
    161 
    162             panTimeout = setTimeout(function () {
    163                 plot.pan({ left: prevPageX - e.pageX,
    164                            top: prevPageY - e.pageY });
    165                 prevPageX = e.pageX;
    166                 prevPageY = e.pageY;
    167 
    168                 panTimeout = null;
    169             }, 1 / frameRate * 1000);
    170         }
    171 
    172         function onDragEnd(e) {
    173             if (panTimeout) {
    174                 clearTimeout(panTimeout);
    175                 panTimeout = null;
    176             }
    177 
    178             plot.getPlaceholder().css('cursor', prevCursor);
    179             plot.pan({ left: prevPageX - e.pageX,
    180                        top: prevPageY - e.pageY });
    181         }
    182 
    183         function bindEvents(plot, eventHolder) {
    184             var o = plot.getOptions();
    185             if (o.zoom.interactive) {
    186                 eventHolder[o.zoom.trigger](onZoomClick);
    187                 eventHolder.mousewheel(onMouseWheel);
    188             }
    189 
    190             if (o.pan.interactive) {
    191                 eventHolder.bind("dragstart", { distance: 10 }, onDragStart);
    192                 eventHolder.bind("drag", onDrag);
    193                 eventHolder.bind("dragend", onDragEnd);
    194             }
    195         }
    196 
    197         plot.zoomOut = function (args) {
    198             if (!args)
    199                 args = {};
    200 
    201             if (!args.amount)
    202                 args.amount = plot.getOptions().zoom.amount;
    203 
    204             args.amount = 1 / args.amount;
    205             plot.zoom(args);
    206         };
    207 
    208         plot.zoom = function (args) {
    209             if (!args)
    210                 args = {};
    211 
    212             var c = args.center,
    213                 amount = args.amount || plot.getOptions().zoom.amount,
    214                 w = plot.width(), h = plot.height();
    215 
    216             if (!c)
    217                 c = { left: w / 2, top: h / 2 };
    218 
    219             var xf = c.left / w,
    220                 yf = c.top / h,
    221                 minmax = {
    222                     x: {
    223                         min: c.left - xf * w / amount,
    224                         max: c.left + (1 - xf) * w / amount
    225                     },
    226                     y: {
    227                         min: c.top - yf * h / amount,
    228                         max: c.top + (1 - yf) * h / amount
    229                     }
    230                 };
    231 
    232             $.each(plot.getAxes(), function(_, axis) {
    233                 var opts = axis.options,
    234                     min = minmax[axis.direction].min,
    235                     max = minmax[axis.direction].max,
    236                     zr = opts.zoomRange,
    237                     pr = opts.panRange;
    238 
    239                 if (zr === false) // no zooming on this axis
    240                     return;
    241 
    242                 min = axis.c2p(min);
    243                 max = axis.c2p(max);
    244                 if (min > max) {
    245                     // make sure min < max
    246                     var tmp = min;
    247                     min = max;
    248                     max = tmp;
    249                 }
    250 
    251                 //Check that we are in panRange
    252                 if (pr) {
    253                     if (pr[0] != null && min < pr[0]) {
    254                         min = pr[0];
    255                     }
    256                     if (pr[1] != null && max > pr[1]) {
    257                         max = pr[1];
    258                     }
    259                 }
    260 
    261                 var range = max - min;
    262                 if (zr &&
    263                     ((zr[0] != null && range < zr[0] && amount >1) ||
    264                      (zr[1] != null && range > zr[1] && amount <1)))
    265                     return;
    266 
    267                 opts.min = min;
    268                 opts.max = max;
    269             });
    270 
    271             plot.setupGrid();
    272             plot.draw();
    273 
    274             if (!args.preventEvent)
    275                 plot.getPlaceholder().trigger("plotzoom", [ plot, args ]);
    276         };
    277 
    278         plot.pan = function (args) {
    279             var delta = {
    280                 x: +args.left,
    281                 y: +args.top
    282             };
    283 
    284             if (isNaN(delta.x))
    285                 delta.x = 0;
    286             if (isNaN(delta.y))
    287                 delta.y = 0;
    288 
    289             $.each(plot.getAxes(), function (_, axis) {
    290                 var opts = axis.options,
    291                     min, max, d = delta[axis.direction];
    292 
    293                 min = axis.c2p(axis.p2c(axis.min) + d),
    294                 max = axis.c2p(axis.p2c(axis.max) + d);
    295 
    296                 var pr = opts.panRange;
    297                 if (pr === false) // no panning on this axis
    298                     return;
    299 
    300                 if (pr) {
    301                     // check whether we hit the wall
    302                     if (pr[0] != null && pr[0] > min) {
    303                         d = pr[0] - min;
    304                         min += d;
    305                         max += d;
    306                     }
    307 
    308                     if (pr[1] != null && pr[1] < max) {
    309                         d = pr[1] - max;
    310                         min += d;
    311                         max += d;
    312                     }
    313                 }
    314 
    315                 opts.min = min;
    316                 opts.max = max;
    317             });
    318 
    319             plot.setupGrid();
    320             plot.draw();
    321 
    322             if (!args.preventEvent)
    323                 plot.getPlaceholder().trigger("plotpan", [ plot, args ]);
    324         };
    325 
    326         function shutdown(plot, eventHolder) {
    327             eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick);
    328             eventHolder.unbind("mousewheel", onMouseWheel);
    329             eventHolder.unbind("dragstart", onDragStart);
    330             eventHolder.unbind("drag", onDrag);
    331             eventHolder.unbind("dragend", onDragEnd);
    332             if (panTimeout)
    333                 clearTimeout(panTimeout);
    334         }
    335 
    336         plot.hooks.bindEvents.push(bindEvents);
    337         plot.hooks.shutdown.push(shutdown);
    338     }
    339 
    340     $.plot.plugins.push({
    341         init: init,
    342         options: options,
    343         name: 'navigate',
    344         version: '1.3'
    345     });
    346 })(jQuery);
    347