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)
15 * A collection of functions to facilitate build custom interaction models.
18 Dygraph
.Interaction
= {};
21 * Called in response to an interaction model operation that
22 * should start the default panning behavior.
24 * It's used in the default callback for "mousedown" operations.
25 * Custom interaction model builders can use it to provide the default
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.
33 Dygraph
.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);
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!
44 var boundedLeftX
= g
.toDomXCoord(xExtremes
[0]) - maxXPixelsToDraw
;
45 var boundedRightX
= g
.toDomXCoord(xExtremes
[1]) + maxXPixelsToDraw
;
47 var boundedLeftDate
= g
.toDataXCoord(boundedLeftX
);
48 var boundedRightDate
= g
.toDataXCoord(boundedRightX
);
49 context
.boundedDates
= [boundedLeftDate
, boundedRightDate
];
51 var boundedValues
= [];
52 var maxYPixelsToDraw
= g
.height_
* g
.attr_("panEdgeFraction");
54 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
55 var axis
= g
.axes_
[i
];
56 var yExtremes
= axis
.extremeRange
;
58 var boundedTopY
= g
.toDomYCoord(yExtremes
[0], i
) + maxYPixelsToDraw
;
59 var boundedBottomY
= g
.toDomYCoord(yExtremes
[1], i
) - maxYPixelsToDraw
;
61 var boundedTopValue
= g
.toDataYCoord(boundedTopY
);
62 var boundedBottomValue
= g
.toDataYCoord(boundedBottomY
);
64 boundedValues
[i
] = [boundedTopValue
, boundedBottomValue
];
66 context
.boundedValues
= boundedValues
;
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.
78 axis
.initialTopValue
= Dygraph
.log10(yRange
[1]);
79 axis
.dragValueRange
= Dygraph
.log10(yRange
[1]) - Dygraph
.log10(yRange
[0]);
81 axis
.initialTopValue
= yRange
[1];
82 axis
.dragValueRange
= yRange
[1] - yRange
[0];
84 axis
.unitsPerPixel
= axis
.dragValueRange
/ (g
.plotter_
.area
.h
- 1);
86 // While calculating axes, set 2dpan.
87 if (axis
.valueWindow
|| axis
.valueRange
) context
.is2DPan
= true;
92 * Called in response to an interaction model operation that
93 * responds to an event that pans the view.
95 * It's used in the default callback for "mousemove" operations.
96 * Custom interaction model builders can use it to provide the default
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.
104 Dygraph
.Interaction
.movePan
= function(event
, g
, context
) {
105 context
.dragEndX
= g
.dragGetX_(event
, context
);
106 context
.dragEndY
= g
.dragGetY_(event
, context
);
108 var minDate
= context
.initialLeftmostDate
-
109 (context
.dragEndX
- context
.dragStartX
) * context
.xUnitsPerPixel
;
110 if (context
.boundedDates
) {
111 minDate
= Math
.max(minDate
, context
.boundedDates
[0]);
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
;
122 g
.dateWindow_
= [minDate
, maxDate
];
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
];
130 var pixelsDragged
= context
.dragEndY
- context
.dragStartY
;
131 var unitsDragged
= pixelsDragged
* axis
.unitsPerPixel
;
133 var boundedValue
= context
.boundedValues
? context
.boundedValues
[i
] : null;
135 // In log scale, maxValue and minValue are the logs of those values.
136 var maxValue
= axis
.initialTopValue
+ unitsDragged
;
138 maxValue
= Math
.min(maxValue
, boundedValue
[1]);
140 var minValue
= maxValue
- axis
.dragValueRange
;
142 if (minValue
< boundedValue
[0]) {
143 // Adjust maxValue, and recompute minValue.
144 maxValue
= maxValue
- (minValue
- boundedValue
[0]);
145 minValue
= maxValue
- axis
.dragValueRange
;
149 axis
.valueWindow
= [ Math
.pow(Dygraph
.LOG_SCALE
, minValue
),
150 Math
.pow(Dygraph
.LOG_SCALE
, maxValue
) ];
152 axis
.valueWindow
= [ minValue
, maxValue
];
161 * Called in response to an interaction model operation that
162 * responds to an event that ends panning.
164 * It's used in the default callback for "mouseup" operations.
165 * Custom interaction model builders can use it to provide the default
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.
173 Dygraph
.Interaction
.endPan
= function(event
, g
, context
) {
174 context
.dragEndX
= g
.dragGetX_(event
, context
);
175 context
.dragEndY
= g
.dragGetY_(event
, context
);
177 var regionWidth
= Math
.abs(context
.dragEndX
- context
.dragStartX
);
178 var regionHeight
= Math
.abs(context
.dragEndY
- context
.dragStartY
);
180 if (regionWidth
< 2 && regionHeight
< 2 &&
181 g
.lastx_
!= undefined
&& g
.lastx_
!= -1) {
182 Dygraph
.Interaction
.treatMouseOpAsClick(g
, event
, context
);
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;
199 * Called in response to an interaction model operation that
200 * responds to an event that starts zooming.
202 * It's used in the default callback for "mousedown" operations.
203 * Custom interaction model builders can use it to provide the default
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.
211 Dygraph
.Interaction
.startZoom
= function(event
, g
, context
) {
212 context
.isZooming
= true;
216 * Called in response to an interaction model operation that
217 * responds to an event that defines zoom boundaries.
219 * It's used in the default callback for "mousemove" operations.
220 * Custom interaction model builders can use it to provide the default
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.
228 Dygraph
.Interaction
.moveZoom
= function(event
, g
, context
) {
229 context
.dragEndX
= g
.dragGetX_(event
, context
);
230 context
.dragEndY
= g
.dragGetY_(event
, context
);
232 var xDelta
= Math
.abs(context
.dragStartX
- context
.dragEndX
);
233 var yDelta
= Math
.abs(context
.dragStartY
- context
.dragEndY
);
235 // drag direction threshold for y axis is twice as large as x axis
236 context
.dragDirection
= (xDelta
< yDelta
/ 2) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
239 context
.dragDirection
,
244 context
.prevDragDirection
,
248 context
.prevEndX
= context
.dragEndX
;
249 context
.prevEndY
= context
.dragEndY
;
250 context
.prevDragDirection
= context
.dragDirection
;
253 Dygraph
.Interaction
.treatMouseOpAsClick
= function(g
, event
, context
) {
254 var clickCallback
= g
.attr_('clickCallback');
255 var pointClickCallback
= g
.attr_('pointClickCallback');
257 var selectedPoint
= null;
259 // Find out if the click occurs on a point. This only matters if there's a pointClickCallback.
260 if (pointClickCallback
) {
262 var closestDistance
= Number
.MAX_VALUE
;
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);
269 if (!isNaN(distance
) &&
270 (closestIdx
== -1 || distance
< closestDistance
)) {
271 closestDistance
= distance
;
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
];
284 pointClickCallback(event
, selectedPoint
);
287 // TODO(danvk): pass along more info about the points, e.g. 'x'
289 clickCallback(event
, g
.lastx_
, g
.selPoints_
);
294 * Called in response to an interaction model operation that
295 * responds to an event that performs a zoom based on previously defined
298 * It's used in the default callback for "mouseup" operations.
299 * Custom interaction model builders can use it to provide the default
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.
307 Dygraph
.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
);
314 if (regionWidth
< 2 && regionHeight
< 2 &&
315 g
.lastx_
!= undefined
&& g
.lastx_
!= -1) {
316 Dygraph
.Interaction
.treatMouseOpAsClick(g
, event
, context
);
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
));
328 context
.dragStartX
= null;
329 context
.dragStartY
= null;
333 * Default interation model for dygraphs. You can refer to specific elements of
334 * this when constructing your own interaction model, e.g.:
336 * interactionModel: {
337 * mousedown: Dygraph.defaultInteractionModel.mousedown
341 Dygraph
.Interaction
.defaultModel
= {
342 // Track the beginning of drag events
343 mousedown
: function(event
, g
, context
) {
344 context
.initializeMouseDown(event
, g
, context
);
346 if (event
.altKey
|| event
.shiftKey
) {
347 Dygraph
.startPan(event
, g
, context
);
349 Dygraph
.startZoom(event
, g
, context
);
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
);
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
);
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;
378 // Disable zooming out if panning.
379 dblclick
: function(event
, g
, context
) {
380 if (event
.altKey
|| event
.shiftKey
) {
383 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
384 // friendlier to public use.
389 Dygraph
.DEFAULT_ATTRS
.interactionModel
= Dygraph
.Interaction
.defaultModel
;
391 // old ways of accessing these methods/properties
392 Dygraph
.defaultInteractionModel
= Dygraph
.Interaction
.defaultModel
;
393 Dygraph
.endZoom
= Dygraph
.Interaction
.endZoom
;
394 Dygraph
.moveZoom
= Dygraph
.Interaction
.moveZoom
;
395 Dygraph
.startZoom
= Dygraph
.Interaction
.startZoom
;
396 Dygraph
.endPan
= Dygraph
.Interaction
.endPan
;
397 Dygraph
.movePan
= Dygraph
.Interaction
.movePan
;
398 Dygraph
.startPan
= Dygraph
.Interaction
.startPan
;
400 Dygraph
.Interaction
.nonInteractiveModel_
= {
401 mousedown
: function(event
, g
, context
) {
402 context
.initializeMouseDown(event
, g
, context
);
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
);
411 if (regionWidth
< 2 && regionHeight
< 2 &&
412 g
.lastx_
!= undefined
&& g
.lastx_
!= -1) {
413 Dygraph
.Interaction
.treatMouseOpAsClick(g
, event
, context
);