1 /* Flot plugin for selecting regions of a plot. |
|
2 |
|
3 Copyright (c) 2007-2014 IOLA and Ole Laursen. |
|
4 Licensed under the MIT license. |
|
5 |
|
6 The plugin supports these options: |
|
7 |
|
8 selection: { |
|
9 mode: null or "x" or "y" or "xy" or "smart", |
|
10 color: color, |
|
11 shape: "round" or "miter" or "bevel", |
|
12 visualization: "fill" or "focus", |
|
13 minSize: number of pixels |
|
14 } |
|
15 |
|
16 Selection support is enabled by setting the mode to one of "x", "y" or "xy". |
|
17 In "x" mode, the user will only be able to specify the x range, similarly for |
|
18 "y" mode. For "xy", the selection becomes a rectangle where both ranges can be |
|
19 specified. "color" is color of the selection (if you need to change the color |
|
20 later on, you can get to it with plot.getOptions().selection.color). "shape" |
|
21 is the shape of the corners of the selection. |
|
22 |
|
23 The way how the selection is visualized, can be changed by using the option |
|
24 "visualization". Flot currently supports two modes: "focus" and "fill". The |
|
25 option "focus" draws a colored bezel around the selected area while keeping |
|
26 the selected area clear. The option "fill" highlights (i.e., fills) the |
|
27 selected area with a colored highlight. |
|
28 |
|
29 "minSize" is the minimum size a selection can be in pixels. This value can |
|
30 be customized to determine the smallest size a selection can be and still |
|
31 have the selection rectangle be displayed. When customizing this value, the |
|
32 fact that it refers to pixels, not axis units must be taken into account. |
|
33 Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 |
|
34 minute, setting "minSize" to 1 will not make the minimum selection size 1 |
|
35 minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent |
|
36 "plotunselected" events from being fired when the user clicks the mouse without |
|
37 dragging. |
|
38 |
|
39 When selection support is enabled, a "plotselected" event will be emitted on |
|
40 the DOM element you passed into the plot function. The event handler gets a |
|
41 parameter with the ranges selected on the axes, like this: |
|
42 |
|
43 placeholder.bind( "plotselected", function( event, ranges ) { |
|
44 alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) |
|
45 // similar for yaxis - with multiple axes, the extra ones are in |
|
46 // x2axis, x3axis, ... |
|
47 }); |
|
48 |
|
49 The "plotselected" event is only fired when the user has finished making the |
|
50 selection. A "plotselecting" event is fired during the process with the same |
|
51 parameters as the "plotselected" event, in case you want to know what's |
|
52 happening while it's happening, |
|
53 |
|
54 A "plotunselected" event with no arguments is emitted when the user clicks the |
|
55 mouse to remove the selection. As stated above, setting "minSize" to 0 will |
|
56 destroy this behavior. |
|
57 |
|
58 The plugin allso adds the following methods to the plot object: |
|
59 |
|
60 - setSelection( ranges, preventEvent ) |
|
61 |
|
62 Set the selection rectangle. The passed in ranges is on the same form as |
|
63 returned in the "plotselected" event. If the selection mode is "x", you |
|
64 should put in either an xaxis range, if the mode is "y" you need to put in |
|
65 an yaxis range and both xaxis and yaxis if the selection mode is "xy", like |
|
66 this: |
|
67 |
|
68 setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); |
|
69 |
|
70 setSelection will trigger the "plotselected" event when called. If you don't |
|
71 want that to happen, e.g. if you're inside a "plotselected" handler, pass |
|
72 true as the second parameter. If you are using multiple axes, you can |
|
73 specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of |
|
74 xaxis, the plugin picks the first one it sees. |
|
75 |
|
76 - clearSelection( preventEvent ) |
|
77 |
|
78 Clear the selection rectangle. Pass in true to avoid getting a |
|
79 "plotunselected" event. |
|
80 |
|
81 - getSelection() |
|
82 |
|
83 Returns the current selection in the same format as the "plotselected" |
|
84 event. If there's currently no selection, the function returns null. |
|
85 |
|
86 */ |
|
87 |
|
88 (function ($) { |
|
89 function init(plot) { |
|
90 var selection = { |
|
91 first: {x: -1, y: -1}, |
|
92 second: {x: -1, y: -1}, |
|
93 show: false, |
|
94 currentMode: 'xy', |
|
95 active: false |
|
96 }; |
|
97 |
|
98 var SNAPPING_CONSTANT = $.plot.uiConstants.SNAPPING_CONSTANT; |
|
99 |
|
100 // FIXME: The drag handling implemented here should be |
|
101 // abstracted out, there's some similar code from a library in |
|
102 // the navigation plugin, this should be massaged a bit to fit |
|
103 // the Flot cases here better and reused. Doing this would |
|
104 // make this plugin much slimmer. |
|
105 var savedhandlers = {}; |
|
106 |
|
107 var mouseUpHandler = null; |
|
108 |
|
109 function onMouseMove(e) { |
|
110 if (selection.active) { |
|
111 updateSelection(e); |
|
112 |
|
113 plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); |
|
114 } |
|
115 } |
|
116 |
|
117 function onMouseDown(e) { |
|
118 var o = plot.getOptions(); |
|
119 // only accept left-click |
|
120 if (e.which !== 1 || o.selection.mode === null) return; |
|
121 |
|
122 // reinitialize currentMode |
|
123 selection.currentMode = 'xy'; |
|
124 |
|
125 // cancel out any text selections |
|
126 document.body.focus(); |
|
127 |
|
128 // prevent text selection and drag in old-school browsers |
|
129 if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { |
|
130 savedhandlers.onselectstart = document.onselectstart; |
|
131 document.onselectstart = function () { return false; }; |
|
132 } |
|
133 if (document.ondrag !== undefined && savedhandlers.ondrag == null) { |
|
134 savedhandlers.ondrag = document.ondrag; |
|
135 document.ondrag = function () { return false; }; |
|
136 } |
|
137 |
|
138 setSelectionPos(selection.first, e); |
|
139 |
|
140 selection.active = true; |
|
141 |
|
142 // this is a bit silly, but we have to use a closure to be |
|
143 // able to whack the same handler again |
|
144 mouseUpHandler = function (e) { onMouseUp(e); }; |
|
145 |
|
146 $(document).one("mouseup", mouseUpHandler); |
|
147 } |
|
148 |
|
149 function onMouseUp(e) { |
|
150 mouseUpHandler = null; |
|
151 |
|
152 // revert drag stuff for old-school browsers |
|
153 if (document.onselectstart !== undefined) { |
|
154 document.onselectstart = savedhandlers.onselectstart; |
|
155 } |
|
156 |
|
157 if (document.ondrag !== undefined) { |
|
158 document.ondrag = savedhandlers.ondrag; |
|
159 } |
|
160 |
|
161 // no more dragging |
|
162 selection.active = false; |
|
163 updateSelection(e); |
|
164 |
|
165 if (selectionIsSane()) { |
|
166 triggerSelectedEvent(); |
|
167 } else { |
|
168 // this counts as a clear |
|
169 plot.getPlaceholder().trigger("plotunselected", [ ]); |
|
170 plot.getPlaceholder().trigger("plotselecting", [ null ]); |
|
171 } |
|
172 |
|
173 return false; |
|
174 } |
|
175 |
|
176 function getSelection() { |
|
177 if (!selectionIsSane()) return null; |
|
178 |
|
179 if (!selection.show) return null; |
|
180 |
|
181 var r = {}, |
|
182 c1 = {x: selection.first.x, y: selection.first.y}, |
|
183 c2 = {x: selection.second.x, y: selection.second.y}; |
|
184 |
|
185 if (selectionDirection(plot) === 'x') { |
|
186 c1.y = 0; |
|
187 c2.y = plot.height(); |
|
188 } |
|
189 |
|
190 if (selectionDirection(plot) === 'y') { |
|
191 c1.x = 0; |
|
192 c2.x = plot.width(); |
|
193 } |
|
194 |
|
195 $.each(plot.getAxes(), function (name, axis) { |
|
196 if (axis.used) { |
|
197 var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); |
|
198 r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; |
|
199 } |
|
200 }); |
|
201 return r; |
|
202 } |
|
203 |
|
204 function triggerSelectedEvent() { |
|
205 var r = getSelection(); |
|
206 |
|
207 plot.getPlaceholder().trigger("plotselected", [ r ]); |
|
208 |
|
209 // backwards-compat stuff, to be removed in future |
|
210 if (r.xaxis && r.yaxis) { |
|
211 plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); |
|
212 } |
|
213 } |
|
214 |
|
215 function clamp(min, value, max) { |
|
216 return value < min ? min : (value > max ? max : value); |
|
217 } |
|
218 |
|
219 function selectionDirection(plot) { |
|
220 var o = plot.getOptions(); |
|
221 |
|
222 if (o.selection.mode === 'smart') { |
|
223 return selection.currentMode; |
|
224 } else { |
|
225 return o.selection.mode; |
|
226 } |
|
227 } |
|
228 |
|
229 function updateMode(pos) { |
|
230 if (selection.first) { |
|
231 var delta = { |
|
232 x: pos.x - selection.first.x, |
|
233 y: pos.y - selection.first.y |
|
234 }; |
|
235 |
|
236 if (Math.abs(delta.x) < SNAPPING_CONSTANT) { |
|
237 selection.currentMode = 'y'; |
|
238 } else if (Math.abs(delta.y) < SNAPPING_CONSTANT) { |
|
239 selection.currentMode = 'x'; |
|
240 } else { |
|
241 selection.currentMode = 'xy'; |
|
242 } |
|
243 } |
|
244 } |
|
245 |
|
246 function setSelectionPos(pos, e) { |
|
247 var offset = plot.getPlaceholder().offset(); |
|
248 var plotOffset = plot.getPlotOffset(); |
|
249 pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); |
|
250 pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); |
|
251 |
|
252 if (pos !== selection.first) updateMode(pos); |
|
253 |
|
254 if (selectionDirection(plot) === "y") { |
|
255 pos.x = pos === selection.first ? 0 : plot.width(); |
|
256 } |
|
257 |
|
258 if (selectionDirection(plot) === "x") { |
|
259 pos.y = pos === selection.first ? 0 : plot.height(); |
|
260 } |
|
261 } |
|
262 |
|
263 function updateSelection(pos) { |
|
264 if (pos.pageX == null) return; |
|
265 |
|
266 setSelectionPos(selection.second, pos); |
|
267 if (selectionIsSane()) { |
|
268 selection.show = true; |
|
269 plot.triggerRedrawOverlay(); |
|
270 } else clearSelection(true); |
|
271 } |
|
272 |
|
273 function clearSelection(preventEvent) { |
|
274 if (selection.show) { |
|
275 selection.show = false; |
|
276 selection.currentMode = ''; |
|
277 plot.triggerRedrawOverlay(); |
|
278 if (!preventEvent) { |
|
279 plot.getPlaceholder().trigger("plotunselected", [ ]); |
|
280 } |
|
281 } |
|
282 } |
|
283 |
|
284 // function taken from markings support in Flot |
|
285 function extractRange(ranges, coord) { |
|
286 var axis, from, to, key, axes = plot.getAxes(); |
|
287 |
|
288 for (var k in axes) { |
|
289 axis = axes[k]; |
|
290 if (axis.direction === coord) { |
|
291 key = coord + axis.n + "axis"; |
|
292 if (!ranges[key] && axis.n === 1) { |
|
293 // support x1axis as xaxis |
|
294 key = coord + "axis"; |
|
295 } |
|
296 |
|
297 if (ranges[key]) { |
|
298 from = ranges[key].from; |
|
299 to = ranges[key].to; |
|
300 break; |
|
301 } |
|
302 } |
|
303 } |
|
304 |
|
305 // backwards-compat stuff - to be removed in future |
|
306 if (!ranges[key]) { |
|
307 axis = coord === "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; |
|
308 from = ranges[coord + "1"]; |
|
309 to = ranges[coord + "2"]; |
|
310 } |
|
311 |
|
312 // auto-reverse as an added bonus |
|
313 if (from != null && to != null && from > to) { |
|
314 var tmp = from; |
|
315 from = to; |
|
316 to = tmp; |
|
317 } |
|
318 |
|
319 return { from: from, to: to, axis: axis }; |
|
320 } |
|
321 |
|
322 function setSelection(ranges, preventEvent) { |
|
323 var range; |
|
324 |
|
325 if (selectionDirection(plot) === "y") { |
|
326 selection.first.x = 0; |
|
327 selection.second.x = plot.width(); |
|
328 } else { |
|
329 range = extractRange(ranges, "x"); |
|
330 selection.first.x = range.axis.p2c(range.from); |
|
331 selection.second.x = range.axis.p2c(range.to); |
|
332 } |
|
333 |
|
334 if (selectionDirection(plot) === "x") { |
|
335 selection.first.y = 0; |
|
336 selection.second.y = plot.height(); |
|
337 } else { |
|
338 range = extractRange(ranges, "y"); |
|
339 selection.first.y = range.axis.p2c(range.from); |
|
340 selection.second.y = range.axis.p2c(range.to); |
|
341 } |
|
342 |
|
343 selection.show = true; |
|
344 plot.triggerRedrawOverlay(); |
|
345 if (!preventEvent && selectionIsSane()) { |
|
346 triggerSelectedEvent(); |
|
347 } |
|
348 } |
|
349 |
|
350 function selectionIsSane() { |
|
351 var minSize = plot.getOptions().selection.minSize; |
|
352 return Math.abs(selection.second.x - selection.first.x) >= minSize && |
|
353 Math.abs(selection.second.y - selection.first.y) >= minSize; |
|
354 } |
|
355 |
|
356 plot.clearSelection = clearSelection; |
|
357 plot.setSelection = setSelection; |
|
358 plot.getSelection = getSelection; |
|
359 |
|
360 plot.hooks.bindEvents.push(function(plot, eventHolder) { |
|
361 var o = plot.getOptions(); |
|
362 if (o.selection.mode != null) { |
|
363 eventHolder.mousemove(onMouseMove); |
|
364 eventHolder.mousedown(onMouseDown); |
|
365 } |
|
366 }); |
|
367 |
|
368 function drawSelectionDecorations(ctx, x, y, w, h, oX, oY, mode) { |
|
369 var spacing = 3; |
|
370 var fullEarWidth = 15; |
|
371 var earWidth = Math.max(0, Math.min(fullEarWidth, w / 2 - 2, h / 2 - 2)); |
|
372 ctx.fillStyle = '#ffffff'; |
|
373 |
|
374 if (mode === 'xy') { |
|
375 ctx.beginPath(); |
|
376 ctx.moveTo(x, y + earWidth); |
|
377 ctx.lineTo(x - 3, y + earWidth); |
|
378 ctx.lineTo(x - 3, y - 3); |
|
379 ctx.lineTo(x + earWidth, y - 3); |
|
380 ctx.lineTo(x + earWidth, y); |
|
381 ctx.lineTo(x, y); |
|
382 ctx.closePath(); |
|
383 |
|
384 ctx.moveTo(x, y + h - earWidth); |
|
385 ctx.lineTo(x - 3, y + h - earWidth); |
|
386 ctx.lineTo(x - 3, y + h + 3); |
|
387 ctx.lineTo(x + earWidth, y + h + 3); |
|
388 ctx.lineTo(x + earWidth, y + h); |
|
389 ctx.lineTo(x, y + h); |
|
390 ctx.closePath(); |
|
391 |
|
392 ctx.moveTo(x + w, y + earWidth); |
|
393 ctx.lineTo(x + w + 3, y + earWidth); |
|
394 ctx.lineTo(x + w + 3, y - 3); |
|
395 ctx.lineTo(x + w - earWidth, y - 3); |
|
396 ctx.lineTo(x + w - earWidth, y); |
|
397 ctx.lineTo(x + w, y); |
|
398 ctx.closePath(); |
|
399 |
|
400 ctx.moveTo(x + w, y + h - earWidth); |
|
401 ctx.lineTo(x + w + 3, y + h - earWidth); |
|
402 ctx.lineTo(x + w + 3, y + h + 3); |
|
403 ctx.lineTo(x + w - earWidth, y + h + 3); |
|
404 ctx.lineTo(x + w - earWidth, y + h); |
|
405 ctx.lineTo(x + w, y + h); |
|
406 ctx.closePath(); |
|
407 |
|
408 ctx.stroke(); |
|
409 ctx.fill(); |
|
410 } |
|
411 |
|
412 x = oX; |
|
413 y = oY; |
|
414 |
|
415 if (mode === 'x') { |
|
416 ctx.beginPath(); |
|
417 ctx.moveTo(x, y + fullEarWidth); |
|
418 ctx.lineTo(x, y - fullEarWidth); |
|
419 ctx.lineTo(x - spacing, y - fullEarWidth); |
|
420 ctx.lineTo(x - spacing, y + fullEarWidth); |
|
421 ctx.closePath(); |
|
422 |
|
423 ctx.moveTo(x + w, y + fullEarWidth); |
|
424 ctx.lineTo(x + w, y - fullEarWidth); |
|
425 ctx.lineTo(x + w + spacing, y - fullEarWidth); |
|
426 ctx.lineTo(x + w + spacing, y + fullEarWidth); |
|
427 ctx.closePath(); |
|
428 ctx.stroke(); |
|
429 ctx.fill(); |
|
430 } |
|
431 |
|
432 if (mode === 'y') { |
|
433 ctx.beginPath(); |
|
434 |
|
435 ctx.moveTo(x - fullEarWidth, y); |
|
436 ctx.lineTo(x + fullEarWidth, y); |
|
437 ctx.lineTo(x + fullEarWidth, y - spacing); |
|
438 ctx.lineTo(x - fullEarWidth, y - spacing); |
|
439 ctx.closePath(); |
|
440 |
|
441 ctx.moveTo(x - fullEarWidth, y + h); |
|
442 ctx.lineTo(x + fullEarWidth, y + h); |
|
443 ctx.lineTo(x + fullEarWidth, y + h + spacing); |
|
444 ctx.lineTo(x - fullEarWidth, y + h + spacing); |
|
445 ctx.closePath(); |
|
446 ctx.stroke(); |
|
447 ctx.fill(); |
|
448 } |
|
449 } |
|
450 |
|
451 plot.hooks.drawOverlay.push(function (plot, ctx) { |
|
452 // draw selection |
|
453 if (selection.show && selectionIsSane()) { |
|
454 var plotOffset = plot.getPlotOffset(); |
|
455 var o = plot.getOptions(); |
|
456 |
|
457 ctx.save(); |
|
458 ctx.translate(plotOffset.left, plotOffset.top); |
|
459 |
|
460 var c = $.color.parse(o.selection.color); |
|
461 var visualization = o.selection.visualization; |
|
462 |
|
463 var scalingFactor = 1; |
|
464 |
|
465 // use a dimmer scaling factor if visualization is "fill" |
|
466 if (visualization === "fill") { |
|
467 scalingFactor = 0.8; |
|
468 } |
|
469 |
|
470 ctx.strokeStyle = c.scale('a', scalingFactor).toString(); |
|
471 ctx.lineWidth = 1; |
|
472 ctx.lineJoin = o.selection.shape; |
|
473 ctx.fillStyle = c.scale('a', 0.4).toString(); |
|
474 |
|
475 var x = Math.min(selection.first.x, selection.second.x) + 0.5, |
|
476 oX = x, |
|
477 y = Math.min(selection.first.y, selection.second.y) + 0.5, |
|
478 oY = y, |
|
479 w = Math.abs(selection.second.x - selection.first.x) - 1, |
|
480 h = Math.abs(selection.second.y - selection.first.y) - 1; |
|
481 |
|
482 if (selectionDirection(plot) === 'x') { |
|
483 h += y; |
|
484 y = 0; |
|
485 } |
|
486 |
|
487 if (selectionDirection(plot) === 'y') { |
|
488 w += x; |
|
489 x = 0; |
|
490 } |
|
491 |
|
492 if (visualization === "fill") { |
|
493 ctx.fillRect(x, y, w, h); |
|
494 ctx.strokeRect(x, y, w, h); |
|
495 } else { |
|
496 ctx.fillRect(0, 0, plot.width(), plot.height()); |
|
497 ctx.clearRect(x, y, w, h); |
|
498 drawSelectionDecorations(ctx, x, y, w, h, oX, oY, selectionDirection(plot)); |
|
499 } |
|
500 |
|
501 ctx.restore(); |
|
502 } |
|
503 }); |
|
504 |
|
505 plot.hooks.shutdown.push(function (plot, eventHolder) { |
|
506 eventHolder.unbind("mousemove", onMouseMove); |
|
507 eventHolder.unbind("mousedown", onMouseDown); |
|
508 |
|
509 if (mouseUpHandler) { |
|
510 $(document).unbind("mouseup", mouseUpHandler); |
|
511 } |
|
512 }); |
|
513 } |
|
514 |
|
515 $.plot.plugins.push({ |
|
516 init: init, |
|
517 options: { |
|
518 selection: { |
|
519 mode: null, // one of null, "x", "y" or "xy" |
|
520 visualization: "focus", // "focus" or "fill" |
|
521 color: "#888888", |
|
522 shape: "round", // one of "round", "miter", or "bevel" |
|
523 minSize: 5 // minimum number of pixels |
|
524 } |
|
525 }, |
|
526 name: 'selection', |
|
527 version: '1.1' |
|
528 }); |
|
529 })(jQuery); |
|