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