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