Add a simple Usage Gallery to the dygraphs home page.
[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
c0f54d4f
DV
13"use strict";
14
846f3d2d
DV
15
16/**
17 * A collection of functions to facilitate build custom interaction models.
18 * @class
19 */
20Dygraph.Interaction = {};
21
22/**
23 * Called in response to an interaction model operation that
24 * should start the default panning behavior.
25 *
26 * It's used in the default callback for "mousedown" operations.
27 * Custom interaction model builders can use it to provide the default
28 * panning behavior.
29 *
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.
34 */
35Dygraph.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);
41
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!
45
46 var boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw;
47 var boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw;
48
49 var boundedLeftDate = g.toDataXCoord(boundedLeftX);
50 var boundedRightDate = g.toDataXCoord(boundedRightX);
51 context.boundedDates = [boundedLeftDate, boundedRightDate];
52
53 var boundedValues = [];
54 var maxYPixelsToDraw = g.height_ * g.attr_("panEdgeFraction");
55
56 for (var i = 0; i < g.axes_.length; i++) {
57 var axis = g.axes_[i];
58 var yExtremes = axis.extremeRange;
59
60 var boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw;
61 var boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw;
62
63 var boundedTopValue = g.toDataYCoord(boundedTopY);
64 var boundedBottomValue = g.toDataYCoord(boundedBottomY);
65
66 boundedValues[i] = [boundedTopValue, boundedBottomValue];
67 }
68 context.boundedValues = boundedValues;
69 }
70
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.
79 if (axis.logscale) {
80 axis.initialTopValue = Dygraph.log10(yRange[1]);
81 axis.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]);
82 } else {
83 axis.initialTopValue = yRange[1];
84 axis.dragValueRange = yRange[1] - yRange[0];
85 }
86 axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1);
87
88 // While calculating axes, set 2dpan.
89 if (axis.valueWindow || axis.valueRange) context.is2DPan = true;
90 }
91};
92
93/**
94 * Called in response to an interaction model operation that
95 * responds to an event that pans the view.
96 *
97 * It's used in the default callback for "mousemove" operations.
98 * Custom interaction model builders can use it to provide the default
99 * panning behavior.
100 *
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.
105 */
106Dygraph.Interaction.movePan = function(event, g, context) {
107 context.dragEndX = g.dragGetX_(event, context);
108 context.dragEndY = g.dragGetY_(event, context);
109
110 var minDate = context.initialLeftmostDate -
111 (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
112 if (context.boundedDates) {
113 minDate = Math.max(minDate, context.boundedDates[0]);
114 }
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;
121 }
122 }
123
124 g.dateWindow_ = [minDate, maxDate];
125
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];
131
132 var pixelsDragged = context.dragEndY - context.dragStartY;
133 var unitsDragged = pixelsDragged * axis.unitsPerPixel;
920208fb 134
846f3d2d
DV
135 var boundedValue = context.boundedValues ? context.boundedValues[i] : null;
136
137 // In log scale, maxValue and minValue are the logs of those values.
138 var maxValue = axis.initialTopValue + unitsDragged;
139 if (boundedValue) {
140 maxValue = Math.min(maxValue, boundedValue[1]);
141 }
142 var minValue = maxValue - axis.dragValueRange;
143 if (boundedValue) {
144 if (minValue < boundedValue[0]) {
145 // Adjust maxValue, and recompute minValue.
146 maxValue = maxValue - (minValue - boundedValue[0]);
147 minValue = maxValue - axis.dragValueRange;
148 }
149 }
150 if (axis.logscale) {
151 axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue),
152 Math.pow(Dygraph.LOG_SCALE, maxValue) ];
153 } else {
154 axis.valueWindow = [ minValue, maxValue ];
155 }
156 }
157 }
158
159 g.drawGraph_(false);
160};
161
162/**
163 * Called in response to an interaction model operation that
164 * responds to an event that ends panning.
165 *
166 * It's used in the default callback for "mouseup" operations.
167 * Custom interaction model builders can use it to provide the default
168 * panning behavior.
169 *
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.
174 */
175Dygraph.Interaction.endPan = function(event, g, context) {
176 context.dragEndX = g.dragGetX_(event, context);
177 context.dragEndY = g.dragGetY_(event, context);
178
179 var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
180 var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
181
182 if (regionWidth < 2 && regionHeight < 2 &&
183 g.lastx_ != undefined && g.lastx_ != -1) {
184 Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
185 }
186
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;
198};
199
200/**
201 * Called in response to an interaction model operation that
202 * responds to an event that starts zooming.
203 *
204 * It's used in the default callback for "mousedown" operations.
205 * Custom interaction model builders can use it to provide the default
206 * zooming behavior.
207 *
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.
212 */
213Dygraph.Interaction.startZoom = function(event, g, context) {
214 context.isZooming = true;
215};
216
217/**
218 * Called in response to an interaction model operation that
219 * responds to an event that defines zoom boundaries.
220 *
221 * It's used in the default callback for "mousemove" operations.
222 * Custom interaction model builders can use it to provide the default
223 * zooming behavior.
224 *
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.
229 */
230Dygraph.Interaction.moveZoom = function(event, g, context) {
231 context.dragEndX = g.dragGetX_(event, context);
232 context.dragEndY = g.dragGetY_(event, context);
233
234 var xDelta = Math.abs(context.dragStartX - context.dragEndX);
235 var yDelta = Math.abs(context.dragStartY - context.dragEndY);
236
237 // drag direction threshold for y axis is twice as large as x axis
238 context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
239
240 g.drawZoomRect_(
241 context.dragDirection,
242 context.dragStartX,
243 context.dragEndX,
244 context.dragStartY,
245 context.dragEndY,
246 context.prevDragDirection,
247 context.prevEndX,
248 context.prevEndY);
249
250 context.prevEndX = context.dragEndX;
251 context.prevEndY = context.dragEndY;
252 context.prevDragDirection = context.dragDirection;
253};
254
255Dygraph.Interaction.treatMouseOpAsClick = function(g, event, context) {
256 var clickCallback = g.attr_('clickCallback');
257 var pointClickCallback = g.attr_('pointClickCallback');
258
259 var selectedPoint = null;
260
261 // Find out if the click occurs on a point. This only matters if there's a pointClickCallback.
262 if (pointClickCallback) {
263 var closestIdx = -1;
264 var closestDistance = Number.MAX_VALUE;
265
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);
fbff6d71
DV
271 if (!isNaN(distance) &&
272 (closestIdx == -1 || distance < closestDistance)) {
846f3d2d
DV
273 closestDistance = distance;
274 closestIdx = i;
275 }
276 }
277
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];
282 }
283 }
284
285 if (selectedPoint) {
286 pointClickCallback(event, selectedPoint);
287 }
288
289 // TODO(danvk): pass along more info about the points, e.g. 'x'
290 if (clickCallback) {
291 clickCallback(event, g.lastx_, g.selPoints_);
292 }
293};
294
295/**
296 * Called in response to an interaction model operation that
297 * responds to an event that performs a zoom based on previously defined
298 * bounds..
299 *
300 * It's used in the default callback for "mouseup" operations.
301 * Custom interaction model builders can use it to provide the default
302 * zooming behavior.
303 *
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.
308 */
309Dygraph.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);
315
316 if (regionWidth < 2 && regionHeight < 2 &&
317 g.lastx_ != undefined && g.lastx_ != -1) {
318 Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
319 }
320
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));
327 } else {
920208fb 328 g.clearZoomRect_();
846f3d2d
DV
329 }
330 context.dragStartX = null;
331 context.dragStartY = null;
332};
333
334/**
335 * Default interation model for dygraphs. You can refer to specific elements of
336 * this when constructing your own interaction model, e.g.:
337 * g.updateOptions( {
338 * interactionModel: {
339 * mousedown: Dygraph.defaultInteractionModel.mousedown
340 * }
341 * } );
342 */
343Dygraph.Interaction.defaultModel = {
344 // Track the beginning of drag events
345 mousedown: function(event, g, context) {
346 context.initializeMouseDown(event, g, context);
347
348 if (event.altKey || event.shiftKey) {
349 Dygraph.startPan(event, g, context);
350 } else {
351 Dygraph.startZoom(event, g, context);
352 }
353 },
354
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);
361 }
362 },
363
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);
369 }
370 },
371
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;
377 }
378 },
379
380 // Disable zooming out if panning.
381 dblclick: function(event, g, context) {
382 if (event.altKey || event.shiftKey) {
383 return;
384 }
385 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
386 // friendlier to public use.
387 g.doUnzoom_();
388 }
389};
390
391Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.Interaction.defaultModel;
392
393// old ways of accessing these methods/properties
394Dygraph.defaultInteractionModel = Dygraph.Interaction.defaultModel;
395Dygraph.endZoom = Dygraph.Interaction.endZoom;
396Dygraph.moveZoom = Dygraph.Interaction.moveZoom;
397Dygraph.startZoom = Dygraph.Interaction.startZoom;
398Dygraph.endPan = Dygraph.Interaction.endPan;
399Dygraph.movePan = Dygraph.Interaction.movePan;
400Dygraph.startPan = Dygraph.Interaction.startPan;
401
0290d079 402Dygraph.Interaction.nonInteractiveModel_ = {
027e9e9b
DV
403 mousedown: function(event, g, context) {
404 context.initializeMouseDown(event, g, context);
405 },
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);
412
413 if (regionWidth < 2 && regionHeight < 2 &&
414 g.lastx_ != undefined && g.lastx_ != -1) {
415 Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
416 }
417 }
418};