fix disappearing annotations bug
[dygraphs.git] / dygraph-interaction-model.js
CommitLineData
846f3d2d 1// Copyright 2011 Robert Konigsberg (konigsberg@google.com)
5108eb20 2// MIT-licensed (http://opensource.org/licenses/MIT)
846f3d2d
DV
3
4/**
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)
8 */
9
10
11/**
12 * A collection of functions to facilitate build custom interaction models.
13 * @class
14 */
15Dygraph.Interaction = {};
16
17/**
18 * Called in response to an interaction model operation that
19 * should start the default panning behavior.
20 *
21 * It's used in the default callback for "mousedown" operations.
22 * Custom interaction model builders can use it to provide the default
23 * panning behavior.
24 *
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.
29 */
30Dygraph.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);
36
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!
40
41 var boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw;
42 var boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw;
43
44 var boundedLeftDate = g.toDataXCoord(boundedLeftX);
45 var boundedRightDate = g.toDataXCoord(boundedRightX);
46 context.boundedDates = [boundedLeftDate, boundedRightDate];
47
48 var boundedValues = [];
49 var maxYPixelsToDraw = g.height_ * g.attr_("panEdgeFraction");
50
51 for (var i = 0; i < g.axes_.length; i++) {
52 var axis = g.axes_[i];
53 var yExtremes = axis.extremeRange;
54
55 var boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw;
56 var boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw;
57
58 var boundedTopValue = g.toDataYCoord(boundedTopY);
59 var boundedBottomValue = g.toDataYCoord(boundedBottomY);
60
61 boundedValues[i] = [boundedTopValue, boundedBottomValue];
62 }
63 context.boundedValues = boundedValues;
64 }
65
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.
74 if (axis.logscale) {
75 axis.initialTopValue = Dygraph.log10(yRange[1]);
76 axis.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]);
77 } else {
78 axis.initialTopValue = yRange[1];
79 axis.dragValueRange = yRange[1] - yRange[0];
80 }
81 axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1);
82
83 // While calculating axes, set 2dpan.
84 if (axis.valueWindow || axis.valueRange) context.is2DPan = true;
85 }
86};
87
88/**
89 * Called in response to an interaction model operation that
90 * responds to an event that pans the view.
91 *
92 * It's used in the default callback for "mousemove" operations.
93 * Custom interaction model builders can use it to provide the default
94 * panning behavior.
95 *
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.
100 */
101Dygraph.Interaction.movePan = function(event, g, context) {
102 context.dragEndX = g.dragGetX_(event, context);
103 context.dragEndY = g.dragGetY_(event, context);
104
105 var minDate = context.initialLeftmostDate -
106 (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
107 if (context.boundedDates) {
108 minDate = Math.max(minDate, context.boundedDates[0]);
109 }
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;
116 }
117 }
118
119 g.dateWindow_ = [minDate, maxDate];
120
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];
126
127 var pixelsDragged = context.dragEndY - context.dragStartY;
128 var unitsDragged = pixelsDragged * axis.unitsPerPixel;
129
130 var boundedValue = context.boundedValues ? context.boundedValues[i] : null;
131
132 // In log scale, maxValue and minValue are the logs of those values.
133 var maxValue = axis.initialTopValue + unitsDragged;
134 if (boundedValue) {
135 maxValue = Math.min(maxValue, boundedValue[1]);
136 }
137 var minValue = maxValue - axis.dragValueRange;
138 if (boundedValue) {
139 if (minValue < boundedValue[0]) {
140 // Adjust maxValue, and recompute minValue.
141 maxValue = maxValue - (minValue - boundedValue[0]);
142 minValue = maxValue - axis.dragValueRange;
143 }
144 }
145 if (axis.logscale) {
146 axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue),
147 Math.pow(Dygraph.LOG_SCALE, maxValue) ];
148 } else {
149 axis.valueWindow = [ minValue, maxValue ];
150 }
151 }
152 }
153
154 g.drawGraph_(false);
155};
156
157/**
158 * Called in response to an interaction model operation that
159 * responds to an event that ends panning.
160 *
161 * It's used in the default callback for "mouseup" operations.
162 * Custom interaction model builders can use it to provide the default
163 * panning behavior.
164 *
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.
169 */
170Dygraph.Interaction.endPan = function(event, g, context) {
171 context.dragEndX = g.dragGetX_(event, context);
172 context.dragEndY = g.dragGetY_(event, context);
173
174 var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
175 var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
176
177 if (regionWidth < 2 && regionHeight < 2 &&
178 g.lastx_ != undefined && g.lastx_ != -1) {
179 Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
180 }
181
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;
193};
194
195/**
196 * Called in response to an interaction model operation that
197 * responds to an event that starts zooming.
198 *
199 * It's used in the default callback for "mousedown" operations.
200 * Custom interaction model builders can use it to provide the default
201 * zooming behavior.
202 *
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.
207 */
208Dygraph.Interaction.startZoom = function(event, g, context) {
209 context.isZooming = true;
210};
211
212/**
213 * Called in response to an interaction model operation that
214 * responds to an event that defines zoom boundaries.
215 *
216 * It's used in the default callback for "mousemove" operations.
217 * Custom interaction model builders can use it to provide the default
218 * zooming behavior.
219 *
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.
224 */
225Dygraph.Interaction.moveZoom = function(event, g, context) {
226 context.dragEndX = g.dragGetX_(event, context);
227 context.dragEndY = g.dragGetY_(event, context);
228
229 var xDelta = Math.abs(context.dragStartX - context.dragEndX);
230 var yDelta = Math.abs(context.dragStartY - context.dragEndY);
231
232 // drag direction threshold for y axis is twice as large as x axis
233 context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
234
235 g.drawZoomRect_(
236 context.dragDirection,
237 context.dragStartX,
238 context.dragEndX,
239 context.dragStartY,
240 context.dragEndY,
241 context.prevDragDirection,
242 context.prevEndX,
243 context.prevEndY);
244
245 context.prevEndX = context.dragEndX;
246 context.prevEndY = context.dragEndY;
247 context.prevDragDirection = context.dragDirection;
248};
249
250Dygraph.Interaction.treatMouseOpAsClick = function(g, event, context) {
251 var clickCallback = g.attr_('clickCallback');
252 var pointClickCallback = g.attr_('pointClickCallback');
253
254 var selectedPoint = null;
255
256 // Find out if the click occurs on a point. This only matters if there's a pointClickCallback.
257 if (pointClickCallback) {
258 var closestIdx = -1;
259 var closestDistance = Number.MAX_VALUE;
260
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;
268 closestIdx = i;
269 }
270 }
271
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];
276 }
277 }
278
279 if (selectedPoint) {
280 pointClickCallback(event, selectedPoint);
281 }
282
283 // TODO(danvk): pass along more info about the points, e.g. 'x'
284 if (clickCallback) {
285 clickCallback(event, g.lastx_, g.selPoints_);
286 }
287};
288
289/**
290 * Called in response to an interaction model operation that
291 * responds to an event that performs a zoom based on previously defined
292 * bounds..
293 *
294 * It's used in the default callback for "mouseup" operations.
295 * Custom interaction model builders can use it to provide the default
296 * zooming behavior.
297 *
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.
302 */
303Dygraph.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);
309
310 if (regionWidth < 2 && regionHeight < 2 &&
311 g.lastx_ != undefined && g.lastx_ != -1) {
312 Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
313 }
314
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));
321 } else {
322 g.canvas_ctx_.clearRect(0, 0, g.canvas_.width, g.canvas_.height);
323 }
324 context.dragStartX = null;
325 context.dragStartY = null;
326};
327
328/**
329 * Default interation model for dygraphs. You can refer to specific elements of
330 * this when constructing your own interaction model, e.g.:
331 * g.updateOptions( {
332 * interactionModel: {
333 * mousedown: Dygraph.defaultInteractionModel.mousedown
334 * }
335 * } );
336 */
337Dygraph.Interaction.defaultModel = {
338 // Track the beginning of drag events
339 mousedown: function(event, g, context) {
340 context.initializeMouseDown(event, g, context);
341
342 if (event.altKey || event.shiftKey) {
343 Dygraph.startPan(event, g, context);
344 } else {
345 Dygraph.startZoom(event, g, context);
346 }
347 },
348
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);
355 }
356 },
357
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);
363 }
364 },
365
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;
371 }
372 },
373
374 // Disable zooming out if panning.
375 dblclick: function(event, g, context) {
376 if (event.altKey || event.shiftKey) {
377 return;
378 }
379 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
380 // friendlier to public use.
381 g.doUnzoom_();
382 }
383};
384
385Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.Interaction.defaultModel;
386
387// old ways of accessing these methods/properties
388Dygraph.defaultInteractionModel = Dygraph.Interaction.defaultModel;
389Dygraph.endZoom = Dygraph.Interaction.endZoom;
390Dygraph.moveZoom = Dygraph.Interaction.moveZoom;
391Dygraph.startZoom = Dygraph.Interaction.startZoom;
392Dygraph.endPan = Dygraph.Interaction.endPan;
393Dygraph.movePan = Dygraph.Interaction.movePan;
394Dygraph.startPan = Dygraph.Interaction.startPan;
395
0290d079 396Dygraph.Interaction.nonInteractiveModel_ = {
027e9e9b
DV
397 mousedown: function(event, g, context) {
398 context.initializeMouseDown(event, g, context);
399 },
400 mouseup: function(event, g, context) {
401 // TODO(danvk): this logic is repeated in Dygraph.Interaction.endZoom
402 context.dragEndX = g.dragGetX_(event, context);
403 context.dragEndY = g.dragGetY_(event, context);
404 var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
405 var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
406
407 if (regionWidth < 2 && regionHeight < 2 &&
408 g.lastx_ != undefined && g.lastx_ != -1) {
409 Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
410 }
411 }
412};