Fix issue 186 (dygraphs does not accept "1e-5" as a numeric x-value).
[dygraphs.git] / dygraph-interaction-model.js
CommitLineData
88e95c46
DV
1/**
2 * @license
3 * Copyright 2011 Robert Konigsberg (konigsberg@google.com)
4 * MIT-licensed (http://opensource.org/licenses/MIT)
5 */
846f3d2d 6
88e95c46 7/**
846f3d2d
DV
8 * @fileoverview The default interaction model for Dygraphs. This is kept out
9 * of dygraph.js for better navigability.
10 * @author Robert Konigsberg (konigsberg@google.com)
11 */
12
13
14/**
15 * A collection of functions to facilitate build custom interaction models.
16 * @class
17 */
18Dygraph.Interaction = {};
19
20/**
21 * Called in response to an interaction model operation that
22 * should start the default panning behavior.
23 *
24 * It's used in the default callback for "mousedown" operations.
25 * Custom interaction model builders can use it to provide the default
26 * panning behavior.
27 *
28 * @param { Event } event the event object which led to the startPan call.
29 * @param { Dygraph} g The dygraph on which to act.
30 * @param { Object} context The dragging context object (with
31 * dragStartX/dragStartY/etc. properties). This function modifies the context.
32 */
33Dygraph.Interaction.startPan = function(event, g, context) {
34 context.isPanning = true;
35 var xRange = g.xAxisRange();
36 context.dateRange = xRange[1] - xRange[0];
37 context.initialLeftmostDate = xRange[0];
38 context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
39
40 if (g.attr_("panEdgeFraction")) {
41 var maxXPixelsToDraw = g.width_ * g.attr_("panEdgeFraction");
42 var xExtremes = g.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes!
43
44 var boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw;
45 var boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw;
46
47 var boundedLeftDate = g.toDataXCoord(boundedLeftX);
48 var boundedRightDate = g.toDataXCoord(boundedRightX);
49 context.boundedDates = [boundedLeftDate, boundedRightDate];
50
51 var boundedValues = [];
52 var maxYPixelsToDraw = g.height_ * g.attr_("panEdgeFraction");
53
54 for (var i = 0; i < g.axes_.length; i++) {
55 var axis = g.axes_[i];
56 var yExtremes = axis.extremeRange;
57
58 var boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw;
59 var boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw;
60
61 var boundedTopValue = g.toDataYCoord(boundedTopY);
62 var boundedBottomValue = g.toDataYCoord(boundedBottomY);
63
64 boundedValues[i] = [boundedTopValue, boundedBottomValue];
65 }
66 context.boundedValues = boundedValues;
67 }
68
69 // Record the range of each y-axis at the start of the drag.
70 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
71 context.is2DPan = false;
72 for (var i = 0; i < g.axes_.length; i++) {
73 var axis = g.axes_[i];
74 var yRange = g.yAxisRange(i);
75 // TODO(konigsberg): These values should be in |context|.
76 // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
77 if (axis.logscale) {
78 axis.initialTopValue = Dygraph.log10(yRange[1]);
79 axis.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]);
80 } else {
81 axis.initialTopValue = yRange[1];
82 axis.dragValueRange = yRange[1] - yRange[0];
83 }
84 axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1);
85
86 // While calculating axes, set 2dpan.
87 if (axis.valueWindow || axis.valueRange) context.is2DPan = true;
88 }
89};
90
91/**
92 * Called in response to an interaction model operation that
93 * responds to an event that pans the view.
94 *
95 * It's used in the default callback for "mousemove" operations.
96 * Custom interaction model builders can use it to provide the default
97 * panning behavior.
98 *
99 * @param { Event } event the event object which led to the movePan call.
100 * @param { Dygraph} g The dygraph on which to act.
101 * @param { Object} context The dragging context object (with
102 * dragStartX/dragStartY/etc. properties). This function modifies the context.
103 */
104Dygraph.Interaction.movePan = function(event, g, context) {
105 context.dragEndX = g.dragGetX_(event, context);
106 context.dragEndY = g.dragGetY_(event, context);
107
108 var minDate = context.initialLeftmostDate -
109 (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
110 if (context.boundedDates) {
111 minDate = Math.max(minDate, context.boundedDates[0]);
112 }
113 var maxDate = minDate + context.dateRange;
114 if (context.boundedDates) {
115 if (maxDate > context.boundedDates[1]) {
116 // Adjust minDate, and recompute maxDate.
117 minDate = minDate - (maxDate - context.boundedDates[1]);
118 maxDate = minDate + context.dateRange;
119 }
120 }
121
122 g.dateWindow_ = [minDate, maxDate];
123
124 // y-axis scaling is automatic unless this is a full 2D pan.
125 if (context.is2DPan) {
126 // Adjust each axis appropriately.
127 for (var i = 0; i < g.axes_.length; i++) {
128 var axis = g.axes_[i];
129
130 var pixelsDragged = context.dragEndY - context.dragStartY;
131 var unitsDragged = pixelsDragged * axis.unitsPerPixel;
920208fb 132
846f3d2d
DV
133 var boundedValue = context.boundedValues ? context.boundedValues[i] : null;
134
135 // In log scale, maxValue and minValue are the logs of those values.
136 var maxValue = axis.initialTopValue + unitsDragged;
137 if (boundedValue) {
138 maxValue = Math.min(maxValue, boundedValue[1]);
139 }
140 var minValue = maxValue - axis.dragValueRange;
141 if (boundedValue) {
142 if (minValue < boundedValue[0]) {
143 // Adjust maxValue, and recompute minValue.
144 maxValue = maxValue - (minValue - boundedValue[0]);
145 minValue = maxValue - axis.dragValueRange;
146 }
147 }
148 if (axis.logscale) {
149 axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue),
150 Math.pow(Dygraph.LOG_SCALE, maxValue) ];
151 } else {
152 axis.valueWindow = [ minValue, maxValue ];
153 }
154 }
155 }
156
157 g.drawGraph_(false);
158};
159
160/**
161 * Called in response to an interaction model operation that
162 * responds to an event that ends panning.
163 *
164 * It's used in the default callback for "mouseup" operations.
165 * Custom interaction model builders can use it to provide the default
166 * panning behavior.
167 *
168 * @param { Event } event the event object which led to the startZoom call.
169 * @param { Dygraph} g The dygraph on which to act.
170 * @param { Object} context The dragging context object (with
171 * dragStartX/dragStartY/etc. properties). This function modifies the context.
172 */
173Dygraph.Interaction.endPan = function(event, g, context) {
174 context.dragEndX = g.dragGetX_(event, context);
175 context.dragEndY = g.dragGetY_(event, context);
176
177 var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
178 var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
179
180 if (regionWidth < 2 && regionHeight < 2 &&
181 g.lastx_ != undefined && g.lastx_ != -1) {
182 Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
183 }
184
185 // TODO(konigsberg): Clear the context data from the axis.
186 // (replace with "context = {}" ?)
187 // TODO(konigsberg): mouseup should just delete the
188 // context object, and mousedown should create a new one.
189 context.isPanning = false;
190 context.is2DPan = false;
191 context.initialLeftmostDate = null;
192 context.dateRange = null;
193 context.valueRange = null;
194 context.boundedDates = null;
195 context.boundedValues = null;
196};
197
198/**
199 * Called in response to an interaction model operation that
200 * responds to an event that starts zooming.
201 *
202 * It's used in the default callback for "mousedown" operations.
203 * Custom interaction model builders can use it to provide the default
204 * zooming behavior.
205 *
206 * @param { Event } event the event object which led to the startZoom call.
207 * @param { Dygraph} g The dygraph on which to act.
208 * @param { Object} context The dragging context object (with
209 * dragStartX/dragStartY/etc. properties). This function modifies the context.
210 */
211Dygraph.Interaction.startZoom = function(event, g, context) {
212 context.isZooming = true;
213};
214
215/**
216 * Called in response to an interaction model operation that
217 * responds to an event that defines zoom boundaries.
218 *
219 * It's used in the default callback for "mousemove" operations.
220 * Custom interaction model builders can use it to provide the default
221 * zooming behavior.
222 *
223 * @param { Event } event the event object which led to the moveZoom call.
224 * @param { Dygraph} g The dygraph on which to act.
225 * @param { Object} context The dragging context object (with
226 * dragStartX/dragStartY/etc. properties). This function modifies the context.
227 */
228Dygraph.Interaction.moveZoom = function(event, g, context) {
229 context.dragEndX = g.dragGetX_(event, context);
230 context.dragEndY = g.dragGetY_(event, context);
231
232 var xDelta = Math.abs(context.dragStartX - context.dragEndX);
233 var yDelta = Math.abs(context.dragStartY - context.dragEndY);
234
235 // drag direction threshold for y axis is twice as large as x axis
236 context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
237
238 g.drawZoomRect_(
239 context.dragDirection,
240 context.dragStartX,
241 context.dragEndX,
242 context.dragStartY,
243 context.dragEndY,
244 context.prevDragDirection,
245 context.prevEndX,
246 context.prevEndY);
247
248 context.prevEndX = context.dragEndX;
249 context.prevEndY = context.dragEndY;
250 context.prevDragDirection = context.dragDirection;
251};
252
253Dygraph.Interaction.treatMouseOpAsClick = function(g, event, context) {
254 var clickCallback = g.attr_('clickCallback');
255 var pointClickCallback = g.attr_('pointClickCallback');
256
257 var selectedPoint = null;
258
259 // Find out if the click occurs on a point. This only matters if there's a pointClickCallback.
260 if (pointClickCallback) {
261 var closestIdx = -1;
262 var closestDistance = Number.MAX_VALUE;
263
264 // check if the click was on a particular point.
265 for (var i = 0; i < g.selPoints_.length; i++) {
266 var p = g.selPoints_[i];
267 var distance = Math.pow(p.canvasx - context.dragEndX, 2) +
268 Math.pow(p.canvasy - context.dragEndY, 2);
fbff6d71
DV
269 if (!isNaN(distance) &&
270 (closestIdx == -1 || distance < closestDistance)) {
846f3d2d
DV
271 closestDistance = distance;
272 closestIdx = i;
273 }
274 }
275
276 // Allow any click within two pixels of the dot.
277 var radius = g.attr_('highlightCircleSize') + 2;
278 if (closestDistance <= radius * radius) {
279 selectedPoint = g.selPoints_[closestIdx];
280 }
281 }
282
283 if (selectedPoint) {
284 pointClickCallback(event, selectedPoint);
285 }
286
287 // TODO(danvk): pass along more info about the points, e.g. 'x'
288 if (clickCallback) {
289 clickCallback(event, g.lastx_, g.selPoints_);
290 }
291};
292
293/**
294 * Called in response to an interaction model operation that
295 * responds to an event that performs a zoom based on previously defined
296 * bounds..
297 *
298 * It's used in the default callback for "mouseup" operations.
299 * Custom interaction model builders can use it to provide the default
300 * zooming behavior.
301 *
302 * @param { Event } event the event object which led to the endZoom call.
303 * @param { Dygraph} g The dygraph on which to end the zoom.
304 * @param { Object} context The dragging context object (with
305 * dragStartX/dragStartY/etc. properties). This function modifies the context.
306 */
307Dygraph.Interaction.endZoom = function(event, g, context) {
308 context.isZooming = false;
309 context.dragEndX = g.dragGetX_(event, context);
310 context.dragEndY = g.dragGetY_(event, context);
311 var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
312 var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
313
314 if (regionWidth < 2 && regionHeight < 2 &&
315 g.lastx_ != undefined && g.lastx_ != -1) {
316 Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
317 }
318
319 if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) {
320 g.doZoomX_(Math.min(context.dragStartX, context.dragEndX),
321 Math.max(context.dragStartX, context.dragEndX));
322 } else if (regionHeight >= 10 && context.dragDirection == Dygraph.VERTICAL) {
323 g.doZoomY_(Math.min(context.dragStartY, context.dragEndY),
324 Math.max(context.dragStartY, context.dragEndY));
325 } else {
920208fb 326 g.clearZoomRect_();
846f3d2d
DV
327 }
328 context.dragStartX = null;
329 context.dragStartY = null;
330};
331
332/**
333 * Default interation model for dygraphs. You can refer to specific elements of
334 * this when constructing your own interaction model, e.g.:
335 * g.updateOptions( {
336 * interactionModel: {
337 * mousedown: Dygraph.defaultInteractionModel.mousedown
338 * }
339 * } );
340 */
341Dygraph.Interaction.defaultModel = {
342 // Track the beginning of drag events
343 mousedown: function(event, g, context) {
344 context.initializeMouseDown(event, g, context);
345
346 if (event.altKey || event.shiftKey) {
347 Dygraph.startPan(event, g, context);
348 } else {
349 Dygraph.startZoom(event, g, context);
350 }
351 },
352
353 // Draw zoom rectangles when the mouse is down and the user moves around
354 mousemove: function(event, g, context) {
355 if (context.isZooming) {
356 Dygraph.moveZoom(event, g, context);
357 } else if (context.isPanning) {
358 Dygraph.movePan(event, g, context);
359 }
360 },
361
362 mouseup: function(event, g, context) {
363 if (context.isZooming) {
364 Dygraph.endZoom(event, g, context);
365 } else if (context.isPanning) {
366 Dygraph.endPan(event, g, context);
367 }
368 },
369
370 // Temporarily cancel the dragging event when the mouse leaves the graph
371 mouseout: function(event, g, context) {
372 if (context.isZooming) {
373 context.dragEndX = null;
374 context.dragEndY = null;
375 }
376 },
377
378 // Disable zooming out if panning.
379 dblclick: function(event, g, context) {
380 if (event.altKey || event.shiftKey) {
381 return;
382 }
383 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
384 // friendlier to public use.
385 g.doUnzoom_();
386 }
387};
388
389Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.Interaction.defaultModel;
390
391// old ways of accessing these methods/properties
392Dygraph.defaultInteractionModel = Dygraph.Interaction.defaultModel;
393Dygraph.endZoom = Dygraph.Interaction.endZoom;
394Dygraph.moveZoom = Dygraph.Interaction.moveZoom;
395Dygraph.startZoom = Dygraph.Interaction.startZoom;
396Dygraph.endPan = Dygraph.Interaction.endPan;
397Dygraph.movePan = Dygraph.Interaction.movePan;
398Dygraph.startPan = Dygraph.Interaction.startPan;
399
0290d079 400Dygraph.Interaction.nonInteractiveModel_ = {
027e9e9b
DV
401 mousedown: function(event, g, context) {
402 context.initializeMouseDown(event, g, context);
403 },
404 mouseup: function(event, g, context) {
405 // TODO(danvk): this logic is repeated in Dygraph.Interaction.endZoom
406 context.dragEndX = g.dragGetX_(event, context);
407 context.dragEndY = g.dragGetY_(event, context);
408 var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
409 var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
410
411 if (regionWidth < 2 && regionHeight < 2 &&
412 g.lastx_ != undefined && g.lastx_ != -1) {
413 Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
414 }
415 }
416};