1 // Copyright 2011 Robert Konigsberg (konigsberg@google.com)
2 // All Rights Reserved.
5 * @fileoverview The default interaction model for Dygraphs. This is kept out
6 * of dygraph.js for better navigability.
7 * @author Robert Konigsberg (konigsberg@google.com)
12 * A collection of functions to facilitate build custom interaction models.
15 Dygraph
.Interaction
= {};
18 * Called in response to an interaction model operation that
19 * should start the default panning behavior.
21 * It's used in the default callback for "mousedown" operations.
22 * Custom interaction model builders can use it to provide the default
25 * @param { Event } event the event object which led to the startPan call.
26 * @param { Dygraph} g The dygraph on which to act.
27 * @param { Object} context The dragging context object (with
28 * dragStartX/dragStartY/etc. properties). This function modifies the context.
30 Dygraph
.Interaction
.startPan
= function(event
, g
, context
) {
31 context
.isPanning
= true;
32 var xRange
= g
.xAxisRange();
33 context
.dateRange
= xRange
[1] - xRange
[0];
34 context
.initialLeftmostDate
= xRange
[0];
35 context
.xUnitsPerPixel
= context
.dateRange
/ (g
.plotter_
.area
.w
- 1);
37 if (g
.attr_("panEdgeFraction")) {
38 var maxXPixelsToDraw
= g
.width_
* g
.attr_("panEdgeFraction");
39 var xExtremes
= g
.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes!
41 var boundedLeftX
= g
.toDomXCoord(xExtremes
[0]) - maxXPixelsToDraw
;
42 var boundedRightX
= g
.toDomXCoord(xExtremes
[1]) + maxXPixelsToDraw
;
44 var boundedLeftDate
= g
.toDataXCoord(boundedLeftX
);
45 var boundedRightDate
= g
.toDataXCoord(boundedRightX
);
46 context
.boundedDates
= [boundedLeftDate
, boundedRightDate
];
48 var boundedValues
= [];
49 var maxYPixelsToDraw
= g
.height_
* g
.attr_("panEdgeFraction");
51 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
52 var axis
= g
.axes_
[i
];
53 var yExtremes
= axis
.extremeRange
;
55 var boundedTopY
= g
.toDomYCoord(yExtremes
[0], i
) + maxYPixelsToDraw
;
56 var boundedBottomY
= g
.toDomYCoord(yExtremes
[1], i
) - maxYPixelsToDraw
;
58 var boundedTopValue
= g
.toDataYCoord(boundedTopY
);
59 var boundedBottomValue
= g
.toDataYCoord(boundedBottomY
);
61 boundedValues
[i
] = [boundedTopValue
, boundedBottomValue
];
63 context
.boundedValues
= boundedValues
;
66 // Record the range of each y-axis at the start of the drag.
67 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
68 context
.is2DPan
= false;
69 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
70 var axis
= g
.axes_
[i
];
71 var yRange
= g
.yAxisRange(i
);
72 // TODO(konigsberg): These values should be in |context|.
73 // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
75 axis
.initialTopValue
= Dygraph
.log10(yRange
[1]);
76 axis
.dragValueRange
= Dygraph
.log10(yRange
[1]) - Dygraph
.log10(yRange
[0]);
78 axis
.initialTopValue
= yRange
[1];
79 axis
.dragValueRange
= yRange
[1] - yRange
[0];
81 axis
.unitsPerPixel
= axis
.dragValueRange
/ (g
.plotter_
.area
.h
- 1);
83 // While calculating axes, set 2dpan.
84 if (axis
.valueWindow
|| axis
.valueRange
) context
.is2DPan
= true;
89 * Called in response to an interaction model operation that
90 * responds to an event that pans the view.
92 * It's used in the default callback for "mousemove" operations.
93 * Custom interaction model builders can use it to provide the default
96 * @param { Event } event the event object which led to the movePan call.
97 * @param { Dygraph} g The dygraph on which to act.
98 * @param { Object} context The dragging context object (with
99 * dragStartX/dragStartY/etc. properties). This function modifies the context.
101 Dygraph
.Interaction
.movePan
= function(event
, g
, context
) {
102 context
.dragEndX
= g
.dragGetX_(event
, context
);
103 context
.dragEndY
= g
.dragGetY_(event
, context
);
105 var minDate
= context
.initialLeftmostDate
-
106 (context
.dragEndX
- context
.dragStartX
) * context
.xUnitsPerPixel
;
107 if (context
.boundedDates
) {
108 minDate
= Math
.max(minDate
, context
.boundedDates
[0]);
110 var maxDate
= minDate
+ context
.dateRange
;
111 if (context
.boundedDates
) {
112 if (maxDate
> context
.boundedDates
[1]) {
113 // Adjust minDate, and recompute maxDate.
114 minDate
= minDate
- (maxDate
- context
.boundedDates
[1]);
115 maxDate
= minDate
+ context
.dateRange
;
119 g
.dateWindow_
= [minDate
, maxDate
];
121 // y-axis scaling is automatic unless this is a full 2D pan.
122 if (context
.is2DPan
) {
123 // Adjust each axis appropriately.
124 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
125 var axis
= g
.axes_
[i
];
127 var pixelsDragged
= context
.dragEndY
- context
.dragStartY
;
128 var unitsDragged
= pixelsDragged
* axis
.unitsPerPixel
;
130 var boundedValue
= context
.boundedValues
? context
.boundedValues
[i
] : null;
132 // In log scale, maxValue and minValue are the logs of those values.
133 var maxValue
= axis
.initialTopValue
+ unitsDragged
;
135 maxValue
= Math
.min(maxValue
, boundedValue
[1]);
137 var minValue
= maxValue
- axis
.dragValueRange
;
139 if (minValue
< boundedValue
[0]) {
140 // Adjust maxValue, and recompute minValue.
141 maxValue
= maxValue
- (minValue
- boundedValue
[0]);
142 minValue
= maxValue
- axis
.dragValueRange
;
146 axis
.valueWindow
= [ Math
.pow(Dygraph
.LOG_SCALE
, minValue
),
147 Math
.pow(Dygraph
.LOG_SCALE
, maxValue
) ];
149 axis
.valueWindow
= [ minValue
, maxValue
];
158 * Called in response to an interaction model operation that
159 * responds to an event that ends panning.
161 * It's used in the default callback for "mouseup" operations.
162 * Custom interaction model builders can use it to provide the default
165 * @param { Event } event the event object which led to the startZoom call.
166 * @param { Dygraph} g The dygraph on which to act.
167 * @param { Object} context The dragging context object (with
168 * dragStartX/dragStartY/etc. properties). This function modifies the context.
170 Dygraph
.Interaction
.endPan
= function(event
, g
, context
) {
171 context
.dragEndX
= g
.dragGetX_(event
, context
);
172 context
.dragEndY
= g
.dragGetY_(event
, context
);
174 var regionWidth
= Math
.abs(context
.dragEndX
- context
.dragStartX
);
175 var regionHeight
= Math
.abs(context
.dragEndY
- context
.dragStartY
);
177 if (regionWidth
< 2 && regionHeight
< 2 &&
178 g
.lastx_
!= undefined
&& g
.lastx_
!= -1) {
179 Dygraph
.Interaction
.treatMouseOpAsClick(g
, event
, context
);
182 // TODO(konigsberg): Clear the context data from the axis.
183 // (replace with "context = {}" ?)
184 // TODO(konigsberg): mouseup should just delete the
185 // context object, and mousedown should create a new one.
186 context
.isPanning
= false;
187 context
.is2DPan
= false;
188 context
.initialLeftmostDate
= null;
189 context
.dateRange
= null;
190 context
.valueRange
= null;
191 context
.boundedDates
= null;
192 context
.boundedValues
= null;
196 * Called in response to an interaction model operation that
197 * responds to an event that starts zooming.
199 * It's used in the default callback for "mousedown" operations.
200 * Custom interaction model builders can use it to provide the default
203 * @param { Event } event the event object which led to the startZoom call.
204 * @param { Dygraph} g The dygraph on which to act.
205 * @param { Object} context The dragging context object (with
206 * dragStartX/dragStartY/etc. properties). This function modifies the context.
208 Dygraph
.Interaction
.startZoom
= function(event
, g
, context
) {
209 context
.isZooming
= true;
213 * Called in response to an interaction model operation that
214 * responds to an event that defines zoom boundaries.
216 * It's used in the default callback for "mousemove" operations.
217 * Custom interaction model builders can use it to provide the default
220 * @param { Event } event the event object which led to the moveZoom call.
221 * @param { Dygraph} g The dygraph on which to act.
222 * @param { Object} context The dragging context object (with
223 * dragStartX/dragStartY/etc. properties). This function modifies the context.
225 Dygraph
.Interaction
.moveZoom
= function(event
, g
, context
) {
226 context
.dragEndX
= g
.dragGetX_(event
, context
);
227 context
.dragEndY
= g
.dragGetY_(event
, context
);
229 var xDelta
= Math
.abs(context
.dragStartX
- context
.dragEndX
);
230 var yDelta
= Math
.abs(context
.dragStartY
- context
.dragEndY
);
232 // drag direction threshold for y axis is twice as large as x axis
233 context
.dragDirection
= (xDelta
< yDelta
/ 2) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
236 context
.dragDirection
,
241 context
.prevDragDirection
,
245 context
.prevEndX
= context
.dragEndX
;
246 context
.prevEndY
= context
.dragEndY
;
247 context
.prevDragDirection
= context
.dragDirection
;
250 Dygraph
.Interaction
.treatMouseOpAsClick
= function(g
, event
, context
) {
251 var clickCallback
= g
.attr_('clickCallback');
252 var pointClickCallback
= g
.attr_('pointClickCallback');
254 var selectedPoint
= null;
256 // Find out if the click occurs on a point. This only matters if there's a pointClickCallback.
257 if (pointClickCallback
) {
259 var closestDistance
= Number
.MAX_VALUE
;
261 // check if the click was on a particular point.
262 for (var i
= 0; i
< g
.selPoints_
.length
; i
++) {
263 var p
= g
.selPoints_
[i
];
264 var distance
= Math
.pow(p
.canvasx
- context
.dragEndX
, 2) +
265 Math
.pow(p
.canvasy
- context
.dragEndY
, 2);
266 if (closestIdx
== -1 || distance
< closestDistance
) {
267 closestDistance
= distance
;
272 // Allow any click within two pixels of the dot.
273 var radius
= g
.attr_('highlightCircleSize') + 2;
274 if (closestDistance
<= radius
* radius
) {
275 selectedPoint
= g
.selPoints_
[closestIdx
];
280 pointClickCallback(event
, selectedPoint
);
283 // TODO(danvk): pass along more info about the points, e.g. 'x'
285 clickCallback(event
, g
.lastx_
, g
.selPoints_
);
290 * Called in response to an interaction model operation that
291 * responds to an event that performs a zoom based on previously defined
294 * It's used in the default callback for "mouseup" operations.
295 * Custom interaction model builders can use it to provide the default
298 * @param { Event } event the event object which led to the endZoom call.
299 * @param { Dygraph} g The dygraph on which to end the zoom.
300 * @param { Object} context The dragging context object (with
301 * dragStartX/dragStartY/etc. properties). This function modifies the context.
303 Dygraph
.Interaction
.endZoom
= function(event
, g
, context
) {
304 context
.isZooming
= false;
305 context
.dragEndX
= g
.dragGetX_(event
, context
);
306 context
.dragEndY
= g
.dragGetY_(event
, context
);
307 var regionWidth
= Math
.abs(context
.dragEndX
- context
.dragStartX
);
308 var regionHeight
= Math
.abs(context
.dragEndY
- context
.dragStartY
);
310 if (regionWidth
< 2 && regionHeight
< 2 &&
311 g
.lastx_
!= undefined
&& g
.lastx_
!= -1) {
312 Dygraph
.Interaction
.treatMouseOpAsClick(g
, event
, context
);
315 if (regionWidth
>= 10 && context
.dragDirection
== Dygraph
.HORIZONTAL
) {
316 g
.doZoomX_(Math
.min(context
.dragStartX
, context
.dragEndX
),
317 Math
.max(context
.dragStartX
, context
.dragEndX
));
318 } else if (regionHeight
>= 10 && context
.dragDirection
== Dygraph
.VERTICAL
) {
319 g
.doZoomY_(Math
.min(context
.dragStartY
, context
.dragEndY
),
320 Math
.max(context
.dragStartY
, context
.dragEndY
));
322 g
.canvas_ctx_
.clearRect(0, 0, g
.canvas_
.width
, g
.canvas_
.height
);
324 context
.dragStartX
= null;
325 context
.dragStartY
= null;
329 * Default interation model for dygraphs. You can refer to specific elements of
330 * this when constructing your own interaction model, e.g.:
332 * interactionModel: {
333 * mousedown: Dygraph.defaultInteractionModel.mousedown
337 Dygraph
.Interaction
.defaultModel
= {
338 // Track the beginning of drag events
339 mousedown
: function(event
, g
, context
) {
340 context
.initializeMouseDown(event
, g
, context
);
342 if (event
.altKey
|| event
.shiftKey
) {
343 Dygraph
.startPan(event
, g
, context
);
345 Dygraph
.startZoom(event
, g
, context
);
349 // Draw zoom rectangles when the mouse is down and the user moves around
350 mousemove
: function(event
, g
, context
) {
351 if (context
.isZooming
) {
352 Dygraph
.moveZoom(event
, g
, context
);
353 } else if (context
.isPanning
) {
354 Dygraph
.movePan(event
, g
, context
);
358 mouseup
: function(event
, g
, context
) {
359 if (context
.isZooming
) {
360 Dygraph
.endZoom(event
, g
, context
);
361 } else if (context
.isPanning
) {
362 Dygraph
.endPan(event
, g
, context
);
366 // Temporarily cancel the dragging event when the mouse leaves the graph
367 mouseout
: function(event
, g
, context
) {
368 if (context
.isZooming
) {
369 context
.dragEndX
= null;
370 context
.dragEndY
= null;
374 // Disable zooming out if panning.
375 dblclick
: function(event
, g
, context
) {
376 if (event
.altKey
|| event
.shiftKey
) {
379 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
380 // friendlier to public use.
385 Dygraph
.DEFAULT_ATTRS
.interactionModel
= Dygraph
.Interaction
.defaultModel
;
387 // old ways of accessing these methods/properties
388 Dygraph
.defaultInteractionModel
= Dygraph
.Interaction
.defaultModel
;
389 Dygraph
.endZoom
= Dygraph
.Interaction
.endZoom
;
390 Dygraph
.moveZoom
= Dygraph
.Interaction
.moveZoom
;
391 Dygraph
.startZoom
= Dygraph
.Interaction
.startZoom
;
392 Dygraph
.endPan
= Dygraph
.Interaction
.endPan
;
393 Dygraph
.movePan
= Dygraph
.Interaction
.movePan
;
394 Dygraph
.startPan
= Dygraph
.Interaction
.startPan
;