<script type="text/javascript" src="../tests/axis_labels.js"></script>
<script type="text/javascript" src="../tests/multi_csv.js"></script>
<script type="text/javascript" src="../tests/to_dom_coords.js"></script>
+ <script type="text/javascript" src="../tests/interaction_model.js"></script>
</head>
<body>
<div id='graph'></div>
g.canvas_.dispatchEvent(event);
};
-DygraphOps.dispatchMouseDown = function(g, x, y, custom) {
- var px = Dygraph.findPosX(g.canvas_);
- var py = Dygraph.findPosY(g.canvas_);
-
- var pageX = px + g.toDomXCoord(x);
- var pageY = py + g.toDomYCoord(y);
+DygraphOps.dispatchMouseDown_Point = function(g, x, y, custom) {
+ var pageX = Dygraph.findPosX(g.canvas_) + x;
+ var pageY = Dygraph.findPosY(g.canvas_) + y;
var opts = {
type : 'mousedown',
var event = DygraphOps.createEvent_(opts, custom);
g.canvas_.dispatchEvent(event);
-};
-
-DygraphOps.dispatchMouseMove = function(g, x, y, custom) {
- var px = Dygraph.findPosX(g.canvas_);
- var py = Dygraph.findPosY(g.canvas_);
+}
- var pageX = px + g.toDomXCoord(x);
- var pageY = py + g.toDomYCoord(y);
+DygraphOps.dispatchMouseMove_Point = function(g, x, y, custom) {
+ var pageX = Dygraph.findPosX(g.canvas_) + x;
+ var pageY = Dygraph.findPosY(g.canvas_) + y;
var opts = {
type : 'mousemove',
g.canvas_.dispatchEvent(event);
};
-DygraphOps.dispatchMouseUp = function(g, x, y, custom) {
- var px = Dygraph.findPosX(g.canvas_);
- var py = Dygraph.findPosY(g.canvas_);
-
- var pageX = px + g.toDomXCoord(x);
- var pageY = py + g.toDomYCoord(y);
+DygraphOps.dispatchMouseUp_Point = function(g, x, y, custom) {
+ var pageX = Dygraph.findPosX(g.canvas_) + x;
+ var pageY = Dygraph.findPosY(g.canvas_) + y;
var opts = {
type : 'mouseup',
var event = DygraphOps.createEvent_(opts, custom);
g.canvas_.dispatchEvent(event);
};
+
+/**
+ * Dispatches a mouse down using the graph's data coordinate system.
+ * (The y value mapped to the first axis.)
+ */
+DygraphOps.dispatchMouseDown = function(g, x, y, custom) {
+ DygraphOps.dispatchMouseDown_Point(
+ g,
+ g.toDomXCoord(x),
+ g.toDomYCoord(y),
+ custom);
+};
+
+/**
+ * Dispatches a mouse move using the graph's data coordinate system.
+ * (The y value mapped to the first axis.)
+ */
+DygraphOps.dispatchMouseMove = function(g, x, y, custom) {
+ DygraphOps.dispatchMouseMove_Point(
+ g,
+ g.toDomXCoord(x),
+ g.toDomYCoord(y),
+ custom);
+};
+
+/**
+ * Dispatches a mouse up using the graph's data coordinate system.
+ * (The y value mapped to the first axis.)
+ */
+DygraphOps.dispatchMouseUp = function(g, x, y, custom) {
+ DygraphOps.dispatchMouseUp_Point(
+ g,
+ g.toDomXCoord(x),
+ g.toDomYCoord(y),
+ custom);
+};
+
--- /dev/null
+/**
+ * @fileoverview Test cases for the interaction model.
+ *
+ * @author konigsberg@google.com (Robert Konigsbrg)
+ */
+var InteractionModelTestCase = TestCase("interaction-model");
+
+InteractionModelTestCase.prototype.setUp = function() {
+ document.body.innerHTML = "<div id='graph'></div>";
+};
+
+InteractionModelTestCase.prototype.tearDown = function() {
+};
+
+var data1 = "X,Y\n" +
+ "20,-1\n" +
+ "21,0\n" +
+ "22,1\n" +
+ "23,0\n";
+
+var data2 =
+ [[1, 10],
+ [2, 20],
+ [3, 30],
+ [4, 40],
+ [5, 120],
+ [6, 50],
+ [7, 70],
+ [8, 90],
+ [9, 50]];
+
+function getXLabels() {
+ var x_labels = document.getElementsByClassName("dygraph-axis-label-x");
+ var ary = [];
+ for (var i = 0; i < x_labels.length; i++) {
+ ary.push(x_labels[i].innerHTML);
+ }
+ return ary;
+}
+
+InteractionModelTestCase.prototype.pan = function(g, xRange, yRange) {
+ var originalXRange = g.xAxisRange();
+ var originalYRange = g.yAxisRange(0);
+
+ DygraphOps.dispatchMouseDown(g, xRange[0], yRange[0]);
+ DygraphOps.dispatchMouseMove(g, xRange[1], yRange[0]); // this is really necessary.
+ DygraphOps.dispatchMouseUp(g, xRange[1], yRange[0]);
+
+ assertEqualsDelta(xRange, g.xAxisRange(), 0.2);
+ // assertEqualsDelta(originalYRange, g.yAxisRange(0), 0.2); // Not true, it's something in the middle.
+
+ var midX = (xRange[1] - xRange[0]) / 2;
+ DygraphOps.dispatchMouseDown(g, midX, yRange[0]);
+ DygraphOps.dispatchMouseMove(g, midX, yRange[1]); // this is really necessary.
+ DygraphOps.dispatchMouseUp(g, midX, yRange[1]);
+
+ assertEqualsDelta(xRange, g.xAxisRange(), 0.2);
+ assertEqualsDelta(yRange, g.yAxisRange(0), 0.2);
+}
+
+/**
+ * This tests that when changing the interaction model so pan is used instead
+ * of zoom as the default behavior, a standard click method is still called.
+ */
+InteractionModelTestCase.prototype.testClickCallbackIsCalled = function() {
+ var clicked;
+
+ var clickCallback = function(event, x) {
+ clicked = x;
+ };
+
+ var graph = document.getElementById("graph");
+ var g = new Dygraph(graph, data1,
+ {
+ width: 100,
+ height : 100,
+ clickCallback : clickCallback
+ });
+
+ DygraphOps.dispatchMouseDown_Point(g, 10, 10);
+ DygraphOps.dispatchMouseMove_Point(g, 10, 10);
+ DygraphOps.dispatchMouseUp_Point(g, 10, 10);
+
+ assertEquals(20, clicked);
+};
+
+/**
+ * This tests that when changing the interaction model so pan is used instead
+ * of zoom as the default behavior, a standard click method is still called.
+ */
+InteractionModelTestCase.prototype.testClickCallbackIsCalledOnCustomPan = function() {
+ var clicked;
+
+ var clickCallback = function(event, x) {
+ clicked = x;
+ };
+
+ function customDown(event, g, context) {
+ context.initializeMouseDown(event, g, context);
+ Dygraph.startPan(event, g, context);
+ }
+
+ function customMove(event, g, context) {
+ Dygraph.movePan(event, g, context);
+ }
+
+ function customUp(event, g, context) {
+ Dygraph.endPan(event, g, context);
+ }
+
+ var opts = {
+ width: 100,
+ height : 100,
+ clickCallback : clickCallback,
+ interactionModel : {
+ 'mousedown' : customDown,
+ 'mousemove' : customMove,
+ 'mouseup' : customUp,
+ }
+ };
+
+ var graph = document.getElementById("graph");
+ var g = new Dygraph(graph, data1, opts);
+
+ DygraphOps.dispatchMouseDown_Point(g, 10, 10);
+ DygraphOps.dispatchMouseMove_Point(g, 10, 10);
+ DygraphOps.dispatchMouseUp_Point(g, 10, 10);
+
+ assertEquals(20, clicked);
+};
+
+InteractionModelTestCase.clickAt = function(g, x, y) {
+ DygraphOps.dispatchMouseDown(g, x, y);
+ DygraphOps.dispatchMouseMove(g, x, y);
+ DygraphOps.dispatchMouseUp(g, x, y);
+}
+
+/**
+ * A sanity test to ensure pointClickCallback is called.
+ */
+InteractionModelTestCase.prototype.testPointClickCallback = function() {
+ var clicked;
+ var g = new Dygraph(document.getElementById("graph"), data2, {
+ pointClickCallback : function(event, point) {
+ clicked = point;
+ }
+ });
+
+ InteractionModelTestCase.clickAt(g, 4, 40);
+
+ assertEquals(4, clicked.xval);
+ assertEquals(40, clicked.yval);
+};
+
+/**
+ * A sanity test to ensure pointClickCallback is not called when out of range.
+ */
+InteractionModelTestCase.prototype.testNoPointClickCallbackWhenOffPoint = function() {
+ var clicked;
+ var g = new Dygraph(document.getElementById("graph"), data2, {
+ pointClickCallback : function(event, point) {
+ clicked = point;
+ }
+ });
+
+ InteractionModelTestCase.clickAt(g, 5, 40);
+
+ assertUndefined(clicked);
+};
+
+/**
+ * Ensures pointClickCallback circle size is taken into account.
+ */
+InteractionModelTestCase.prototype.testPointClickCallback_circleSize = function() {
+ // TODO(konigsberg): Implement.
+};
+
+/**
+ * Ensures that pointClickCallback is called prior to clickCallback
+ */
+InteractionModelTestCase.prototype.testPointClickCallbackCalledPriorToClickCallback = function() {
+ var counter = 0;
+ var pointClicked;
+ var clicked;
+ var g = new Dygraph(document.getElementById("graph"), data2, {
+ pointClickCallback : function(event, point) {
+ counter++;
+ pointClicked = counter;
+ },
+ clickCallback : function(event, point) {
+ counter++;
+ clicked = counter;
+ }
+ });
+
+ InteractionModelTestCase.clickAt(g, 4, 40);
+ assertEquals(1, pointClicked);
+ assertEquals(2, clicked);
+};
+
+/**
+ * Ensures that when there's no pointClickCallback, clicking on a point still calls
+ * clickCallback
+ */
+InteractionModelTestCase.prototype.testClickCallback_clickOnPoint = function() {
+ var clicked;
+ var g = new Dygraph(document.getElementById("graph"), data2, {
+ clickCallback : function(event, point) {
+ clicked = 1;
+ }
+ });
+
+ InteractionModelTestCase.clickAt(g, 4, 40);
+ assertEquals(1, clicked);
+};
+
var originalXRange = g.xAxisRange();
var originalYRange = g.yAxisRange(0);
- // Editing e.shiftKey post construction doesn't work for Firefox. Damn.
DygraphOps.dispatchMouseDown(g, xRange[0], yRange[0]);
DygraphOps.dispatchMouseMove(g, xRange[1], yRange[0]); // this is really necessary.
DygraphOps.dispatchMouseUp(g, xRange[1], yRange[0]);
g.updateOptions({});
- // This currently fails.
- // See http://code.google.com/p/dygraphs/issues/detail?id=192
assertEqualsDelta([11, 18], g.xAxisRange(), 0.1);
- // assertEqualsDelta([35, 40], g.yAxisRange(0), 0.2);
+ assertEqualsDelta([35, 40], g.yAxisRange(0), 0.2);
}
/**
return;
}
Dygraph.update(a, ann[i]);
- if (!a.xval) a.xval = parse(a.x);
+ if (!a.xval) a.xval = parse(a.x, this.dygraph_);
this.annotations.push(a);
}
};
*/
Dygraph.prototype.yAxisRange = function(idx) {
if (typeof(idx) == "undefined") idx = 0;
- if (idx < 0 || idx >= this.axes_.length) return null;
- return [ this.axes_[idx].computedValueRange[0],
- this.axes_[idx].computedValueRange[1] ];
+ if (idx < 0 || idx >= this.axes_.length) {
+ return null;
+ }
+ var axis = this.axes_[idx];
+ return [ axis.computedValueRange[0], axis.computedValueRange[1] ];
};
/**
}
}
- g.drawGraph_();
+ g.drawGraph_(false);
};
/**
* dragStartX/dragStartY/etc. properties). This function modifies the context.
*/
Dygraph.Interaction.endPan = function(event, g, context) {
+ 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) {
+ Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
+ }
+
// TODO(konigsberg): Clear the context data from the axis.
+ // (replace with "context = {}" ?)
// TODO(konigsberg): mouseup should just delete the
// context object, and mousedown should create a new one.
context.isPanning = false;
context.prevDragDirection = context.dragDirection;
};
+Dygraph.Interaction.treatMouseOpAsClick = function(g, event, context) {
+ var clickCallback = g.attr_('clickCallback');
+ var pointClickCallback = g.attr_('pointClickCallback');
+
+ var selectedPoint = null;
+
+ // Find out if the click occurs on a point. This only matters if there's a pointClickCallback.
+ if (pointClickCallback) {
+ var closestIdx = -1;
+ var closestDistance = Number.MAX_VALUE;
+
+ // check if the click was on a particular point.
+ 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;
+ }
+ }
+
+ // Allow any click within two pixels of the dot.
+ var radius = g.attr_('highlightCircleSize') + 2;
+ if (closestDistance <= radius * radius) {
+ selectedPoint = g.selPoints_[closestIdx];
+ }
+ }
+
+ if (selectedPoint) {
+ pointClickCallback(event, selectedPoint);
+ }
+
+ // TODO(danvk): pass along more info about the points, e.g. 'x'
+ if (clickCallback) {
+ clickCallback(event, g.lastx_, g.selPoints_);
+ }
+};
+
/**
* Called in response to an interaction model operation that
* responds to an event that performs a zoom based on previously defined
* dragStartX/dragStartY/etc. properties). This function modifies the context.
*/
Dygraph.Interaction.endZoom = function(event, g, context) {
- // TODO(konigsberg): Refactor or rename this fn -- it deals with clicks, too.
context.isZooming = false;
context.dragEndX = g.dragGetX_(event, context);
context.dragEndY = g.dragGetY_(event, context);
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;
- }
- }
-
- // 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]);
- }
- }
+ Dygraph.Interaction.treatMouseOpAsClick(g, event, context);
}
if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) {
* Update the graph with new data. This method is called when the viewing area
* has changed. If the underlying data or options have changed, predraw_ will
* be called before drawGraph_ is called.
+ *
+ * clearSelection, when undefined or true, causes this.clearSelection to be
+ * called at the end of the draw operation. This should rarely be defined,
+ * and never true (that is it should be undefined most of the time, and
+ * rarely false.)
+ *
* @private
*/
-Dygraph.prototype.drawGraph_ = function() {
+Dygraph.prototype.drawGraph_ = function(clearSelection) {
+ if (typeof(clearSelection) === 'undefined') {
+ clearSelection = true;
+ }
+
var data = this.rawData_;
// This is used to set the second parameter to drawCallback, below.
// Generate a static legend before any particular point is selected.
this.setLegendHTML_();
} else {
- if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) {
- // We should select the point nearest the page x/y here, but it's easier
- // to just clear the selection. This prevents erroneous hover dots from
- // being displayed.
- this.clearSelection();
- } else {
- this.clearSelection();
+ if (clearSelection) {
+ if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) {
+ // We should select the point nearest the page x/y here, but it's easier
+ // to just clear the selection. This prevents erroneous hover dots from
+ // being displayed.
+ this.clearSelection();
+ } else {
+ this.clearSelection();
+ }
}
}
* indices are into the axes_ array.
*/
Dygraph.prototype.computeYAxes_ = function() {
+ // Preserve valueWindow settings if they exist, and if the user hasn't
+ // specified a new valueRange.
+ var valueWindows;
+ if (this.axes_ != undefined && this.user_attrs_.hasOwnProperty("valueRange") == false) {
+ valueWindows = [];
+ for (var index = 0; index < this.axes_.length; index++) {
+ valueWindows.push(this.axes_[index].valueWindow);
+ }
+ }
+
+
this.axes_ = [{ yAxisId : 0, g : this }]; // always have at least one y-axis.
this.seriesToAxisMap_ = {};
if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s];
}
this.seriesToAxisMap_ = seriesToAxisFiltered;
+
+ if (valueWindows != undefined) {
+ // Restore valueWindow settings.
+ for (var index = 0; index < valueWindows.length; index++) {
+ this.axes_[index].valueWindow = valueWindows[index];
+ }
+ }
};
/**
// These functions are all based on MochiKit.
/**
+ * Copies all the properties from o to self.
+ *
* @private
*/
Dygraph.update = function (self, o) {
"xValueParser": {
"default": "parseFloat() or Date.parse()*",
"labels": ["CSV parsing"],
- "type": "function(str) -> number",
+ "type": "function(str[, dygraph]) -> number",
"description": "A function which parses x-values (i.e. the dependent series). Must return a number, even when the values are dates. In this case, millis since epoch are used. This is used primarily for parsing CSV data. *=Dygraphs is slightly more accepting in the dates which it will parse. See code for details."
},
"stackedGraph": {