Dygraph.DEFAULT_HEIGHT = 320;
Dygraph.AXIS_LINE_WIDTH = 0.3;
+
// Default attribute values.
Dygraph.DEFAULT_ATTRS = {
highlightCircleSize: 3,
hideOverlayOnMouseOut: true,
stepPlot: false,
- avoidMinZero: false
+ avoidMinZero: false,
+
+ interactionModel: null // will be set to Dygraph.defaultInteractionModel.
};
// Various logging levels.
/**
* Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
- * and interaction <canvas> inside of it. See the constructor for details
+ * and context <canvas> inside of it. See the constructor for details.
* on the parameters.
* @param {Element} div the Element to render the graph into.
* @param {String | Function} file Source data
}
};
+
+// Based on the article at
+// http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel
+Dygraph.cancelEvent = function(e) {
+ e = e ? e : window.event;
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ }
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+ e.cancelBubble = true;
+ e.cancel = true;
+ e.returnValue = false;
+ return false;
+}
+
/**
* Generates interface elements for the Dygraph: a containing div, a div to
* display the current point, and a textbox to adjust the rolling average
}
};
-/**
- * Set up all the mouse handlers needed to capture dragging behavior for zoom
- * events.
- * @private
- */
-Dygraph.prototype.createDragInterface_ = function() {
- var self = this;
+Dygraph.prototype.dragGetX_ = function(e, context) {
+ return Dygraph.pageX(e) - context.px
+};
- // Tracks whether the mouse is down right now
- var isZooming = false;
- var isPanning = false; // is this drag part of a pan?
- var is2DPan = false; // if so, is that pan 1- or 2-dimensional?
- var dragStartX = null;
- var dragStartY = null;
- var dragEndX = null;
- var dragEndY = null;
- var dragDirection = null;
- var prevEndX = null;
- var prevEndY = null;
- var prevDragDirection = null;
+Dygraph.prototype.dragGetY_ = function(e, context) {
+ return Dygraph.pageY(e) - context.py
+};
- // TODO(danvk): update this comment
- // draggingDate and draggingValue represent the [date,value] point on the
- // graph at which the mouse was pressed. As the mouse moves while panning,
- // the viewport must pan so that the mouse position points to
- // [draggingDate, draggingValue]
- var draggingDate = null;
+// Called in response to an interaction model operation that
+// should start the default panning behavior.
+//
+// It's used in the default callback for "mousedown" operations.
+// Custom interaction model builders can use it to provide the default
+// panning behavior.
+//
+Dygraph.startPan = function(event, g, context) {
+ // have to be zoomed in to pan.
+ // TODO(konigsberg): Let's loosen this zoom-to-pan restriction, also
+ // perhaps create panning boundaries? A more flexible pan would make it,
+ // ahem, 'pan-useful'.
+ var zoomedY = false;
+ for (var i = 0; i < g.axes_.length; i++) {
+ if (g.axes_[i].valueWindow || g.axes_[i].valueRange) {
+ zoomedY = true;
+ break;
+ }
+ }
+ if (!g.dateWindow_ && !zoomedY) return;
- // TODO(danvk): update this comment
- // The range in second/value units that the viewport encompasses during a
- // panning operation.
- var dateRange = null;
+ context.isPanning = true;
+ var xRange = g.xAxisRange();
+ context.dateRange = xRange[1] - xRange[0];
- // Utility function to convert page-wide coordinates to canvas coords
- var px = 0;
- var py = 0;
- var getX = function(e) { return Dygraph.pageX(e) - px };
- var getY = function(e) { return Dygraph.pageY(e) - py };
+ // Record the range of each y-axis at the start of the drag.
+ // If any axis has a valueRange or valueWindow, then we want a 2D pan.
+ context.is2DPan = false;
+ for (var i = 0; i < g.axes_.length; i++) {
+ var axis = g.axes_[i];
+ var yRange = g.yAxisRange(i);
+ axis.dragValueRange = yRange[1] - yRange[0];
+ var r = g.toDataCoords(null, context.dragStartY, i);
+ axis.draggingValue = r[1];
+ if (axis.valueWindow || axis.valueRange) context.is2DPan = true;
+ }
- // Draw zoom rectangles when the mouse is down and the user moves around
- Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(event) {
- if (isZooming) {
- dragEndX = getX(event);
- dragEndY = getY(event);
-
- var xDelta = Math.abs(dragStartX - dragEndX);
- var yDelta = Math.abs(dragStartY - dragEndY);
-
- // drag direction threshold for y axis is twice as large as x axis
- dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
-
- self.drawZoomRect_(dragDirection, dragStartX, dragEndX, dragStartY, dragEndY,
- prevDragDirection, prevEndX, prevEndY);
-
- prevEndX = dragEndX;
- prevEndY = dragEndY;
- prevDragDirection = dragDirection;
- } else if (isPanning) {
- dragEndX = getX(event);
- dragEndY = getY(event);
-
- // TODO(danvk): update this comment
- // Want to have it so that:
- // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
- // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
- // 3. draggingValue appears at dragEndY.
- // 4. valueRange is unaltered.
-
- var minDate = draggingDate - (dragEndX / self.width_) * dateRange;
- var maxDate = minDate + dateRange;
- self.dateWindow_ = [minDate, maxDate];
-
-
- // y-axis scaling is automatic unless this is a full 2D pan.
- if (is2DPan) {
- // Adjust each axis appropriately.
- var y_frac = dragEndY / self.height_;
- for (var i = 0; i < self.axes_.length; i++) {
- var axis = self.axes_[i];
- var maxValue = axis.draggingValue + y_frac * axis.dragValueRange;
- var minValue = maxValue - axis.dragValueRange;
- axis.valueWindow = [ minValue, maxValue ];
- }
- }
+ // TODO(konigsberg): Switch from all this math to toDataCoords?
+ // Seems to work for the dragging value.
+ context.draggingDate = (context.dragStartX / g.width_) * context.dateRange + xRange[0];
+};
- self.drawGraph_();
- }
- });
+// Called in response to an interaction model operation that
+// responds to an event that pans the view.
+//
+// It's used in the default callback for "mousemove" operations.
+// Custom interaction model builders can use it to provide the default
+// panning behavior.
+//
+Dygraph.movePan = function(event, g, context) {
+ context.dragEndX = g.dragGetX_(event, context);
+ context.dragEndY = g.dragGetY_(event, context);
- // Track the beginning of drag events
- Dygraph.addEvent(this.mouseEventElement_, 'mousedown', function(event) {
- // prevents mouse drags from selecting page text.
- if (event.preventDefault) {
- event.preventDefault(); // Firefox, Chrome, etc.
- } else {
- event.returnValue = false; // IE
- event.cancelBubble = true;
+ // TODO(danvk): update this comment
+ // Want to have it so that:
+ // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
+ // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
+ // 3. draggingValue appears at dragEndY.
+ // 4. valueRange is unaltered.
+
+ var minDate = context.draggingDate - (context.dragEndX / g.width_) * context.dateRange;
+ var maxDate = minDate + context.dateRange;
+ g.dateWindow_ = [minDate, maxDate];
+
+ // y-axis scaling is automatic unless this is a full 2D pan.
+ if (context.is2DPan) {
+ // Adjust each axis appropriately.
+ var y_frac = context.dragEndY / g.height_;
+ for (var i = 0; i < g.axes_.length; i++) {
+ var axis = g.axes_[i];
+ var maxValue = axis.draggingValue + y_frac * axis.dragValueRange;
+ var minValue = maxValue - axis.dragValueRange;
+ axis.valueWindow = [ minValue, maxValue ];
}
+ }
- px = Dygraph.findPosX(self.canvas_);
- py = Dygraph.findPosY(self.canvas_);
- dragStartX = getX(event);
- dragStartY = getY(event);
+ g.drawGraph_();
+}
- if (event.altKey || event.shiftKey) {
- // have to be zoomed in to pan.
- var zoomedY = false;
- for (var i = 0; i < self.axes_.length; i++) {
- if (self.axes_[i].valueWindow || self.axes_[i].valueRange) {
- zoomedY = true;
- break;
+// Called in response to an interaction model operation that
+// responds to an event that ends panning.
+//
+// It's used in the default callback for "mouseup" operations.
+// Custom interaction model builders can use it to provide the default
+// panning behavior.
+//
+Dygraph.endPan = function(event, g, context) {
+ context.isPanning = false;
+ context.is2DPan = false;
+ context.draggingDate = null;
+ context.dateRange = null;
+ context.valueRange = null;
+}
+
+// Called in response to an interaction model operation that
+// responds to an event that starts zooming.
+//
+// It's used in the default callback for "mousedown" operations.
+// Custom interaction model builders can use it to provide the default
+// zooming behavior.
+//
+Dygraph.startZoom = function(event, g, context) {
+ context.isZooming = true;
+}
+
+// Called in response to an interaction model operation that
+// responds to an event that defines zoom boundaries.
+//
+// It's used in the default callback for "mousemove" operations.
+// Custom interaction model builders can use it to provide the default
+// zooming behavior.
+//
+Dygraph.moveZoom = function(event, g, context) {
+ context.dragEndX = g.dragGetX_(event, context);
+ context.dragEndY = g.dragGetY_(event, context);
+
+ var xDelta = Math.abs(context.dragStartX - context.dragEndX);
+ var yDelta = Math.abs(context.dragStartY - context.dragEndY);
+
+ // drag direction threshold for y axis is twice as large as x axis
+ context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
+
+ g.drawZoomRect_(
+ context.dragDirection,
+ context.dragStartX,
+ context.dragEndX,
+ context.dragStartY,
+ context.dragEndY,
+ context.prevDragDirection,
+ context.prevEndX,
+ context.prevEndY);
+
+ context.prevEndX = context.dragEndX;
+ context.prevEndY = context.dragEndY;
+ context.prevDragDirection = context.dragDirection;
+}
+
+// Called in response to an interaction model operation that
+// responds to an event that performs a zoom based on previously defined
+// bounds..
+//
+// It's used in the default callback for "mouseup" operations.
+// Custom interaction model builders can use it to provide the default
+// zooming behavior.
+//
+Dygraph.endZoom = function(event, g, context) {
+ context.isZooming = false;
+ context.dragEndX = g.dragGetX_(event, context);
+ context.dragEndY = g.dragGetY_(event, context);
+ var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
+ var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
+
+ if (regionWidth < 2 && regionHeight < 2 &&
+ g.lastx_ != undefined && g.lastx_ != -1) {
+ // TODO(danvk): pass along more info about the points, e.g. 'x'
+ if (g.attr_('clickCallback') != null) {
+ g.attr_('clickCallback')(event, g.lastx_, g.selPoints_);
+ }
+ if (g.attr_('pointClickCallback')) {
+ // check if the click was on a particular point.
+ var closestIdx = -1;
+ var closestDistance = 0;
+ for (var i = 0; i < g.selPoints_.length; i++) {
+ var p = g.selPoints_[i];
+ var distance = Math.pow(p.canvasx - context.dragEndX, 2) +
+ Math.pow(p.canvasy - context.dragEndY, 2);
+ if (closestIdx == -1 || distance < closestDistance) {
+ closestDistance = distance;
+ closestIdx = i;
}
}
- if (!self.dateWindow_ && !zoomedY) return;
- isPanning = true;
- var xRange = self.xAxisRange();
- dateRange = xRange[1] - xRange[0];
-
- // Record the range of each y-axis at the start of the drag.
- // If any axis has a valueRange or valueWindow, then we want a 2D pan.
- is2DPan = false;
- for (var i = 0; i < self.axes_.length; i++) {
- var axis = self.axes_[i];
- var yRange = self.yAxisRange(i);
- axis.dragValueRange = yRange[1] - yRange[0];
- var r = self.toDataCoords(null, dragStartY, i);
- axis.draggingValue = r[1];
- if (axis.valueWindow || axis.valueRange) is2DPan = true;
+ // Allow any click within two pixels of the dot.
+ var radius = g.attr_('highlightCircleSize') + 2;
+ if (closestDistance <= 5 * 5) {
+ g.attr_('pointClickCallback')(event, g.selPoints_[closestIdx]);
}
+ }
+ }
+
+ if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) {
+ g.doZoomX_(Math.min(context.dragStartX, context.dragEndX),
+ Math.max(context.dragStartX, context.dragEndX));
+ } else if (regionHeight >= 10 && context.dragDirection == Dygraph.VERTICAL) {
+ g.doZoomY_(Math.min(context.dragStartY, context.dragEndY),
+ Math.max(context.dragStartY, context.dragEndY));
+ } else {
+ g.canvas_.getContext("2d").clearRect(0, 0,
+ g.canvas_.width,
+ g.canvas_.height);
+ }
+ context.dragStartX = null;
+ context.dragStartY = null;
+}
+
+Dygraph.defaultInteractionModel = {
+ // Track the beginning of drag events
+ mousedown: function(event, g, context) {
+ context.initializeMouseDown(event, g, context);
- // TODO(konigsberg): Switch from all this math to toDataCoords?
- // Seems to work for the dragging value.
- draggingDate = (dragStartX / self.width_) * dateRange + xRange[0];
+ if (event.altKey || event.shiftKey) {
+ Dygraph.startPan(event, g, context);
} else {
- isZooming = true;
+ Dygraph.startZoom(event, g, context);
}
- });
+ },
- // If the user releases the mouse button during a drag, but not over the
- // canvas, then it doesn't count as a zooming action.
- Dygraph.addEvent(document, 'mouseup', function(event) {
- if (isZooming || isPanning) {
- isZooming = false;
- dragStartX = null;
- dragStartY = null;
+ // Draw zoom rectangles when the mouse is down and the user moves around
+ mousemove: function(event, g, context) {
+ if (context.isZooming) {
+ Dygraph.moveZoom(event, g, context);
+ } else if (context.isPanning) {
+ Dygraph.movePan(event, g, context);
}
+ },
- if (isPanning) {
- isPanning = false;
- draggingDate = null;
- dateRange = null;
- for (var i = 0; i < self.axes_.length; i++) {
- delete self.axes_[i].draggingValue;
- delete self.axes_[i].dragValueRange;
- }
+ mouseup: function(event, g, context) {
+ if (context.isZooming) {
+ Dygraph.endZoom(event, g, context);
+ } else if (context.isPanning) {
+ Dygraph.endPan(event, g, context);
}
- });
+ },
// Temporarily cancel the dragging event when the mouse leaves the graph
- Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(event) {
- if (isZooming) {
- dragEndX = null;
- dragEndY = null;
+ mouseout: function(event, g, context) {
+ if (context.isZooming) {
+ context.dragEndX = null;
+ context.dragEndY = null;
}
- });
+ },
- // If the mouse is released on the canvas during a drag event, then it's a
- // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
- Dygraph.addEvent(this.mouseEventElement_, 'mouseup', function(event) {
- if (isZooming) {
- isZooming = false;
- dragEndX = getX(event);
- dragEndY = getY(event);
- var regionWidth = Math.abs(dragEndX - dragStartX);
- var regionHeight = Math.abs(dragEndY - dragStartY);
-
- if (regionWidth < 2 && regionHeight < 2 &&
- self.lastx_ != undefined && self.lastx_ != -1) {
- // TODO(danvk): pass along more info about the points, e.g. 'x'
- if (self.attr_('clickCallback') != null) {
- self.attr_('clickCallback')(event, self.lastx_, self.selPoints_);
- }
- if (self.attr_('pointClickCallback')) {
- // check if the click was on a particular point.
- var closestIdx = -1;
- var closestDistance = 0;
- for (var i = 0; i < self.selPoints_.length; i++) {
- var p = self.selPoints_[i];
- var distance = Math.pow(p.canvasx - dragEndX, 2) +
- Math.pow(p.canvasy - dragEndY, 2);
- if (closestIdx == -1 || distance < closestDistance) {
- closestDistance = distance;
- closestIdx = i;
- }
- }
+ // Disable zooming out if panning.
+ dblclick: function(event, g, context) {
+ if (event.altKey || event.shiftKey) {
+ return;
+ }
+ // TODO(konigsberg): replace g.doUnzoom()_ with something that is
+ // friendlier to public use.
+ g.doUnzoom_();
+ }
+};
- // Allow any click within two pixels of the dot.
- var radius = self.attr_('highlightCircleSize') + 2;
- if (closestDistance <= 5 * 5) {
- self.attr_('pointClickCallback')(event, self.selPoints_[closestIdx]);
- }
- }
- }
+Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.defaultInteractionModel;
- if (regionWidth >= 10 && dragDirection == Dygraph.HORIZONTAL) {
- self.doZoomX_(Math.min(dragStartX, dragEndX),
- Math.max(dragStartX, dragEndX));
- } else if (regionHeight >= 10 && dragDirection == Dygraph.VERTICAL){
- self.doZoomY_(Math.min(dragStartY, dragEndY),
- Math.max(dragStartY, dragEndY));
+/**
+ * Set up all the mouse handlers needed to capture dragging behavior for zoom
+ * events.
+ * @private
+ */
+Dygraph.prototype.createDragInterface_ = function() {
+ var context = {
+ // Tracks whether the mouse is down right now
+ isZooming: false,
+ isPanning: false, // is this drag part of a pan?
+ is2DPan: false, // if so, is that pan 1- or 2-dimensional?
+ dragStartX: null,
+ dragStartY: null,
+ dragEndX: null,
+ dragEndY: null,
+ dragDirection: null,
+ prevEndX: null,
+ prevEndY: null,
+ prevDragDirection: null,
+
+ // TODO(danvk): update this comment
+ // draggingDate and draggingValue represent the [date,value] point on the
+ // graph at which the mouse was pressed. As the mouse moves while panning,
+ // the viewport must pan so that the mouse position points to
+ // [draggingDate, draggingValue]
+ draggingDate: null,
+
+ // TODO(danvk): update this comment
+ // The range in second/value units that the viewport encompasses during a
+ // panning operation.
+ dateRange: null,
+
+ // Utility function to convert page-wide coordinates to canvas coords
+ px: 0,
+ py: 0,
+
+ initializeMouseDown: function(event, g, context) {
+ // prevents mouse drags from selecting page text.
+ if (event.preventDefault) {
+ event.preventDefault(); // Firefox, Chrome, etc.
} else {
- self.canvas_.getContext("2d").clearRect(0, 0,
- self.canvas_.width,
- self.canvas_.height);
+ event.returnValue = false; // IE
+ event.cancelBubble = true;
}
- dragStartX = null;
- dragStartY = null;
+ context.px = Dygraph.findPosX(g.canvas_);
+ context.py = Dygraph.findPosY(g.canvas_);
+ context.dragStartX = g.dragGetX_(event, context);
+ context.dragStartY = g.dragGetY_(event, context);
}
+ };
- if (isPanning) {
- isPanning = false;
- is2DPan = false;
- draggingDate = null;
- dateRange = null;
- valueRange = null;
- }
- });
+ var interactionModel = this.attr_("interactionModel");
- // Double-clicking zooms back out
- Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) {
- // Disable zooming out if panning.
- if (event.altKey || event.shiftKey) return;
+ // Self is the graph.
+ var self = this;
+
+ // Function that binds the graph and context to the handler.
+ var bindHandler = function(handler) {
+ return function(event) {
+ handler(event, self, context);
+ };
+ };
- self.doUnzoom_();
+ for (var eventName in interactionModel) {
+ if (!interactionModel.hasOwnProperty(eventName)) continue;
+ Dygraph.addEvent(this.mouseEventElement_, eventName,
+ bindHandler(interactionModel[eventName]));
+ }
+
+ // If the user releases the mouse button during a drag, but not over the
+ // canvas, then it doesn't count as a zooming action.
+ Dygraph.addEvent(document, 'mouseup', function(event) {
+ if (context.isZooming || context.isPanning) {
+ context.isZooming = false;
+ context.dragStartX = null;
+ context.dragStartY = null;
+ }
+
+ if (context.isPanning) {
+ context.isPanning = false;
+ context.draggingDate = null;
+ context.dateRange = null;
+ for (var i = 0; i < self.axes_.length; i++) {
+ delete self.axes_[i].draggingValue;
+ delete self.axes_[i].dragValueRange;
+ }
+ }
});
};
for (var i = 0; i < points.length; i++) {
var point = points[i];
if (point == null) continue;
- var dist = Math.abs(points[i].canvasx - canvasx);
+ var dist = Math.abs(point.canvasx - canvasx);
if (dist > minDist) continue;
minDist = dist;
idx = i;
* @private
*/
Dygraph.dateAxisFormatter = function(date, granularity) {
- if (granularity >= Dygraph.MONTHLY) {
+ if (granularity >= Dygraph.DECADAL) {
+ return date.strftime('%Y');
+ } else if (granularity >= Dygraph.MONTHLY) {
return date.strftime('%b %y');
} else {
var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
Dygraph.BIANNUAL = 17;
Dygraph.ANNUAL = 18;
Dygraph.DECADAL = 19;
-Dygraph.NUM_GRANULARITIES = 20;
+Dygraph.CENTENNIAL = 20;
+Dygraph.NUM_GRANULARITIES = 21;
Dygraph.SHORT_SPACINGS = [];
Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1;
if (granularity == Dygraph.BIANNUAL) num_months = 2;
if (granularity == Dygraph.ANNUAL) num_months = 1;
if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
+ if (granularity == Dygraph.CENTENNIAL) { num_months = 1; year_mod = 100; }
var msInYear = 365.2524 * 24 * 3600 * 1000;
var num_years = 1.0 * (end_time - start_time) / msInYear;
} else if (granularity == Dygraph.DECADAL) {
months = [ 0 ];
year_mod = 10;
+ } else if (granularity == Dygraph.CENTENNIAL) {
+ months = [ 0 ];
+ year_mod = 100;
+ } else {
+ this.warn("Span of dates is too long");
}
var start_year = new Date(start_time).getFullYear();
--- /dev/null
+
+<html>
+ <head>
+ <title>interaction model</title>
+ <!--[if IE]>
+ <script type="text/javascript" src="../excanvas.js"></script>
+ <![endif]-->
+ <script type="text/javascript" src="../strftime/strftime-min.js"></script>
+ <script type="text/javascript" src="../rgbcolor/rgbcolor.js"></script>
+ <script type="text/javascript" src="../dygraph-canvas.js"></script>
+ <script type="text/javascript" src="../dygraph.js"></script>
+ <script type="text/javascript" src="data.js"></script>
+ </head>
+ <body>
+ <table border='1'>
+ <tr><td>
+ <b>Default interaction model</b>
+ <div id="div_g" style="width:600px; height:300px;"></div>
+ </td><td>Zoom: click-drag<br/>Pan: shift-click-drag<br/>Restore zoom level: double-click<br/>
+ </td></tr>
+ <tr><td>
+ <b>No interaction model</b>
+ <div id="div_g2" style="width:600px; height:300px;"></div>
+ </td><td>Click and drag all you like, it won't do anything!</td></tr>
+ <tr><td>
+ <b>Custom interaction model</b>
+
+ <input type="button" value="Unzoom" onclick="unzoomGraph(g3)">
+ <div id="div_g3" style="width:600px; height:300px;"></div>
+ </td><td>
+ Zoom in: double-click, scroll wheel<br/>
+ Zoom out: ctrl-double-click, scroll wheel<br/>
+ Standard Zoom: shift-click-drag
+ Standard Pan: click-drag<br/>
+ Restore zoom level: press button<br/>
+ </td></tr>
+ <tr><td>
+ <b>Fun model!</b>
+ <div id="div_g4" style="width:600px; height:300px;"></div>
+ </td><td>
+ Keep the mouse button pressed, and hover over all points
+ to mark them.
+ </td></tr>
+
+ </table>
+
+ <script type="text/javascript">
+ function downV3(event, g, context) {
+ context.initializeMouseDown(event, g, context);
+ if (event.altKey || event.shiftKey) {
+ Dygraph.startZoom(event, g, context);
+ } else {
+ Dygraph.startPan(event, g, context);
+ }
+ }
+
+ function moveV3(event, g, context) {
+ if (context.isPanning) {
+ Dygraph.movePan(event, g, context);
+ } else if (context.isZooming) {
+ Dygraph.moveZoom(event, g, context);
+ }
+ }
+
+ function upV3(event, g, context) {
+ if (context.isPanning) {
+ Dygraph.endPan(event, g, context);
+ } else if (context.isZooming) {
+ Dygraph.endZoom(event, g, context);
+ }
+ }
+
+ function dblClickV3(event, g, context) {
+ if (event.ctrlKey) {
+ zoom(g, -(1/9));
+ } else {
+ zoom(g, +.1);
+ }
+ }
+
+ function scrollV3(event, g, context) {
+ var normal = event.detail ? event.detail * -1 : event.wheelDelta / 40;
+ // For me the normalized value shows 0.075 for one click. If I took
+ // that verbatim, it would be a 7.5%. I think I'm gonna take 1/10 of that.
+ // (double for left and right side)
+ var percentage = normal / 100;
+
+ zoom(g, percentage);
+ Dygraph.cancelEvent(event);
+ }
+
+ function zoom(g, percentage) {
+ // Adjusts [x, y] toward each other by percentage%
+ function adjustAxis(axis, percentage) {
+ var delta = axis[1] - axis[0];
+ var increment = delta * percentage;
+ return [ axis[0] + increment, axis[1] - increment ];
+ }
+
+ var yAxes = g.yAxisRanges();
+ var newYAxes = [];
+ for (var i = 0; i < yAxes.length; i++) {
+ newYAxes[i] = adjustAxis(yAxes[i], percentage);
+ }
+
+ g.updateOptions({
+ dateWindow: adjustAxis(g.xAxisRange(), percentage),
+ valueRange: newYAxes[0]
+ });
+ }
+
+ var v4Active = false;
+ var v4Canvas = null;
+
+ function downV4(event, g, context) {
+ context.initializeMouseDown(event, g, context);
+ v4Active = true;
+ moveV4(event, g, context); // in case the mouse went down on a data point.
+ }
+
+ var processed = [];
+
+ function moveV4(event, g, context) {
+ var RANGE = 7;
+
+ if (v4Active) {
+ var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(g.graphDiv);
+ var canvasy = Dygraph.pageY(event) - Dygraph.findPosY(g.graphDiv);
+
+ var rows = g.numRows();
+ // Row layout:
+ // [date, [val1, stdev1], [val2, stdev2]]
+ for (var row = 0; row < rows; row++) {
+ var date = g.getValue(row, 0);
+ var x = g.toDomCoords(date, null)[0];
+ var diff = Math.abs(canvasx - x);
+ if (diff < RANGE) {
+ for (var col = 1; col < 3; col++) {
+ // TODO(konigsberg): these will throw exceptions as data is removed.
+ var vals = g.getValue(row, col);
+ if (vals == null) { continue; }
+ var val = vals[0];
+ var y = g.toDomCoords(null, val)[1];
+ var diff2 = Math.abs(canvasy - y);
+ if (diff2 < RANGE) {
+ var found = false;
+ for (var i in processed) {
+ var stored = processed[i];
+ if(stored[0] == row && stored[1] == col) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ processed.push([row, col]);
+ drawV4(x, y);
+ }
+ return;
+ }
+ }
+ // drawV4(false, canvasx, canvasy);
+ }
+ }
+ // drawV4(false, canvasx, canvasy);
+ }
+ }
+
+ function upV4(event, g, context) {
+ if (v4Active) {
+ v4Active = false;
+ }
+ }
+
+ function dblClickV4(event, g, context) {
+ unzoomGraph(g4);
+ }
+
+ function drawV4(x, y) {
+ var ctx = v4Canvas;
+
+ ctx.strokeStyle = "#000000";
+ ctx.fillStyle = "#FFFF00";
+ ctx.beginPath();
+ ctx.arc(x,y,5,0,Math.PI*2,true);
+ ctx.closePath();
+ ctx.stroke();
+ ctx.fill();
+ }
+
+ function captureCanvas(canvas, area, g) {
+ v4Canvas = canvas;
+ }
+
+ var g = new Dygraph(document.getElementById("div_g"),
+ NoisyData, { errorBars : true });
+ var g2 = new Dygraph(document.getElementById("div_g2"),
+ NoisyData, { errorBars : true, interactionModel : {} });
+ var g3 = new Dygraph(document.getElementById("div_g3"),
+ NoisyData, { errorBars : true, interactionModel : {
+ 'mousedown' : downV3,
+ 'mousemove' : moveV3,
+ 'mouseup' : upV3,
+ 'dblclick' : dblClickV3,
+ 'mousewheel' : scrollV3
+ }});
+ var g4 = new Dygraph(document.getElementById("div_g4"),
+ NoisyData, { errorBars : true, drawPoints : true, interactionModel : {
+ 'mousedown' : downV4,
+ 'mousemove' : moveV4,
+ 'mouseup' : upV4,
+ 'dblclick' : dblClickV4,
+ },
+ underlayCallback : captureCanvas
+ });
+
+ function unzoomGraph(g) {
+ g.updateOptions({
+ dateWindow: null,
+ valueRange: null
+ });
+ }
+ </script>
+
+ </body>
+</html>