From: Dan Vanderkam Date: Wed, 15 Dec 2010 23:30:29 +0000 (-0500) Subject: Merge https://github.com/kberg/dygraphs X-Git-Tag: v1.0.0~593^2~7 X-Git-Url: https://adrianiainlam.tk/git/?a=commitdiff_plain;h=158e2643585a524d29894101daaf11735eec8cdb;hp=cd12bba0d1895c8a28eee36771be72e21873f88e;p=dygraphs.git Merge https://github.com/kberg/dygraphs --- diff --git a/dygraph.js b/dygraph.js index 1ed103c..dd4e7b5 100644 --- a/dygraph.js +++ b/dygraph.js @@ -79,6 +79,7 @@ Dygraph.DEFAULT_WIDTH = 480; Dygraph.DEFAULT_HEIGHT = 320; Dygraph.AXIS_LINE_WIDTH = 0.3; + // Default attribute values. Dygraph.DEFAULT_ATTRS = { highlightCircleSize: 3, @@ -127,7 +128,9 @@ Dygraph.DEFAULT_ATTRS = { hideOverlayOnMouseOut: true, stepPlot: false, - avoidMinZero: false + avoidMinZero: false, + + interactionModel: null // will be set to Dygraph.defaultInteractionModel. }; // Various logging levels. @@ -158,7 +161,7 @@ Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) { /** * 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 @@ -430,6 +433,23 @@ Dygraph.addEvent = function(el, evt, fn) { } }; + +// 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 @@ -762,242 +782,348 @@ Dygraph.pageY = function(e) { } }; -/** - * 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; + + context.isPanning = true; + var xRange = g.xAxisRange(); + context.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. + 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; + } + + // 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]; +}; + +// 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); // TODO(danvk): update this comment - // The range in second/value units that the viewport encompasses during a - // panning operation. - var dateRange = null; + // 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. - // 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 }; + var minDate = context.draggingDate - (context.dragEndX / g.width_) * context.dateRange; + var maxDate = minDate + context.dateRange; + g.dateWindow_ = [minDate, maxDate]; - // 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 ]; + // 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 ]; + } + } + + g.drawGraph_(); +} + +// 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; } } - self.drawGraph_(); + // 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 - 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; - } + mousedown : function(event, g, context) { + context.initializeMouseDown(event, g, context); - px = Dygraph.findPosX(self.canvas_); - py = Dygraph.findPosY(self.canvas_); - dragStartX = getX(event); - dragStartY = getY(event); + if (event.altKey || event.shiftKey) { + Dygraph.startPan(event, g, context); + } else { + Dygraph.startZoom(event, g, context); + } + }, - 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; - } + // 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 (!self.dateWindow_ && !zoomedY) return; + }, - isPanning = true; - var xRange = self.xAxisRange(); - dateRange = xRange[1] - xRange[0]; + mouseup : function(event, g, context) { + if (context.isZooming) { + Dygraph.endZoom(event, g, context); + } else if (context.isPanning) { + Dygraph.endPan(event, g, context); + } + }, - // 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; + // Temporarily cancel the dragging event when the mouse leaves the graph + mouseout : function(event, g, context) { + if (context.isZooming) { + context.dragEndX = null; + context.dragEndY = null; } + }, - // TODO(konigsberg): Switch from all this math to toDataCoords? - // Seems to work for the dragging value. - draggingDate = (dragStartX / self.width_) * dateRange + xRange[0]; - } else { - isZooming = true; + // 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_(); } - }); +}; - // 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; - } +Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.defaultInteractionModel; - 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; +/** + * 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 { + event.returnValue = false; // IE + event.cancelBubble = true; } + + context.px = Dygraph.findPosX(g.canvas_); + context.py = Dygraph.findPosY(g.canvas_); + context.dragStartX = g.dragGetX_(event, context); + context.dragStartY = g.dragGetY_(event, 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; - } - }); + var interactionModel = this.attr_("interactionModel"); - // 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; - } - } - // 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]); - } - } - } + // Self is the graph. + var self = this; - 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)); - } else { - self.canvas_.getContext("2d").clearRect(0, 0, - self.canvas_.width, - self.canvas_.height); - } + // Function that binds the graph and context to the handler. + var bindHandler = function(handler) { + return function(event) { + handler(event, self, context); + }; + }; - dragStartX = null; - dragStartY = null; - } + for (var eventName in interactionModel) { + if (!interactionModel.hasOwnProperty(eventName)) continue; + Dygraph.addEvent(this.mouseEventElement_, eventName, + bindHandler(interactionModel[eventName])); + } - if (isPanning) { - isPanning = false; - is2DPan = false; - draggingDate = null; - dateRange = null; - valueRange = null; + // 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; } - }); - - // Double-clicking zooms back out - Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) { - // Disable zooming out if panning. - if (event.altKey || event.shiftKey) return; - self.doUnzoom_(); + 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; + } + } }); }; @@ -1171,7 +1297,7 @@ Dygraph.prototype.mouseMove_ = function(event) { 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; diff --git a/tests/interaction.html b/tests/interaction.html new file mode 100644 index 0000000..082a54e --- /dev/null +++ b/tests/interaction.html @@ -0,0 +1,216 @@ + + + + interaction model + + + + + + + + + + + + + + +
+ Default interaction model +
+
Zoom: click-drag
Pan: shift-click-drag
Restore zoom level: double-click
+
+ No interaction model +
+
Click and drag all you like, it won't do anything!
+ Custom interaction model + + +
+
+ Zoom in: double-click, scroll wheel
+ Zoom out: ctrl-double-click, scroll wheel
+ Pan: click-drag
+ Restore zoom level: press button
+
+ Fun model! +
+
+ Keep the mouse button pressed, and hover over all points + to mark them. +
+ + + + + diff --git a/tests/zoom.html b/tests/zoom.html index 10b0457..2f0cf98 100644 --- a/tests/zoom.html +++ b/tests/zoom.html @@ -40,9 +40,9 @@ document.getElementById("div_g"), NoisyData, { errorBars: true, - zoomCallback : function(minDate, maxDate, yRange) { - showDimensions(minDate, maxDate, yRange); - } + zoomCallback : function(minDate, maxDate, yRanges) { + showDimensions(minDate, maxDate, yRanges); + } } ); @@ -52,12 +52,13 @@ // Pull an initial value for logging. var minDate = g.xAxisRange()[0]; var maxDate = g.xAxisRange()[1]; - var minValue = g.yAxisRange(); - showDimensions(minDate, maxDate, yAxisRange); + var minValue = g.yAxisRange()[0]; + var maxValue = g.yAxisRange()[1]; + showDimensions(minDate, maxDate, [minValue, maxValue]); - function showDimensions(minDate, maxDate, yAxisRange) { + function showDimensions(minDate, maxDate, yRanges) { showXDimensions(minDate, maxDate); - showYDimensions(yAxisRange); + showYDimensions(yRanges); } function showXDimensions(first, second) { @@ -65,9 +66,9 @@ elem.innerHTML = "dateWindow : [" + first + ", "+ second + "]"; } - function showYDimensions(range) { + function showYDimensions(ranges) { var elem = document.getElementById("ydimensions"); - elem.innerHTML = "valueRange : [" + range + "]"; + elem.innerHTML = "valueRange : [" + ranges + "]"; } function zoomGraphX(minDate, maxDate) { @@ -81,7 +82,7 @@ g.updateOptions({ valueRange: [minValue, maxValue] }); - showYDimensions(minValue, maxValue); + showYDimensions(g.yAxisRanges()); } function unzoomGraph() {