3 * Copyright 2011 Robert Konigsberg (konigsberg@google.com)
4 * MIT-licensed (http://opensource.org/licenses/MIT)
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)
17 * A collection of functions to facilitate build custom interaction models.
20 Dygraph
.Interaction
= {};
23 * Called in response to an interaction model operation that
24 * should start the default panning behavior.
26 * It's used in the default callback for "mousedown" operations.
27 * Custom interaction model builders can use it to provide the default
30 * @param { Event } event the event object which led to the startPan call.
31 * @param { Dygraph} g The dygraph on which to act.
32 * @param { Object} context The dragging context object (with
33 * dragStartX/dragStartY/etc. properties). This function modifies the context.
35 Dygraph
.Interaction
.startPan
= function(event
, g
, context
) {
36 context
.isPanning
= true;
37 var xRange
= g
.xAxisRange();
38 context
.dateRange
= xRange
[1] - xRange
[0];
39 context
.initialLeftmostDate
= xRange
[0];
40 context
.xUnitsPerPixel
= context
.dateRange
/ (g
.plotter_
.area
.w
- 1);
42 if (g
.attr_("panEdgeFraction")) {
43 var maxXPixelsToDraw
= g
.width_
* g
.attr_("panEdgeFraction");
44 var xExtremes
= g
.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes!
46 var boundedLeftX
= g
.toDomXCoord(xExtremes
[0]) - maxXPixelsToDraw
;
47 var boundedRightX
= g
.toDomXCoord(xExtremes
[1]) + maxXPixelsToDraw
;
49 var boundedLeftDate
= g
.toDataXCoord(boundedLeftX
);
50 var boundedRightDate
= g
.toDataXCoord(boundedRightX
);
51 context
.boundedDates
= [boundedLeftDate
, boundedRightDate
];
53 var boundedValues
= [];
54 var maxYPixelsToDraw
= g
.height_
* g
.attr_("panEdgeFraction");
56 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
57 var axis
= g
.axes_
[i
];
58 var yExtremes
= axis
.extremeRange
;
60 var boundedTopY
= g
.toDomYCoord(yExtremes
[0], i
) + maxYPixelsToDraw
;
61 var boundedBottomY
= g
.toDomYCoord(yExtremes
[1], i
) - maxYPixelsToDraw
;
63 var boundedTopValue
= g
.toDataYCoord(boundedTopY
);
64 var boundedBottomValue
= g
.toDataYCoord(boundedBottomY
);
66 boundedValues
[i
] = [boundedTopValue
, boundedBottomValue
];
68 context
.boundedValues
= boundedValues
;
71 // Record the range of each y-axis at the start of the drag.
72 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
73 context
.is2DPan
= false;
74 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
75 var axis
= g
.axes_
[i
];
76 var yRange
= g
.yAxisRange(i
);
77 // TODO(konigsberg): These values should be in |context|.
78 // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
80 axis
.initialTopValue
= Dygraph
.log10(yRange
[1]);
81 axis
.dragValueRange
= Dygraph
.log10(yRange
[1]) - Dygraph
.log10(yRange
[0]);
83 axis
.initialTopValue
= yRange
[1];
84 axis
.dragValueRange
= yRange
[1] - yRange
[0];
86 axis
.unitsPerPixel
= axis
.dragValueRange
/ (g
.plotter_
.area
.h
- 1);
88 // While calculating axes, set 2dpan.
89 if (axis
.valueWindow
|| axis
.valueRange
) context
.is2DPan
= true;
94 * Called in response to an interaction model operation that
95 * responds to an event that pans the view.
97 * It's used in the default callback for "mousemove" operations.
98 * Custom interaction model builders can use it to provide the default
101 * @param { Event } event the event object which led to the movePan call.
102 * @param { Dygraph} g The dygraph on which to act.
103 * @param { Object} context The dragging context object (with
104 * dragStartX/dragStartY/etc. properties). This function modifies the context.
106 Dygraph
.Interaction
.movePan
= function(event
, g
, context
) {
107 context
.dragEndX
= g
.dragGetX_(event
, context
);
108 context
.dragEndY
= g
.dragGetY_(event
, context
);
110 var minDate
= context
.initialLeftmostDate
-
111 (context
.dragEndX
- context
.dragStartX
) * context
.xUnitsPerPixel
;
112 if (context
.boundedDates
) {
113 minDate
= Math
.max(minDate
, context
.boundedDates
[0]);
115 var maxDate
= minDate
+ context
.dateRange
;
116 if (context
.boundedDates
) {
117 if (maxDate
> context
.boundedDates
[1]) {
118 // Adjust minDate, and recompute maxDate.
119 minDate
= minDate
- (maxDate
- context
.boundedDates
[1]);
120 maxDate
= minDate
+ context
.dateRange
;
124 g
.dateWindow_
= [minDate
, maxDate
];
126 // y-axis scaling is automatic unless this is a full 2D pan.
127 if (context
.is2DPan
) {
128 // Adjust each axis appropriately.
129 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
130 var axis
= g
.axes_
[i
];
132 var pixelsDragged
= context
.dragEndY
- context
.dragStartY
;
133 var unitsDragged
= pixelsDragged
* axis
.unitsPerPixel
;
135 var boundedValue
= context
.boundedValues
? context
.boundedValues
[i
] : null;
137 // In log scale, maxValue and minValue are the logs of those values.
138 var maxValue
= axis
.initialTopValue
+ unitsDragged
;
140 maxValue
= Math
.min(maxValue
, boundedValue
[1]);
142 var minValue
= maxValue
- axis
.dragValueRange
;
144 if (minValue
< boundedValue
[0]) {
145 // Adjust maxValue, and recompute minValue.
146 maxValue
= maxValue
- (minValue
- boundedValue
[0]);
147 minValue
= maxValue
- axis
.dragValueRange
;
151 axis
.valueWindow
= [ Math
.pow(Dygraph
.LOG_SCALE
, minValue
),
152 Math
.pow(Dygraph
.LOG_SCALE
, maxValue
) ];
154 axis
.valueWindow
= [ minValue
, maxValue
];
163 * Called in response to an interaction model operation that
164 * responds to an event that ends panning.
166 * It's used in the default callback for "mouseup" operations.
167 * Custom interaction model builders can use it to provide the default
170 * @param { Event } event the event object which led to the startZoom call.
171 * @param { Dygraph} g The dygraph on which to act.
172 * @param { Object} context The dragging context object (with
173 * dragStartX/dragStartY/etc. properties). This function modifies the context.
175 Dygraph
.Interaction
.endPan
= function(event
, g
, context
) {
176 context
.dragEndX
= g
.dragGetX_(event
, context
);
177 context
.dragEndY
= g
.dragGetY_(event
, context
);
179 var regionWidth
= Math
.abs(context
.dragEndX
- context
.dragStartX
);
180 var regionHeight
= Math
.abs(context
.dragEndY
- context
.dragStartY
);
182 if (regionWidth
< 2 && regionHeight
< 2 &&
183 g
.lastx_
!= undefined
&& g
.lastx_
!= -1) {
184 Dygraph
.Interaction
.treatMouseOpAsClick(g
, event
, context
);
187 // TODO(konigsberg): Clear the context data from the axis.
188 // (replace with "context = {}" ?)
189 // TODO(konigsberg): mouseup should just delete the
190 // context object, and mousedown should create a new one.
191 context
.isPanning
= false;
192 context
.is2DPan
= false;
193 context
.initialLeftmostDate
= null;
194 context
.dateRange
= null;
195 context
.valueRange
= null;
196 context
.boundedDates
= null;
197 context
.boundedValues
= null;
201 * Called in response to an interaction model operation that
202 * responds to an event that starts zooming.
204 * It's used in the default callback for "mousedown" operations.
205 * Custom interaction model builders can use it to provide the default
208 * @param { Event } event the event object which led to the startZoom call.
209 * @param { Dygraph} g The dygraph on which to act.
210 * @param { Object} context The dragging context object (with
211 * dragStartX/dragStartY/etc. properties). This function modifies the context.
213 Dygraph
.Interaction
.startZoom
= function(event
, g
, context
) {
214 context
.isZooming
= true;
218 * Called in response to an interaction model operation that
219 * responds to an event that defines zoom boundaries.
221 * It's used in the default callback for "mousemove" operations.
222 * Custom interaction model builders can use it to provide the default
225 * @param { Event } event the event object which led to the moveZoom call.
226 * @param { Dygraph} g The dygraph on which to act.
227 * @param { Object} context The dragging context object (with
228 * dragStartX/dragStartY/etc. properties). This function modifies the context.
230 Dygraph
.Interaction
.moveZoom
= function(event
, g
, context
) {
231 context
.dragEndX
= g
.dragGetX_(event
, context
);
232 context
.dragEndY
= g
.dragGetY_(event
, context
);
234 var xDelta
= Math
.abs(context
.dragStartX
- context
.dragEndX
);
235 var yDelta
= Math
.abs(context
.dragStartY
- context
.dragEndY
);
237 // drag direction threshold for y axis is twice as large as x axis
238 context
.dragDirection
= (xDelta
< yDelta
/ 2) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
241 context
.dragDirection
,
246 context
.prevDragDirection
,
250 context
.prevEndX
= context
.dragEndX
;
251 context
.prevEndY
= context
.dragEndY
;
252 context
.prevDragDirection
= context
.dragDirection
;
255 Dygraph
.Interaction
.treatMouseOpAsClick
= function(g
, event
, context
) {
256 var clickCallback
= g
.attr_('clickCallback');
257 var pointClickCallback
= g
.attr_('pointClickCallback');
259 var selectedPoint
= null;
261 // Find out if the click occurs on a point. This only matters if there's a pointClickCallback.
262 if (pointClickCallback
) {
264 var closestDistance
= Number
.MAX_VALUE
;
266 // check if the click was on a particular point.
267 for (var i
= 0; i
< g
.selPoints_
.length
; i
++) {
268 var p
= g
.selPoints_
[i
];
269 var distance
= Math
.pow(p
.canvasx
- context
.dragEndX
, 2) +
270 Math
.pow(p
.canvasy
- context
.dragEndY
, 2);
271 if (!isNaN(distance
) &&
272 (closestIdx
== -1 || distance
< closestDistance
)) {
273 closestDistance
= distance
;
278 // Allow any click within two pixels of the dot.
279 var radius
= g
.attr_('highlightCircleSize') + 2;
280 if (closestDistance
<= radius
* radius
) {
281 selectedPoint
= g
.selPoints_
[closestIdx
];
286 pointClickCallback(event
, selectedPoint
);
289 // TODO(danvk): pass along more info about the points, e.g. 'x'
291 clickCallback(event
, g
.lastx_
, g
.selPoints_
);
296 * Called in response to an interaction model operation that
297 * responds to an event that performs a zoom based on previously defined
300 * It's used in the default callback for "mouseup" operations.
301 * Custom interaction model builders can use it to provide the default
304 * @param { Event } event the event object which led to the endZoom call.
305 * @param { Dygraph} g The dygraph on which to end the zoom.
306 * @param { Object} context The dragging context object (with
307 * dragStartX/dragStartY/etc. properties). This function modifies the context.
309 Dygraph
.Interaction
.endZoom
= function(event
, g
, context
) {
310 context
.isZooming
= false;
311 context
.dragEndX
= g
.dragGetX_(event
, context
);
312 context
.dragEndY
= g
.dragGetY_(event
, context
);
313 var regionWidth
= Math
.abs(context
.dragEndX
- context
.dragStartX
);
314 var regionHeight
= Math
.abs(context
.dragEndY
- context
.dragStartY
);
316 if (regionWidth
< 2 && regionHeight
< 2 &&
317 g
.lastx_
!= undefined
&& g
.lastx_
!= -1) {
318 Dygraph
.Interaction
.treatMouseOpAsClick(g
, event
, context
);
321 if (regionWidth
>= 10 && context
.dragDirection
== Dygraph
.HORIZONTAL
) {
322 g
.doZoomX_(Math
.min(context
.dragStartX
, context
.dragEndX
),
323 Math
.max(context
.dragStartX
, context
.dragEndX
));
324 } else if (regionHeight
>= 10 && context
.dragDirection
== Dygraph
.VERTICAL
) {
325 g
.doZoomY_(Math
.min(context
.dragStartY
, context
.dragEndY
),
326 Math
.max(context
.dragStartY
, context
.dragEndY
));
330 context
.dragStartX
= null;
331 context
.dragStartY
= null;
335 * Default interation model for dygraphs. You can refer to specific elements of
336 * this when constructing your own interaction model, e.g.:
338 * interactionModel: {
339 * mousedown: Dygraph.defaultInteractionModel.mousedown
343 Dygraph
.Interaction
.defaultModel
= {
344 // Track the beginning of drag events
345 mousedown
: function(event
, g
, context
) {
346 context
.initializeMouseDown(event
, g
, context
);
348 if (event
.altKey
|| event
.shiftKey
) {
349 Dygraph
.startPan(event
, g
, context
);
351 Dygraph
.startZoom(event
, g
, context
);
355 // Draw zoom rectangles when the mouse is down and the user moves around
356 mousemove
: function(event
, g
, context
) {
357 if (context
.isZooming
) {
358 Dygraph
.moveZoom(event
, g
, context
);
359 } else if (context
.isPanning
) {
360 Dygraph
.movePan(event
, g
, context
);
364 mouseup
: function(event
, g
, context
) {
365 if (context
.isZooming
) {
366 Dygraph
.endZoom(event
, g
, context
);
367 } else if (context
.isPanning
) {
368 Dygraph
.endPan(event
, g
, context
);
372 // Temporarily cancel the dragging event when the mouse leaves the graph
373 mouseout
: function(event
, g
, context
) {
374 if (context
.isZooming
) {
375 context
.dragEndX
= null;
376 context
.dragEndY
= null;
380 // Disable zooming out if panning.
381 dblclick
: function(event
, g
, context
) {
382 if (event
.altKey
|| event
.shiftKey
) {
385 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
386 // friendlier to public use.
391 Dygraph
.DEFAULT_ATTRS
.interactionModel
= Dygraph
.Interaction
.defaultModel
;
393 // old ways of accessing these methods/properties
394 Dygraph
.defaultInteractionModel
= Dygraph
.Interaction
.defaultModel
;
395 Dygraph
.endZoom
= Dygraph
.Interaction
.endZoom
;
396 Dygraph
.moveZoom
= Dygraph
.Interaction
.moveZoom
;
397 Dygraph
.startZoom
= Dygraph
.Interaction
.startZoom
;
398 Dygraph
.endPan
= Dygraph
.Interaction
.endPan
;
399 Dygraph
.movePan
= Dygraph
.Interaction
.movePan
;
400 Dygraph
.startPan
= Dygraph
.Interaction
.startPan
;
402 Dygraph
.Interaction
.nonInteractiveModel_
= {
403 mousedown
: function(event
, g
, context
) {
404 context
.initializeMouseDown(event
, g
, context
);
406 mouseup
: function(event
, g
, context
) {
407 // TODO(danvk): this logic is repeated in Dygraph.Interaction.endZoom
408 context
.dragEndX
= g
.dragGetX_(event
, context
);
409 context
.dragEndY
= g
.dragGetY_(event
, context
);
410 var regionWidth
= Math
.abs(context
.dragEndX
- context
.dragStartX
);
411 var regionHeight
= Math
.abs(context
.dragEndY
- context
.dragStartY
);
413 if (regionWidth
< 2 && regionHeight
< 2 &&
414 g
.lastx_
!= undefined
&& g
.lastx_
!= -1) {
415 Dygraph
.Interaction
.treatMouseOpAsClick(g
, event
, context
);