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