1/* Flot plugin for selecting regions of a plot. 2 3Copyright (c) 2007-2014 IOLA and Ole Laursen. 4Licensed under the MIT license. 5 6The plugin supports these options: 7 8selection: { 9 mode: null or "x" or "y" or "xy", 10 color: color, 11 shape: "round" or "miter" or "bevel", 12 minSize: number of pixels 13} 14 15Selection support is enabled by setting the mode to one of "x", "y" or "xy". 16In "x" mode, the user will only be able to specify the x range, similarly for 17"y" mode. For "xy", the selection becomes a rectangle where both ranges can be 18specified. "color" is color of the selection (if you need to change the color 19later on, you can get to it with plot.getOptions().selection.color). "shape" 20is the shape of the corners of the selection. 21 22"minSize" is the minimum size a selection can be in pixels. This value can 23be customized to determine the smallest size a selection can be and still 24have the selection rectangle be displayed. When customizing this value, the 25fact that it refers to pixels, not axis units must be taken into account. 26Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 27minute, setting "minSize" to 1 will not make the minimum selection size 1 28minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent 29"plotunselected" events from being fired when the user clicks the mouse without 30dragging. 31 32When selection support is enabled, a "plotselected" event will be emitted on 33the DOM element you passed into the plot function. The event handler gets a 34parameter with the ranges selected on the axes, like this: 35 36 placeholder.bind( "plotselected", function( event, ranges ) { 37 alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) 38 // similar for yaxis - with multiple axes, the extra ones are in 39 // x2axis, x3axis, ... 40 }); 41 42The "plotselected" event is only fired when the user has finished making the 43selection. A "plotselecting" event is fired during the process with the same 44parameters as the "plotselected" event, in case you want to know what's 45happening while it's happening, 46 47A "plotunselected" event with no arguments is emitted when the user clicks the 48mouse to remove the selection. As stated above, setting "minSize" to 0 will 49destroy this behavior. 50 51The plugin allso adds the following methods to the plot object: 52 53- setSelection( ranges, preventEvent ) 54 55 Set the selection rectangle. The passed in ranges is on the same form as 56 returned in the "plotselected" event. If the selection mode is "x", you 57 should put in either an xaxis range, if the mode is "y" you need to put in 58 an yaxis range and both xaxis and yaxis if the selection mode is "xy", like 59 this: 60 61 setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); 62 63 setSelection will trigger the "plotselected" event when called. If you don't 64 want that to happen, e.g. if you're inside a "plotselected" handler, pass 65 true as the second parameter. If you are using multiple axes, you can 66 specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of 67 xaxis, the plugin picks the first one it sees. 68 69- clearSelection( preventEvent ) 70 71 Clear the selection rectangle. Pass in true to avoid getting a 72 "plotunselected" event. 73 74- getSelection() 75 76 Returns the current selection in the same format as the "plotselected" 77 event. If there's currently no selection, the function returns null. 78 79*/ 80 81(function ($) { 82 function init(plot) { 83 var selection = { 84 first: { x: -1, y: -1}, second: { x: -1, y: -1}, 85 show: false, 86 active: false 87 }; 88 89 // FIXME: The drag handling implemented here should be 90 // abstracted out, there's some similar code from a library in 91 // the navigation plugin, this should be massaged a bit to fit 92 // the Flot cases here better and reused. Doing this would 93 // make this plugin much slimmer. 94 var savedhandlers = {}; 95 96 var mouseUpHandler = null; 97 98 function onMouseMove(e) { 99 if (selection.active) { 100 updateSelection(e); 101 102 plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); 103 } 104 } 105 106 function onMouseDown(e) { 107 if (e.which != 1) // only accept left-click 108 return; 109 110 // cancel out any text selections 111 document.body.focus(); 112 113 // prevent text selection and drag in old-school browsers 114 if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { 115 savedhandlers.onselectstart = document.onselectstart; 116 document.onselectstart = function () { return false; }; 117 } 118 if (document.ondrag !== undefined && savedhandlers.ondrag == null) { 119 savedhandlers.ondrag = document.ondrag; 120 document.ondrag = function () { return false; }; 121 } 122 123 setSelectionPos(selection.first, e); 124 125 selection.active = true; 126 127 // this is a bit silly, but we have to use a closure to be 128 // able to whack the same handler again 129 mouseUpHandler = function (e) { onMouseUp(e); }; 130 131 $(document).one("mouseup", mouseUpHandler); 132 } 133 134 function onMouseUp(e) { 135 mouseUpHandler = null; 136 137 // revert drag stuff for old-school browsers 138 if (document.onselectstart !== undefined) 139 document.onselectstart = savedhandlers.onselectstart; 140 if (document.ondrag !== undefined) 141 document.ondrag = savedhandlers.ondrag; 142 143 // no more dragging 144 selection.active = false; 145 updateSelection(e); 146 147 if (selectionIsSane()) 148 triggerSelectedEvent(); 149 else { 150 // this counts as a clear 151 plot.getPlaceholder().trigger("plotunselected", [ ]); 152 plot.getPlaceholder().trigger("plotselecting", [ null ]); 153 } 154 155 return false; 156 } 157 158 function getSelection() { 159 if (!selectionIsSane()) 160 return null; 161 162 if (!selection.show) return null; 163 164 var r = {}, c1 = selection.first, c2 = selection.second; 165 $.each(plot.getAxes(), function (name, axis) { 166 if (axis.used) { 167 var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); 168 r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; 169 } 170 }); 171 return r; 172 } 173 174 function triggerSelectedEvent() { 175 var r = getSelection(); 176 177 plot.getPlaceholder().trigger("plotselected", [ r ]); 178 179 // backwards-compat stuff, to be removed in future 180 if (r.xaxis && r.yaxis) 181 plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); 182 } 183 184 function clamp(min, value, max) { 185 return value < min ? min: (value > max ? max: value); 186 } 187 188 function setSelectionPos(pos, e) { 189 var o = plot.getOptions(); 190 var offset = plot.getPlaceholder().offset(); 191 var plotOffset = plot.getPlotOffset(); 192 pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); 193 pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); 194 195 if (o.selection.mode == "y") 196 pos.x = pos == selection.first ? 0 : plot.width(); 197 198 if (o.selection.mode == "x") 199 pos.y = pos == selection.first ? 0 : plot.height(); 200 } 201 202 function updateSelection(pos) { 203 if (pos.pageX == null) 204 return; 205 206 setSelectionPos(selection.second, pos); 207 if (selectionIsSane()) { 208 selection.show = true; 209 plot.triggerRedrawOverlay(); 210 } 211 else 212 clearSelection(true); 213 } 214 215 function clearSelection(preventEvent) { 216 if (selection.show) { 217 selection.show = false; 218 plot.triggerRedrawOverlay(); 219 if (!preventEvent) 220 plot.getPlaceholder().trigger("plotunselected", [ ]); 221 } 222 } 223 224 // function taken from markings support in Flot 225 function extractRange(ranges, coord) { 226 var axis, from, to, key, axes = plot.getAxes(); 227 228 for (var k in axes) { 229 axis = axes[k]; 230 if (axis.direction == coord) { 231 key = coord + axis.n + "axis"; 232 if (!ranges[key] && axis.n == 1) 233 key = coord + "axis"; // support x1axis as xaxis 234 if (ranges[key]) { 235 from = ranges[key].from; 236 to = ranges[key].to; 237 break; 238 } 239 } 240 } 241 242 // backwards-compat stuff - to be removed in future 243 if (!ranges[key]) { 244 axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; 245 from = ranges[coord + "1"]; 246 to = ranges[coord + "2"]; 247 } 248 249 // auto-reverse as an added bonus 250 if (from != null && to != null && from > to) { 251 var tmp = from; 252 from = to; 253 to = tmp; 254 } 255 256 return { from: from, to: to, axis: axis }; 257 } 258 259 function setSelection(ranges, preventEvent) { 260 var axis, range, o = plot.getOptions(); 261 262 if (o.selection.mode == "y") { 263 selection.first.x = 0; 264 selection.second.x = plot.width(); 265 } 266 else { 267 range = extractRange(ranges, "x"); 268 269 selection.first.x = range.axis.p2c(range.from); 270 selection.second.x = range.axis.p2c(range.to); 271 } 272 273 if (o.selection.mode == "x") { 274 selection.first.y = 0; 275 selection.second.y = plot.height(); 276 } 277 else { 278 range = extractRange(ranges, "y"); 279 280 selection.first.y = range.axis.p2c(range.from); 281 selection.second.y = range.axis.p2c(range.to); 282 } 283 284 selection.show = true; 285 plot.triggerRedrawOverlay(); 286 if (!preventEvent && selectionIsSane()) 287 triggerSelectedEvent(); 288 } 289 290 function selectionIsSane() { 291 var minSize = plot.getOptions().selection.minSize; 292 return Math.abs(selection.second.x - selection.first.x) >= minSize && 293 Math.abs(selection.second.y - selection.first.y) >= minSize; 294 } 295 296 plot.clearSelection = clearSelection; 297 plot.setSelection = setSelection; 298 plot.getSelection = getSelection; 299 300 plot.hooks.bindEvents.push(function(plot, eventHolder) { 301 var o = plot.getOptions(); 302 if (o.selection.mode != null) { 303 eventHolder.mousemove(onMouseMove); 304 eventHolder.mousedown(onMouseDown); 305 } 306 }); 307 308 309 plot.hooks.drawOverlay.push(function (plot, ctx) { 310 // draw selection 311 if (selection.show && selectionIsSane()) { 312 var plotOffset = plot.getPlotOffset(); 313 var o = plot.getOptions(); 314 315 ctx.save(); 316 ctx.translate(plotOffset.left, plotOffset.top); 317 318 var c = $.color.parse(o.selection.color); 319 320 ctx.strokeStyle = c.scale('a', 0.8).toString(); 321 ctx.lineWidth = 1; 322 ctx.lineJoin = o.selection.shape; 323 ctx.fillStyle = c.scale('a', 0.4).toString(); 324 325 var x = Math.min(selection.first.x, selection.second.x) + 0.5, 326 y = Math.min(selection.first.y, selection.second.y) + 0.5, 327 w = Math.abs(selection.second.x - selection.first.x) - 1, 328 h = Math.abs(selection.second.y - selection.first.y) - 1; 329 330 ctx.fillRect(x, y, w, h); 331 ctx.strokeRect(x, y, w, h); 332 333 ctx.restore(); 334 } 335 }); 336 337 plot.hooks.shutdown.push(function (plot, eventHolder) { 338 eventHolder.unbind("mousemove", onMouseMove); 339 eventHolder.unbind("mousedown", onMouseDown); 340 341 if (mouseUpHandler) 342 $(document).unbind("mouseup", mouseUpHandler); 343 }); 344 345 } 346 347 $.plot.plugins.push({ 348 init: init, 349 options: { 350 selection: { 351 mode: null, // one of null, "x", "y" or "xy" 352 color: "#e8cfac", 353 shape: "round", // one of "round", "miter", or "bevel" 354 minSize: 5 // minimum number of pixels 355 } 356 }, 357 name: 'selection', 358 version: '1.1' 359 }); 360})(jQuery); 361