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)
13 /*jshint globalstrict: true */
14 /*global Dygraph:false */
18 * A collection of functions to facilitate build custom interaction models.
21 Dygraph
.Interaction
= {};
24 * Called in response to an interaction model operation that
25 * should start the default panning behavior.
27 * It's used in the default callback for "mousedown" operations.
28 * Custom interaction model builders can use it to provide the default
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.
36 Dygraph
.Interaction
.startPan
= function(event
, g
, context
) {
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);
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!
48 var boundedLeftX
= g
.toDomXCoord(xExtremes
[0]) - maxXPixelsToDraw
;
49 var boundedRightX
= g
.toDomXCoord(xExtremes
[1]) + maxXPixelsToDraw
;
51 var boundedLeftDate
= g
.toDataXCoord(boundedLeftX
);
52 var boundedRightDate
= g
.toDataXCoord(boundedRightX
);
53 context
.boundedDates
= [boundedLeftDate
, boundedRightDate
];
55 var boundedValues
= [];
56 var maxYPixelsToDraw
= g
.height_
* g
.attr_("panEdgeFraction");
58 for (i
= 0; i
< g
.axes_
.length
; i
++) {
60 var yExtremes
= axis
.extremeRange
;
62 var boundedTopY
= g
.toDomYCoord(yExtremes
[0], i
) + maxYPixelsToDraw
;
63 var boundedBottomY
= g
.toDomYCoord(yExtremes
[1], i
) - maxYPixelsToDraw
;
65 var boundedTopValue
= g
.toDataYCoord(boundedTopY
);
66 var boundedBottomValue
= g
.toDataYCoord(boundedBottomY
);
68 boundedValues
[i
] = [boundedTopValue
, boundedBottomValue
];
70 context
.boundedValues
= boundedValues
;
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;
76 for (i
= 0; i
< g
.axes_
.length
; i
++) {
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.
82 axis
.initialTopValue
= Dygraph
.log10(yRange
[1]);
83 axis
.dragValueRange
= Dygraph
.log10(yRange
[1]) - Dygraph
.log10(yRange
[0]);
85 axis
.initialTopValue
= yRange
[1];
86 axis
.dragValueRange
= yRange
[1] - yRange
[0];
88 axis
.unitsPerPixel
= axis
.dragValueRange
/ (g
.plotter_
.area
.h
- 1);
90 // While calculating axes, set 2dpan.
91 if (axis
.valueWindow
|| axis
.valueRange
) context
.is2DPan
= true;
96 * Called in response to an interaction model operation that
97 * responds to an event that pans the view.
99 * It's used in the default callback for "mousemove" operations.
100 * Custom interaction model builders can use it to provide the default
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.
108 Dygraph
.Interaction
.movePan
= function(event
, g
, context
) {
109 context
.dragEndX
= g
.dragGetX_(event
, context
);
110 context
.dragEndY
= g
.dragGetY_(event
, context
);
112 var minDate
= context
.initialLeftmostDate
-
113 (context
.dragEndX
- context
.dragStartX
) * context
.xUnitsPerPixel
;
114 if (context
.boundedDates
) {
115 minDate
= Math
.max(minDate
, context
.boundedDates
[0]);
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
;
126 g
.dateWindow_
= [minDate
, maxDate
];
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
];
134 var pixelsDragged
= context
.dragEndY
- context
.dragStartY
;
135 var unitsDragged
= pixelsDragged
* axis
.unitsPerPixel
;
137 var boundedValue
= context
.boundedValues
? context
.boundedValues
[i
] : null;
139 // In log scale, maxValue and minValue are the logs of those values.
140 var maxValue
= axis
.initialTopValue
+ unitsDragged
;
142 maxValue
= Math
.min(maxValue
, boundedValue
[1]);
144 var minValue
= maxValue
- axis
.dragValueRange
;
146 if (minValue
< boundedValue
[0]) {
147 // Adjust maxValue, and recompute minValue.
148 maxValue
= maxValue
- (minValue
- boundedValue
[0]);
149 minValue
= maxValue
- axis
.dragValueRange
;
153 axis
.valueWindow
= [ Math
.pow(Dygraph
.LOG_SCALE
, minValue
),
154 Math
.pow(Dygraph
.LOG_SCALE
, maxValue
) ];
156 axis
.valueWindow
= [ minValue
, maxValue
];
165 * Called in response to an interaction model operation that
166 * responds to an event that ends panning.
168 * It's used in the default callback for "mouseup" operations.
169 * Custom interaction model builders can use it to provide the default
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.
177 Dygraph
.Interaction
.endPan
= function(event
, g
, context
) {
178 context
.dragEndX
= g
.dragGetX_(event
, context
);
179 context
.dragEndY
= g
.dragGetY_(event
, context
);
181 var regionWidth
= Math
.abs(context
.dragEndX
- context
.dragStartX
);
182 var regionHeight
= Math
.abs(context
.dragEndY
- context
.dragStartY
);
184 if (regionWidth
< 2 && regionHeight
< 2 &&
185 g
.lastx_
!== undefined
&& g
.lastx_
!= -1) {
186 Dygraph
.Interaction
.treatMouseOpAsClick(g
, event
, context
);
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;
203 * Called in response to an interaction model operation that
204 * responds to an event that starts zooming.
206 * It's used in the default callback for "mousedown" operations.
207 * Custom interaction model builders can use it to provide the default
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.
215 Dygraph
.Interaction
.startZoom
= function(event
, g
, context
) {
216 context
.isZooming
= true;
220 * Called in response to an interaction model operation that
221 * responds to an event that defines zoom boundaries.
223 * It's used in the default callback for "mousemove" operations.
224 * Custom interaction model builders can use it to provide the default
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.
232 Dygraph
.Interaction
.moveZoom
= function(event
, g
, context
) {
233 context
.dragEndX
= g
.dragGetX_(event
, context
);
234 context
.dragEndY
= g
.dragGetY_(event
, context
);
236 var xDelta
= Math
.abs(context
.dragStartX
- context
.dragEndX
);
237 var yDelta
= Math
.abs(context
.dragStartY
- context
.dragEndY
);
239 // drag direction threshold for y axis is twice as large as x axis
240 context
.dragDirection
= (xDelta
< yDelta
/ 2) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
243 context
.dragDirection
,
248 context
.prevDragDirection
,
252 context
.prevEndX
= context
.dragEndX
;
253 context
.prevEndY
= context
.dragEndY
;
254 context
.prevDragDirection
= context
.dragDirection
;
257 Dygraph
.Interaction
.treatMouseOpAsClick
= function(g
, event
, context
) {
258 var clickCallback
= g
.attr_('clickCallback');
259 var pointClickCallback
= g
.attr_('pointClickCallback');
261 var selectedPoint
= null;
263 // Find out if the click occurs on a point. This only matters if there's a pointClickCallback.
264 if (pointClickCallback
) {
266 var closestDistance
= Number
.MAX_VALUE
;
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);
273 if (!isNaN(distance
) &&
274 (closestIdx
== -1 || distance
< closestDistance
)) {
275 closestDistance
= distance
;
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
];
288 pointClickCallback(event
, selectedPoint
);
291 // TODO(danvk): pass along more info about the points, e.g. 'x'
293 clickCallback(event
, g
.lastx_
, g
.selPoints_
);
298 * Called in response to an interaction model operation that
299 * responds to an event that performs a zoom based on previously defined
302 * It's used in the default callback for "mouseup" operations.
303 * Custom interaction model builders can use it to provide the default
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.
311 Dygraph
.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
);
318 if (regionWidth
< 2 && regionHeight
< 2 &&
319 g
.lastx_
!== undefined
&& g
.lastx_
!= -1) {
320 Dygraph
.Interaction
.treatMouseOpAsClick(g
, event
, context
);
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
));
332 context
.dragStartX
= null;
333 context
.dragStartY
= null;
337 * Default interation model for dygraphs. You can refer to specific elements of
338 * this when constructing your own interaction model, e.g.:
340 * interactionModel: {
341 * mousedown: Dygraph.defaultInteractionModel.mousedown
345 Dygraph
.Interaction
.defaultModel
= {
346 // Track the beginning of drag events
347 mousedown
: function(event
, g
, context
) {
348 context
.initializeMouseDown(event
, g
, context
);
350 if (event
.altKey
|| event
.shiftKey
) {
351 Dygraph
.startPan(event
, g
, context
);
353 Dygraph
.startZoom(event
, g
, context
);
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
);
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
);
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;
382 // Disable zooming out if panning.
383 dblclick
: function(event
, g
, context
) {
384 if (event
.altKey
|| event
.shiftKey
) {
387 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
388 // friendlier to public use.
393 Dygraph
.DEFAULT_ATTRS
.interactionModel
= Dygraph
.Interaction
.defaultModel
;
395 // old ways of accessing these methods/properties
396 Dygraph
.defaultInteractionModel
= Dygraph
.Interaction
.defaultModel
;
397 Dygraph
.endZoom
= Dygraph
.Interaction
.endZoom
;
398 Dygraph
.moveZoom
= Dygraph
.Interaction
.moveZoom
;
399 Dygraph
.startZoom
= Dygraph
.Interaction
.startZoom
;
400 Dygraph
.endPan
= Dygraph
.Interaction
.endPan
;
401 Dygraph
.movePan
= Dygraph
.Interaction
.movePan
;
402 Dygraph
.startPan
= Dygraph
.Interaction
.startPan
;
404 Dygraph
.Interaction
.nonInteractiveModel_
= {
405 mousedown
: function(event
, g
, context
) {
406 context
.initializeMouseDown(event
, g
, context
);
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
);
415 if (regionWidth
< 2 && regionHeight
< 2 &&
416 g
.lastx_
!== undefined
&& g
.lastx_
!= -1) {
417 Dygraph
.Interaction
.treatMouseOpAsClick(g
, event
, context
);